@veloxts/core 0.3.3 → 0.3.5
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 +697 -16
- package/dist/app.d.ts +67 -10
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +79 -12
- package/dist/app.js.map +1 -1
- package/dist/context.d.ts +26 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +29 -0
- package/dist/context.js.map +1 -1
- package/dist/di/container.d.ts +406 -0
- package/dist/di/container.d.ts.map +1 -0
- package/dist/di/container.js +699 -0
- package/dist/di/container.js.map +1 -0
- package/dist/di/decorators.d.ts +235 -0
- package/dist/di/decorators.d.ts.map +1 -0
- package/dist/di/decorators.js +297 -0
- package/dist/di/decorators.js.map +1 -0
- package/dist/di/index.d.ts +65 -0
- package/dist/di/index.d.ts.map +1 -0
- package/dist/di/index.js +73 -0
- package/dist/di/index.js.map +1 -0
- package/dist/di/providers.d.ts +397 -0
- package/dist/di/providers.d.ts.map +1 -0
- package/dist/di/providers.js +380 -0
- package/dist/di/providers.js.map +1 -0
- package/dist/di/scope.d.ts +230 -0
- package/dist/di/scope.d.ts.map +1 -0
- package/dist/di/scope.js +294 -0
- package/dist/di/scope.js.map +1 -0
- package/dist/di/tokens.d.ts +227 -0
- package/dist/di/tokens.d.ts.map +1 -0
- package/dist/di/tokens.js +192 -0
- package/dist/di/tokens.js.map +1 -0
- package/dist/errors/catalog.d.ts +79 -0
- package/dist/errors/catalog.d.ts.map +1 -0
- package/dist/errors/catalog.js +492 -0
- package/dist/errors/catalog.js.map +1 -0
- package/dist/errors/formatter.d.ts +101 -0
- package/dist/errors/formatter.d.ts.map +1 -0
- package/dist/errors/formatter.js +330 -0
- package/dist/errors/formatter.js.map +1 -0
- package/dist/errors/index.d.ts +14 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +16 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors.d.ts +35 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +57 -4
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -6
- package/dist/index.js.map +1 -1
- package/package.json +14 -2
package/README.md
CHANGED
|
@@ -15,10 +15,10 @@ pnpm add @veloxts/core
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
17
17
|
```typescript
|
|
18
|
-
import {
|
|
18
|
+
import { veloxApp } from '@veloxts/core';
|
|
19
19
|
|
|
20
20
|
// Create application
|
|
21
|
-
const app = await
|
|
21
|
+
const app = await veloxApp({
|
|
22
22
|
port: 3210,
|
|
23
23
|
host: '0.0.0.0',
|
|
24
24
|
logger: true,
|
|
@@ -31,7 +31,7 @@ console.log(`Server running on ${app.address}`);
|
|
|
31
31
|
|
|
32
32
|
## Core API
|
|
33
33
|
|
|
34
|
-
### `
|
|
34
|
+
### `veloxApp(config?)`
|
|
35
35
|
|
|
36
36
|
Creates a new VeloxTS application instance with sensible defaults.
|
|
37
37
|
|
|
@@ -49,7 +49,7 @@ interface VeloxAppConfig {
|
|
|
49
49
|
**Example:**
|
|
50
50
|
|
|
51
51
|
```typescript
|
|
52
|
-
const app = await
|
|
52
|
+
const app = await veloxApp({
|
|
53
53
|
port: 4000,
|
|
54
54
|
host: '127.0.0.1',
|
|
55
55
|
logger: {
|
|
@@ -80,21 +80,21 @@ interface VeloxApp {
|
|
|
80
80
|
start(): Promise<void>; // Start the server
|
|
81
81
|
stop(): Promise<void>; // Stop the server gracefully
|
|
82
82
|
register(plugin, options?): Promise<void>; // Register a plugin
|
|
83
|
-
|
|
83
|
+
beforeShutdown(handler): void; // Add shutdown handler
|
|
84
84
|
}
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
**Example:**
|
|
88
88
|
|
|
89
89
|
```typescript
|
|
90
|
-
const app = await
|
|
90
|
+
const app = await veloxApp();
|
|
91
91
|
|
|
92
92
|
// Register plugins
|
|
93
93
|
await app.register(databasePlugin);
|
|
94
94
|
await app.register(routerPlugin);
|
|
95
95
|
|
|
96
96
|
// Add shutdown hook
|
|
97
|
-
app.
|
|
97
|
+
app.beforeShutdown(async () => {
|
|
98
98
|
console.log('Cleaning up resources...');
|
|
99
99
|
});
|
|
100
100
|
|
|
@@ -205,7 +205,7 @@ declare module '@veloxts/core' {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
// Use in app
|
|
208
|
-
const app = await
|
|
208
|
+
const app = await veloxApp();
|
|
209
209
|
await app.register(databasePlugin);
|
|
210
210
|
```
|
|
211
211
|
|
|
@@ -392,7 +392,7 @@ app.server.get('/users/:id', async (request, reply) => {
|
|
|
392
392
|
### Environment-Based Configuration
|
|
393
393
|
|
|
394
394
|
```typescript
|
|
395
|
-
const app = await
|
|
395
|
+
const app = await veloxApp({
|
|
396
396
|
port: Number(process.env.PORT) || 3210,
|
|
397
397
|
host: process.env.HOST || '0.0.0.0',
|
|
398
398
|
logger: process.env.NODE_ENV !== 'production',
|
|
@@ -405,7 +405,7 @@ const app = await createVeloxApp({
|
|
|
405
405
|
### Production Configuration
|
|
406
406
|
|
|
407
407
|
```typescript
|
|
408
|
-
const app = await
|
|
408
|
+
const app = await veloxApp({
|
|
409
409
|
port: 3210,
|
|
410
410
|
host: '0.0.0.0',
|
|
411
411
|
logger: {
|
|
@@ -426,10 +426,10 @@ const app = await createVeloxApp({
|
|
|
426
426
|
### Graceful Shutdown
|
|
427
427
|
|
|
428
428
|
```typescript
|
|
429
|
-
const app = await
|
|
429
|
+
const app = await veloxApp();
|
|
430
430
|
|
|
431
431
|
// Add custom shutdown handlers
|
|
432
|
-
app.
|
|
432
|
+
app.beforeShutdown(async () => {
|
|
433
433
|
console.log('Cleaning up resources...');
|
|
434
434
|
await database.disconnect();
|
|
435
435
|
await cache.flush();
|
|
@@ -494,7 +494,7 @@ const fastify = Fastify({
|
|
|
494
494
|
trustProxy: true,
|
|
495
495
|
});
|
|
496
496
|
|
|
497
|
-
const app = await
|
|
497
|
+
const app = await veloxApp({ fastify });
|
|
498
498
|
```
|
|
499
499
|
|
|
500
500
|
### Accessing Fastify Directly
|
|
@@ -502,7 +502,7 @@ const app = await createVeloxApp({ fastify });
|
|
|
502
502
|
The underlying Fastify instance is available via `app.server`:
|
|
503
503
|
|
|
504
504
|
```typescript
|
|
505
|
-
const app = await
|
|
505
|
+
const app = await veloxApp();
|
|
506
506
|
|
|
507
507
|
// Add Fastify plugins
|
|
508
508
|
await app.server.register(fastifyCors, {
|
|
@@ -520,7 +520,7 @@ app.server.get('/custom', async (request, reply) => {
|
|
|
520
520
|
### Complete Application Setup
|
|
521
521
|
|
|
522
522
|
```typescript
|
|
523
|
-
import {
|
|
523
|
+
import { veloxApp } from '@veloxts/core';
|
|
524
524
|
import { createDatabasePlugin } from '@veloxts/orm';
|
|
525
525
|
import { registerRestRoutes } from '@veloxts/router';
|
|
526
526
|
import { PrismaClient } from '@prisma/client';
|
|
@@ -528,7 +528,7 @@ import { userProcedures } from './procedures/users';
|
|
|
528
528
|
|
|
529
529
|
// Initialize
|
|
530
530
|
const prisma = new PrismaClient();
|
|
531
|
-
const app = await
|
|
531
|
+
const app = await veloxApp({
|
|
532
532
|
port: Number(process.env.PORT) || 3210,
|
|
533
533
|
logger: true,
|
|
534
534
|
});
|
|
@@ -558,6 +558,687 @@ await app.start();
|
|
|
558
558
|
console.log(`Server running at ${app.address}`);
|
|
559
559
|
```
|
|
560
560
|
|
|
561
|
+
## Dependency Injection
|
|
562
|
+
|
|
563
|
+
VeloxTS provides a powerful, type-safe dependency injection container inspired by Angular and NestJS. The DI system enables automatic constructor injection, lifecycle management, and clean separation of concerns.
|
|
564
|
+
|
|
565
|
+
### Overview
|
|
566
|
+
|
|
567
|
+
The DI container manages service creation, dependency resolution, and lifecycle:
|
|
568
|
+
|
|
569
|
+
- **Automatic constructor injection** via TypeScript decorators
|
|
570
|
+
- **Multiple provider types**: classes, factories, values, aliases
|
|
571
|
+
- **Lifecycle scopes**: singleton, transient, request-scoped
|
|
572
|
+
- **Type safety**: Full TypeScript inference without code generation
|
|
573
|
+
- **Circular dependency detection**
|
|
574
|
+
- **Fastify integration** for request-scoped services
|
|
575
|
+
|
|
576
|
+
### Accessing the Container
|
|
577
|
+
|
|
578
|
+
Every VeloxApp instance has a DI container available:
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
const app = await veloxApp();
|
|
582
|
+
|
|
583
|
+
// Access the container
|
|
584
|
+
app.container.register({ /* ... */ });
|
|
585
|
+
const service = app.container.resolve(MyService);
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Tokens
|
|
589
|
+
|
|
590
|
+
Tokens are unique identifiers for services. VeloxTS supports three token types:
|
|
591
|
+
|
|
592
|
+
#### Class Tokens
|
|
593
|
+
|
|
594
|
+
Use the class itself as a token:
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
@Injectable()
|
|
598
|
+
class UserService {
|
|
599
|
+
getUsers() { /* ... */ }
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Register with class token
|
|
603
|
+
app.container.register({
|
|
604
|
+
provide: UserService,
|
|
605
|
+
useClass: UserService
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Resolve with class token
|
|
609
|
+
const userService = app.container.resolve(UserService);
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
#### String Tokens
|
|
613
|
+
|
|
614
|
+
Use string literals for named services:
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
import { token } from '@veloxts/core';
|
|
618
|
+
|
|
619
|
+
interface DatabaseClient {
|
|
620
|
+
query(sql: string): Promise<unknown>;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const DATABASE = token<DatabaseClient>('DATABASE');
|
|
624
|
+
|
|
625
|
+
app.container.register({
|
|
626
|
+
provide: DATABASE,
|
|
627
|
+
useFactory: () => createDatabaseClient()
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const db = app.container.resolve(DATABASE);
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
#### Symbol Tokens
|
|
634
|
+
|
|
635
|
+
Use symbols for guaranteed uniqueness:
|
|
636
|
+
|
|
637
|
+
```typescript
|
|
638
|
+
import { token } from '@veloxts/core';
|
|
639
|
+
|
|
640
|
+
interface Logger {
|
|
641
|
+
log(message: string): void;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const LOGGER = token.symbol<Logger>('Logger');
|
|
645
|
+
|
|
646
|
+
app.container.register({
|
|
647
|
+
provide: LOGGER,
|
|
648
|
+
useClass: ConsoleLogger
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const logger = app.container.resolve(LOGGER);
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Provider Types
|
|
655
|
+
|
|
656
|
+
The container supports four provider types for different use cases. For common patterns, VeloxTS provides succinct provider helpers that simplify registration.
|
|
657
|
+
|
|
658
|
+
#### Succinct Provider Helpers
|
|
659
|
+
|
|
660
|
+
VeloxTS provides convenient helpers for common provider patterns:
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
import { singleton, scoped, transient, value, factory } from '@veloxts/core';
|
|
664
|
+
|
|
665
|
+
// Singleton scope (one instance for entire app)
|
|
666
|
+
app.container.register(singleton(ConfigService));
|
|
667
|
+
|
|
668
|
+
// Request scope (one instance per HTTP request)
|
|
669
|
+
app.container.register(scoped(UserContext));
|
|
670
|
+
|
|
671
|
+
// Transient scope (new instance every time)
|
|
672
|
+
app.container.register(transient(RequestIdGenerator));
|
|
673
|
+
|
|
674
|
+
// Value provider (provide existing value)
|
|
675
|
+
app.container.register(value(CONFIG, { port: 3210 }));
|
|
676
|
+
|
|
677
|
+
// Factory provider (use factory function)
|
|
678
|
+
app.container.register(
|
|
679
|
+
factory(DATABASE, (config: ConfigService) => {
|
|
680
|
+
return new PrismaClient({ datasources: { db: { url: config.databaseUrl } } });
|
|
681
|
+
}, [ConfigService])
|
|
682
|
+
);
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
These helpers are equivalent to the full provider syntax but more concise and readable.
|
|
686
|
+
|
|
687
|
+
#### Class Provider
|
|
688
|
+
|
|
689
|
+
Instantiate a class with automatic dependency injection:
|
|
690
|
+
|
|
691
|
+
```typescript
|
|
692
|
+
@Injectable()
|
|
693
|
+
class UserService {
|
|
694
|
+
constructor(
|
|
695
|
+
private db: DatabaseClient,
|
|
696
|
+
private logger: Logger
|
|
697
|
+
) {}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Using succinct helper (recommended)
|
|
701
|
+
app.container.register(singleton(UserService));
|
|
702
|
+
|
|
703
|
+
// Or using full syntax
|
|
704
|
+
app.container.register({
|
|
705
|
+
provide: UserService,
|
|
706
|
+
useClass: UserService,
|
|
707
|
+
scope: Scope.SINGLETON
|
|
708
|
+
});
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
#### Factory Provider
|
|
712
|
+
|
|
713
|
+
Use a factory function to create instances:
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
// Using succinct helper (recommended)
|
|
717
|
+
app.container.register(
|
|
718
|
+
factory(DATABASE, (config: ConfigService) => {
|
|
719
|
+
return createDatabaseClient(config.databaseUrl);
|
|
720
|
+
}, [ConfigService])
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
// Or using full syntax
|
|
724
|
+
app.container.register({
|
|
725
|
+
provide: DATABASE,
|
|
726
|
+
useFactory: (config: ConfigService) => {
|
|
727
|
+
return createDatabaseClient(config.databaseUrl);
|
|
728
|
+
},
|
|
729
|
+
inject: [ConfigService],
|
|
730
|
+
scope: Scope.SINGLETON
|
|
731
|
+
});
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
#### Value Provider
|
|
735
|
+
|
|
736
|
+
Provide an existing value directly:
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
const CONFIG = token<AppConfig>('CONFIG');
|
|
740
|
+
|
|
741
|
+
// Using succinct helper (recommended)
|
|
742
|
+
app.container.register(
|
|
743
|
+
value(CONFIG, {
|
|
744
|
+
port: 3210,
|
|
745
|
+
host: 'localhost',
|
|
746
|
+
debug: true
|
|
747
|
+
})
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
// Or using full syntax
|
|
751
|
+
app.container.register({
|
|
752
|
+
provide: CONFIG,
|
|
753
|
+
useValue: {
|
|
754
|
+
port: 3210,
|
|
755
|
+
host: 'localhost',
|
|
756
|
+
debug: true
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
#### Existing Provider (Alias)
|
|
762
|
+
|
|
763
|
+
Create an alias to another token:
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
// Register concrete implementation
|
|
767
|
+
app.container.register({
|
|
768
|
+
provide: ConsoleLogger,
|
|
769
|
+
useClass: ConsoleLogger
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// Create an alias
|
|
773
|
+
app.container.register({
|
|
774
|
+
provide: LOGGER,
|
|
775
|
+
useExisting: ConsoleLogger
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// Both resolve to the same instance
|
|
779
|
+
const logger1 = app.container.resolve(LOGGER);
|
|
780
|
+
const logger2 = app.container.resolve(ConsoleLogger);
|
|
781
|
+
// logger1 === logger2
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
### Lifecycle Scopes
|
|
785
|
+
|
|
786
|
+
Scopes determine how service instances are created and shared.
|
|
787
|
+
|
|
788
|
+
#### Singleton Scope
|
|
789
|
+
|
|
790
|
+
One instance for the entire application (default):
|
|
791
|
+
|
|
792
|
+
```typescript
|
|
793
|
+
@Injectable({ scope: Scope.SINGLETON })
|
|
794
|
+
class ConfigService {
|
|
795
|
+
// Shared across all requests
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Register using succinct helper
|
|
799
|
+
app.container.register(singleton(ConfigService));
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
**Best for:**
|
|
803
|
+
- Configuration services
|
|
804
|
+
- Database connection pools
|
|
805
|
+
- Cache clients
|
|
806
|
+
- Stateless utility services
|
|
807
|
+
|
|
808
|
+
#### Transient Scope
|
|
809
|
+
|
|
810
|
+
New instance on every resolution:
|
|
811
|
+
|
|
812
|
+
```typescript
|
|
813
|
+
@Injectable({ scope: Scope.TRANSIENT })
|
|
814
|
+
class RequestIdGenerator {
|
|
815
|
+
readonly id = crypto.randomUUID();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Register using succinct helper
|
|
819
|
+
app.container.register(transient(RequestIdGenerator));
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
**Best for:**
|
|
823
|
+
- Services with mutable state
|
|
824
|
+
- Factories that produce unique objects
|
|
825
|
+
- Services where isolation is critical
|
|
826
|
+
|
|
827
|
+
#### Request Scope
|
|
828
|
+
|
|
829
|
+
One instance per HTTP request:
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
833
|
+
class UserContext {
|
|
834
|
+
constructor(private request: FastifyRequest) {}
|
|
835
|
+
|
|
836
|
+
get userId(): string {
|
|
837
|
+
return this.request.user?.id;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Register using succinct helper
|
|
842
|
+
app.container.register(scoped(UserContext));
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
**Best for:**
|
|
846
|
+
- User context/session data
|
|
847
|
+
- Request-specific caching
|
|
848
|
+
- Transaction management
|
|
849
|
+
- Audit logging with request context
|
|
850
|
+
|
|
851
|
+
**Note:** Request-scoped services require a Fastify request context. Resolving them outside a request handler throws an error.
|
|
852
|
+
|
|
853
|
+
### Decorators
|
|
854
|
+
|
|
855
|
+
Use TypeScript decorators for automatic dependency injection.
|
|
856
|
+
|
|
857
|
+
#### Prerequisites
|
|
858
|
+
|
|
859
|
+
Enable decorators in `tsconfig.json`:
|
|
860
|
+
|
|
861
|
+
```json
|
|
862
|
+
{
|
|
863
|
+
"compilerOptions": {
|
|
864
|
+
"experimentalDecorators": true,
|
|
865
|
+
"emitDecoratorMetadata": true
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
Import `reflect-metadata` at your app's entry point:
|
|
871
|
+
|
|
872
|
+
```typescript
|
|
873
|
+
import 'reflect-metadata';
|
|
874
|
+
import { veloxApp } from '@veloxts/core';
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
#### @Injectable()
|
|
878
|
+
|
|
879
|
+
Marks a class as injectable:
|
|
880
|
+
|
|
881
|
+
```typescript
|
|
882
|
+
@Injectable()
|
|
883
|
+
class UserService {
|
|
884
|
+
constructor(private db: DatabaseClient) {}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
888
|
+
class RequestLogger {
|
|
889
|
+
constructor(private request: FastifyRequest) {}
|
|
890
|
+
}
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
#### @Inject()
|
|
894
|
+
|
|
895
|
+
Explicitly specifies the injection token:
|
|
896
|
+
|
|
897
|
+
```typescript
|
|
898
|
+
const DATABASE = token<DatabaseClient>('DATABASE');
|
|
899
|
+
|
|
900
|
+
@Injectable()
|
|
901
|
+
class UserService {
|
|
902
|
+
constructor(
|
|
903
|
+
@Inject(DATABASE) private db: DatabaseClient,
|
|
904
|
+
private config: ConfigService // Auto-injected by class token
|
|
905
|
+
) {}
|
|
906
|
+
}
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
Use `@Inject()` when:
|
|
910
|
+
- Injecting by string or symbol token
|
|
911
|
+
- Injecting an interface (TypeScript interfaces are erased at runtime)
|
|
912
|
+
- Automatic type resolution doesn't work
|
|
913
|
+
|
|
914
|
+
#### @Optional()
|
|
915
|
+
|
|
916
|
+
Marks a dependency as optional:
|
|
917
|
+
|
|
918
|
+
```typescript
|
|
919
|
+
@Injectable()
|
|
920
|
+
class NotificationService {
|
|
921
|
+
constructor(
|
|
922
|
+
@Optional() private emailService?: EmailService,
|
|
923
|
+
@Optional() @Inject(SMS_SERVICE) private smsService?: SmsService
|
|
924
|
+
) {}
|
|
925
|
+
|
|
926
|
+
notify(message: string) {
|
|
927
|
+
// Gracefully handle missing services
|
|
928
|
+
this.emailService?.send(message);
|
|
929
|
+
this.smsService?.send(message);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
If an optional dependency cannot be resolved, `undefined` is injected instead of throwing an error.
|
|
935
|
+
|
|
936
|
+
### Container API
|
|
937
|
+
|
|
938
|
+
#### Registering Services
|
|
939
|
+
|
|
940
|
+
Register a single provider:
|
|
941
|
+
|
|
942
|
+
```typescript
|
|
943
|
+
// Using succinct helper (recommended)
|
|
944
|
+
app.container.register(scoped(UserService));
|
|
945
|
+
|
|
946
|
+
// Or using full syntax
|
|
947
|
+
app.container.register({
|
|
948
|
+
provide: UserService,
|
|
949
|
+
useClass: UserService,
|
|
950
|
+
scope: Scope.REQUEST
|
|
951
|
+
});
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
Register multiple providers:
|
|
955
|
+
|
|
956
|
+
```typescript
|
|
957
|
+
// Using succinct helpers (recommended)
|
|
958
|
+
app.container.registerMany([
|
|
959
|
+
singleton(UserService),
|
|
960
|
+
singleton(PostService),
|
|
961
|
+
value(CONFIG, appConfig)
|
|
962
|
+
]);
|
|
963
|
+
|
|
964
|
+
// Or using full syntax
|
|
965
|
+
app.container.registerMany([
|
|
966
|
+
{ provide: UserService, useClass: UserService },
|
|
967
|
+
{ provide: PostService, useClass: PostService },
|
|
968
|
+
{ provide: CONFIG, useValue: appConfig }
|
|
969
|
+
]);
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
#### Resolving Services
|
|
973
|
+
|
|
974
|
+
Synchronous resolution:
|
|
975
|
+
|
|
976
|
+
```typescript
|
|
977
|
+
const userService = app.container.resolve(UserService);
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
Asynchronous resolution (for async factories):
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
// Using succinct helper (recommended)
|
|
984
|
+
app.container.register(
|
|
985
|
+
factory(DATABASE, async (config: ConfigService) => {
|
|
986
|
+
const client = createClient(config.dbUrl);
|
|
987
|
+
await client.connect();
|
|
988
|
+
return client;
|
|
989
|
+
}, [ConfigService])
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
const db = await app.container.resolveAsync(DATABASE);
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
Optional resolution (returns `undefined` if not found):
|
|
996
|
+
|
|
997
|
+
```typescript
|
|
998
|
+
const service = app.container.resolveOptional(OptionalService);
|
|
999
|
+
if (service) {
|
|
1000
|
+
service.doSomething();
|
|
1001
|
+
}
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
#### Request Context
|
|
1005
|
+
|
|
1006
|
+
Resolve request-scoped services with context:
|
|
1007
|
+
|
|
1008
|
+
```typescript
|
|
1009
|
+
app.server.get('/users', async (request, reply) => {
|
|
1010
|
+
const ctx = Container.createContext(request);
|
|
1011
|
+
const userContext = app.container.resolve(UserContext, ctx);
|
|
1012
|
+
|
|
1013
|
+
return { userId: userContext.userId };
|
|
1014
|
+
});
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
### Integration with VeloxApp
|
|
1018
|
+
|
|
1019
|
+
The container is automatically attached to the Fastify server for request-scoped services:
|
|
1020
|
+
|
|
1021
|
+
```typescript
|
|
1022
|
+
const app = await veloxApp();
|
|
1023
|
+
|
|
1024
|
+
// Container is already attached to app.server
|
|
1025
|
+
// Request-scoped services work automatically
|
|
1026
|
+
|
|
1027
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
1028
|
+
class RequestLogger {
|
|
1029
|
+
constructor(private request: FastifyRequest) {}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Register using succinct helper
|
|
1033
|
+
app.container.register(scoped(RequestLogger));
|
|
1034
|
+
|
|
1035
|
+
app.server.get('/log', async (request, reply) => {
|
|
1036
|
+
const ctx = Container.createContext(request);
|
|
1037
|
+
const logger = app.container.resolve(RequestLogger, ctx);
|
|
1038
|
+
logger.log('Request received');
|
|
1039
|
+
return { ok: true };
|
|
1040
|
+
});
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
### Practical Examples
|
|
1044
|
+
|
|
1045
|
+
#### Complete Service Layer
|
|
1046
|
+
|
|
1047
|
+
```typescript
|
|
1048
|
+
import 'reflect-metadata';
|
|
1049
|
+
import {
|
|
1050
|
+
veloxApp,
|
|
1051
|
+
Injectable,
|
|
1052
|
+
Inject,
|
|
1053
|
+
Scope,
|
|
1054
|
+
token,
|
|
1055
|
+
singleton,
|
|
1056
|
+
scoped,
|
|
1057
|
+
factory
|
|
1058
|
+
} from '@veloxts/core';
|
|
1059
|
+
import { PrismaClient } from '@prisma/client';
|
|
1060
|
+
|
|
1061
|
+
// Define tokens
|
|
1062
|
+
const DATABASE = token<PrismaClient>('DATABASE');
|
|
1063
|
+
const CONFIG = token<AppConfig>('CONFIG');
|
|
1064
|
+
|
|
1065
|
+
// Configuration service
|
|
1066
|
+
@Injectable()
|
|
1067
|
+
class ConfigService {
|
|
1068
|
+
get databaseUrl(): string {
|
|
1069
|
+
return process.env.DATABASE_URL!;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Database service
|
|
1074
|
+
@Injectable()
|
|
1075
|
+
class DatabaseService {
|
|
1076
|
+
constructor(@Inject(DATABASE) private db: PrismaClient) {}
|
|
1077
|
+
|
|
1078
|
+
async findUser(id: string) {
|
|
1079
|
+
return this.db.user.findUnique({ where: { id } });
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Business logic service
|
|
1084
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
1085
|
+
class UserService {
|
|
1086
|
+
constructor(
|
|
1087
|
+
private database: DatabaseService,
|
|
1088
|
+
private config: ConfigService
|
|
1089
|
+
) {}
|
|
1090
|
+
|
|
1091
|
+
async getUser(id: string) {
|
|
1092
|
+
return this.database.findUser(id);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Create app
|
|
1097
|
+
const app = await veloxApp();
|
|
1098
|
+
|
|
1099
|
+
// Register services using succinct helpers
|
|
1100
|
+
app.container.register(
|
|
1101
|
+
factory(DATABASE, (config: ConfigService) => {
|
|
1102
|
+
return new PrismaClient({
|
|
1103
|
+
datasources: { db: { url: config.databaseUrl } }
|
|
1104
|
+
});
|
|
1105
|
+
}, [ConfigService])
|
|
1106
|
+
);
|
|
1107
|
+
|
|
1108
|
+
app.container.register(singleton(ConfigService));
|
|
1109
|
+
app.container.register(singleton(DatabaseService));
|
|
1110
|
+
app.container.register(scoped(UserService));
|
|
1111
|
+
|
|
1112
|
+
// Use in routes
|
|
1113
|
+
app.server.get('/users/:id', async (request, reply) => {
|
|
1114
|
+
const ctx = Container.createContext(request);
|
|
1115
|
+
const userService = app.container.resolve(UserService, ctx);
|
|
1116
|
+
|
|
1117
|
+
const user = await userService.getUser(request.params.id);
|
|
1118
|
+
return user;
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
await app.start();
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
#### Testing with Child Containers
|
|
1125
|
+
|
|
1126
|
+
```typescript
|
|
1127
|
+
import { createContainer, asClass } from '@veloxts/core';
|
|
1128
|
+
|
|
1129
|
+
// Create a child container for testing
|
|
1130
|
+
const testContainer = app.container.createChild();
|
|
1131
|
+
|
|
1132
|
+
// Override services with mocks
|
|
1133
|
+
class MockDatabaseService {
|
|
1134
|
+
async findUser(id: string) {
|
|
1135
|
+
return { id, name: 'Test User' };
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
testContainer.register({
|
|
1140
|
+
provide: DatabaseService,
|
|
1141
|
+
useClass: MockDatabaseService
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// Test with mocked dependencies
|
|
1145
|
+
const userService = testContainer.resolve(UserService);
|
|
1146
|
+
const user = await userService.getUser('123');
|
|
1147
|
+
// user comes from MockDatabaseService
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
#### Auto-Registration
|
|
1151
|
+
|
|
1152
|
+
Enable auto-registration to automatically register `@Injectable` classes:
|
|
1153
|
+
|
|
1154
|
+
```typescript
|
|
1155
|
+
const app = await createVeloxApp();
|
|
1156
|
+
const autoContainer = createContainer({ autoRegister: true });
|
|
1157
|
+
|
|
1158
|
+
@Injectable()
|
|
1159
|
+
class AutoService {}
|
|
1160
|
+
|
|
1161
|
+
// No need to manually register
|
|
1162
|
+
const service = autoContainer.resolve(AutoService);
|
|
1163
|
+
// Automatically registers and resolves
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
### Advanced Features
|
|
1167
|
+
|
|
1168
|
+
#### Circular Dependency Detection
|
|
1169
|
+
|
|
1170
|
+
The container detects circular dependencies:
|
|
1171
|
+
|
|
1172
|
+
```typescript
|
|
1173
|
+
@Injectable()
|
|
1174
|
+
class ServiceA {
|
|
1175
|
+
constructor(private b: ServiceB) {}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
@Injectable()
|
|
1179
|
+
class ServiceB {
|
|
1180
|
+
constructor(private a: ServiceA) {}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Throws: Circular dependency detected: ServiceA -> ServiceB -> ServiceA
|
|
1184
|
+
const service = app.container.resolve(ServiceA);
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
#### Container Hierarchy
|
|
1188
|
+
|
|
1189
|
+
Create parent-child container hierarchies:
|
|
1190
|
+
|
|
1191
|
+
```typescript
|
|
1192
|
+
const parentContainer = createContainer();
|
|
1193
|
+
parentContainer.register({
|
|
1194
|
+
provide: ConfigService,
|
|
1195
|
+
useClass: ConfigService
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
const childContainer = parentContainer.createChild();
|
|
1199
|
+
// Child inherits ConfigService from parent
|
|
1200
|
+
// Can override with its own providers
|
|
1201
|
+
|
|
1202
|
+
childContainer.register({
|
|
1203
|
+
provide: UserService,
|
|
1204
|
+
useClass: UserService
|
|
1205
|
+
});
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
#### Debug Information
|
|
1209
|
+
|
|
1210
|
+
Get container statistics:
|
|
1211
|
+
|
|
1212
|
+
```typescript
|
|
1213
|
+
const info = app.container.getDebugInfo();
|
|
1214
|
+
console.log(info);
|
|
1215
|
+
// {
|
|
1216
|
+
// providerCount: 5,
|
|
1217
|
+
// providers: [
|
|
1218
|
+
// 'class(UserService, request)',
|
|
1219
|
+
// 'factory(DATABASE, singleton)',
|
|
1220
|
+
// 'value(CONFIG)',
|
|
1221
|
+
// 'existing(LOGGER => ConsoleLogger)'
|
|
1222
|
+
// ],
|
|
1223
|
+
// hasParent: false,
|
|
1224
|
+
// autoRegister: false
|
|
1225
|
+
// }
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
### Best Practices
|
|
1229
|
+
|
|
1230
|
+
1. **Use tokens for interfaces**: Create string or symbol tokens for interface types since TypeScript interfaces don't exist at runtime.
|
|
1231
|
+
|
|
1232
|
+
2. **Prefer singleton scope**: Use `Scope.SINGLETON` by default for stateless services to improve performance.
|
|
1233
|
+
|
|
1234
|
+
3. **Use request scope for user context**: Store user authentication, request-specific state, and transactions in request-scoped services.
|
|
1235
|
+
|
|
1236
|
+
4. **Avoid circular dependencies**: Refactor shared logic into a third service to break circular dependencies.
|
|
1237
|
+
|
|
1238
|
+
5. **Use `@Inject()` for clarity**: Explicitly specify tokens with `@Inject()` to make dependencies clear and avoid runtime type resolution issues.
|
|
1239
|
+
|
|
1240
|
+
6. **Test with child containers**: Create child containers in tests to override services with mocks without affecting the main container.
|
|
1241
|
+
|
|
561
1242
|
## Related Packages
|
|
562
1243
|
|
|
563
1244
|
- [@veloxts/router](/packages/router) - Procedure-based routing
|