@adimm/x-injection 3.0.0 → 3.0.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 +2400 -246
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,40 +13,287 @@
|
|
|
13
13
|
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
|
+
**A powerful, modular dependency injection library for TypeScript** — Built on [InversifyJS](https://github.com/inversify/InversifyJS), inspired by [NestJS](https://github.com/nestjs/nest)'s elegant module architecture.
|
|
17
|
+
|
|
16
18
|
## Table of Contents
|
|
17
19
|
|
|
18
20
|
- [Table of Contents](#table-of-contents)
|
|
21
|
+
- [What Problems Does This Solve?](#what-problems-does-this-solve)
|
|
22
|
+
- [Problem 1: Manual Dependency Wiring](#problem-1-manual-dependency-wiring)
|
|
23
|
+
- [Problem 2: Tight Coupling and Testing Difficulty](#problem-2-tight-coupling-and-testing-difficulty)
|
|
24
|
+
- [Problem 3: Lack of Encapsulation](#problem-3-lack-of-encapsulation)
|
|
25
|
+
- [Problem 4: Lifecycle Management Complexity](#problem-4-lifecycle-management-complexity)
|
|
19
26
|
- [Overview](#overview)
|
|
20
27
|
- [Features](#features)
|
|
21
28
|
- [Installation](#installation)
|
|
22
29
|
- [Quick Start](#quick-start)
|
|
23
|
-
- [
|
|
30
|
+
- [Core Concepts](#core-concepts)
|
|
31
|
+
- [Services with @Injectable](#services-with-injectable)
|
|
32
|
+
- [Modules](#modules)
|
|
33
|
+
- [Blueprints](#blueprints)
|
|
34
|
+
- [AppModule](#appmodule)
|
|
35
|
+
- [OOP-Style Modules with ProviderModuleClass](#oop-style-modules-with-providermoduleclass)
|
|
24
36
|
- [Basic OOP Module](#basic-oop-module)
|
|
25
37
|
- [Advanced OOP Patterns](#advanced-oop-patterns)
|
|
26
38
|
- [When to Use OOP vs Functional](#when-to-use-oop-vs-functional)
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
39
|
+
- [Provider Tokens](#provider-tokens)
|
|
40
|
+
- [1. Class Token](#1-class-token)
|
|
41
|
+
- [2. Class Token with Substitution](#2-class-token-with-substitution)
|
|
42
|
+
- [3. Value Token](#3-value-token)
|
|
43
|
+
- [4. Factory Token](#4-factory-token)
|
|
32
44
|
- [Injection Scopes](#injection-scopes)
|
|
33
45
|
- [Singleton (Default)](#singleton-default)
|
|
34
46
|
- [Transient](#transient)
|
|
35
47
|
- [Request](#request)
|
|
48
|
+
- [Scope Priority Order](#scope-priority-order)
|
|
36
49
|
- [Module System](#module-system)
|
|
37
50
|
- [Import/Export Pattern](#importexport-pattern)
|
|
38
51
|
- [Re-exporting Modules](#re-exporting-modules)
|
|
39
52
|
- [Dynamic Module Updates](#dynamic-module-updates)
|
|
40
53
|
- [Global Modules](#global-modules)
|
|
41
|
-
- [
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
54
|
+
- [Dependency Injection](#dependency-injection)
|
|
55
|
+
- [Constructor Injection](#constructor-injection)
|
|
56
|
+
- [@Inject Decorator](#inject-decorator)
|
|
57
|
+
- [@MultiInject Decorator](#multiinject-decorator)
|
|
58
|
+
- [Optional Dependencies](#optional-dependencies)
|
|
59
|
+
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
60
|
+
- [onReady Hook](#onready-hook)
|
|
61
|
+
- [onReset Hook](#onreset-hook)
|
|
62
|
+
- [onDispose Hook](#ondispose-hook)
|
|
63
|
+
- [Events System](#events-system)
|
|
64
|
+
- [Subscribing to Events](#subscribing-to-events)
|
|
65
|
+
- [Available Event Types](#available-event-types)
|
|
66
|
+
- [Event Use Cases](#event-use-cases)
|
|
67
|
+
- [Middlewares](#middlewares)
|
|
68
|
+
- [BeforeGet Middleware](#beforeget-middleware)
|
|
69
|
+
- [BeforeAddProvider Middleware](#beforeaddprovider-middleware)
|
|
70
|
+
- [BeforeAddImport Middleware](#beforeaddimport-middleware)
|
|
71
|
+
- [OnExportAccess Middleware](#onexportaccess-middleware)
|
|
72
|
+
- [BeforeRemoveImport Middleware](#beforeremoveimport-middleware)
|
|
73
|
+
- [BeforeRemoveProvider Middleware](#beforeremoveprovider-middleware)
|
|
74
|
+
- [BeforeRemoveExport Middleware](#beforeremoveexport-middleware)
|
|
75
|
+
- [All Available Middleware Types](#all-available-middleware-types)
|
|
44
76
|
- [Testing](#testing)
|
|
77
|
+
- [Blueprint Cloning](#blueprint-cloning)
|
|
78
|
+
- [Provider Substitution](#provider-substitution)
|
|
79
|
+
- [Mocking Services](#mocking-services)
|
|
80
|
+
- [Advanced Module API](#advanced-module-api)
|
|
81
|
+
- [Query Methods](#query-methods)
|
|
82
|
+
- [Multiple Provider Binding](#multiple-provider-binding)
|
|
83
|
+
- [Batch Resolution with getMany()](#batch-resolution-with-getmany)
|
|
45
84
|
- [Resources](#resources)
|
|
46
85
|
- [Contributing](#contributing)
|
|
47
86
|
- [Credits](#credits)
|
|
48
87
|
- [License](#license)
|
|
49
88
|
|
|
89
|
+
## What Problems Does This Solve?
|
|
90
|
+
|
|
91
|
+
Modern applications face several dependency management challenges. Let's examine these problems and how xInjection solves them.
|
|
92
|
+
|
|
93
|
+
### Problem 1: Manual Dependency Wiring
|
|
94
|
+
|
|
95
|
+
**Without xInjection:**
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// Manually creating and wiring dependencies
|
|
99
|
+
class DatabaseService {
|
|
100
|
+
constructor(private config: ConfigService) {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class UserRepository {
|
|
104
|
+
constructor(private db: DatabaseService) {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class AuthService {
|
|
108
|
+
constructor(private userRepo: UserRepository) {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Manual instantiation nightmare
|
|
112
|
+
const config = new ConfigService();
|
|
113
|
+
const database = new DatabaseService(config);
|
|
114
|
+
const userRepo = new UserRepository(database);
|
|
115
|
+
const authService = new AuthService(userRepo);
|
|
116
|
+
|
|
117
|
+
// Every file needs to repeat this setup
|
|
118
|
+
// Changes to constructors require updating all instantiation sites
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**With xInjection:**
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
@Injectable()
|
|
125
|
+
class DatabaseService {
|
|
126
|
+
constructor(private config: ConfigService) {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@Injectable()
|
|
130
|
+
class UserRepository {
|
|
131
|
+
constructor(private db: DatabaseService) {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@Injectable()
|
|
135
|
+
class AuthService {
|
|
136
|
+
constructor(private userRepo: UserRepository) {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const AuthModule = ProviderModule.create({
|
|
140
|
+
id: 'AuthModule',
|
|
141
|
+
providers: [ConfigService, DatabaseService, UserRepository, AuthService],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Automatic dependency resolution
|
|
145
|
+
const authService = AuthModule.get(AuthService);
|
|
146
|
+
// All dependencies automatically injected!
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Problem 2: Tight Coupling and Testing Difficulty
|
|
150
|
+
|
|
151
|
+
**Without xInjection:**
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
class PaymentService {
|
|
155
|
+
// Hardcoded dependency - impossible to mock
|
|
156
|
+
private stripe = new StripeClient('api-key');
|
|
157
|
+
|
|
158
|
+
async charge(amount: number) {
|
|
159
|
+
return this.stripe.charge(amount);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Testing requires hitting the real Stripe API
|
|
164
|
+
// No way to inject a mock without changing production code
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**With xInjection:**
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
@Injectable()
|
|
171
|
+
class PaymentService {
|
|
172
|
+
constructor(private paymentGateway: PaymentGateway) {}
|
|
173
|
+
|
|
174
|
+
async charge(amount: number) {
|
|
175
|
+
return this.paymentGateway.charge(amount);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Production: Use real Stripe
|
|
180
|
+
const ProductionModule = ProviderModule.create({
|
|
181
|
+
providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }, PaymentService],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Testing: Use mock (no production code changes needed!)
|
|
185
|
+
const TestModule = ProviderModule.create({
|
|
186
|
+
providers: [{ provide: PaymentGateway, useClass: MockPaymentGateway }, PaymentService],
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Problem 3: Lack of Encapsulation
|
|
191
|
+
|
|
192
|
+
**Without xInjection:**
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// Internal implementation details exposed
|
|
196
|
+
class CacheService {
|
|
197
|
+
// Should be private but other modules need access
|
|
198
|
+
public internalCache = new Map();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
class DatabaseModule {
|
|
202
|
+
// Everything is public - no control over what gets used
|
|
203
|
+
public connection = createConnection();
|
|
204
|
+
public cache = new CacheService();
|
|
205
|
+
public queryBuilder = new QueryBuilder();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Other modules can access internals they shouldn't
|
|
209
|
+
const cache = databaseModule.internalCache; // Bad!
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**With xInjection:**
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const DatabaseModule = ProviderModule.create({
|
|
216
|
+
id: 'DatabaseModule',
|
|
217
|
+
providers: [ConnectionPool, CacheService, QueryBuilder],
|
|
218
|
+
exports: [QueryBuilder], // Only expose public API
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Other modules can only access QueryBuilder
|
|
222
|
+
// ConnectionPool and CacheService remain internal
|
|
223
|
+
const ApiModule = ProviderModule.create({
|
|
224
|
+
imports: [DatabaseModule],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ✅ Works - QueryBuilder is exported
|
|
228
|
+
const queryBuilder = ApiModule.get(QueryBuilder);
|
|
229
|
+
|
|
230
|
+
// ❌ Error - CacheService not exported (properly encapsulated!)
|
|
231
|
+
const cache = ApiModule.get(CacheService);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Problem 4: Lifecycle Management Complexity
|
|
235
|
+
|
|
236
|
+
**Without xInjection:**
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
class AppServices {
|
|
240
|
+
database: DatabaseService;
|
|
241
|
+
cache: CacheService;
|
|
242
|
+
|
|
243
|
+
async initialize() {
|
|
244
|
+
this.database = new DatabaseService();
|
|
245
|
+
await this.database.connect();
|
|
246
|
+
|
|
247
|
+
this.cache = new CacheService();
|
|
248
|
+
await this.cache.initialize();
|
|
249
|
+
|
|
250
|
+
// Manually track initialization order and cleanup
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async cleanup() {
|
|
254
|
+
// Must remember to clean up in reverse order
|
|
255
|
+
await this.cache.dispose();
|
|
256
|
+
await this.database.disconnect();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Easy to forget cleanup, leading to resource leaks
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**With xInjection:**
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
const AppModule = ProviderModule.create({
|
|
267
|
+
id: 'AppModule',
|
|
268
|
+
providers: [DatabaseService, CacheService],
|
|
269
|
+
onReady: async (module) => {
|
|
270
|
+
// Initialization logic - called immediately after module creation
|
|
271
|
+
const db = module.get(DatabaseService);
|
|
272
|
+
await db.connect();
|
|
273
|
+
},
|
|
274
|
+
onDispose: () => {
|
|
275
|
+
return {
|
|
276
|
+
before: async (mod) => {
|
|
277
|
+
// Automatic cleanup in proper order
|
|
278
|
+
const db = mod.get(DatabaseService);
|
|
279
|
+
await db.disconnect();
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Lifecycle automatically managed
|
|
286
|
+
await AppModule.dispose(); // Everything cleaned up properly
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
xInjection transforms these pain points into elegant, maintainable code through:
|
|
290
|
+
|
|
291
|
+
- **Automatic dependency resolution** - No manual wiring
|
|
292
|
+
- **Inversion of Control** - Easy testing and flexibility
|
|
293
|
+
- **Encapsulation** - Fine-grained control over module boundaries
|
|
294
|
+
- **Lifecycle hooks** - Proper initialization and cleanup
|
|
295
|
+
- **Modular architecture** - Scalable application structure
|
|
296
|
+
|
|
50
297
|
## Overview
|
|
51
298
|
|
|
52
299
|
**xInjection** is a powerful [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) library built on [InversifyJS](https://github.com/inversify/InversifyJS), inspired by [NestJS](https://github.com/nestjs/nest)'s modular architecture. It provides fine-grained control over dependency encapsulation through a module-based system where each module manages its own container with explicit import/export boundaries.
|
|
@@ -59,6 +306,7 @@
|
|
|
59
306
|
- **Lazy Loading** - Blueprint pattern for deferred module instantiation
|
|
60
307
|
- **Lifecycle Hooks** - `onReady`, `onReset`, `onDispose` for module lifecycle management
|
|
61
308
|
- **Events & Middlewares** - Deep customization through event subscriptions and middleware chains
|
|
309
|
+
- **OOP Support** - `ProviderModuleClass` for class-based module architecture
|
|
62
310
|
- **Framework Agnostic** - Works in Node.js and browser environments
|
|
63
311
|
- **TypeScript First** - Full type safety with decorator support
|
|
64
312
|
|
|
@@ -117,7 +365,232 @@ const authService = AuthModule.get(AuthService);
|
|
|
117
365
|
console.log(authService.login('123')); // "Logged in as John Doe"
|
|
118
366
|
```
|
|
119
367
|
|
|
120
|
-
##
|
|
368
|
+
## Core Concepts
|
|
369
|
+
|
|
370
|
+
### Services with @Injectable
|
|
371
|
+
|
|
372
|
+
The `@Injectable()` decorator marks a class as available for dependency injection. It enables automatic constructor parameter resolution.
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
import { Injectable, InjectionScope } from '@adimm/x-injection';
|
|
376
|
+
|
|
377
|
+
// Basic injectable service (Singleton by default)
|
|
378
|
+
@Injectable()
|
|
379
|
+
class LoggerService {
|
|
380
|
+
log(message: string) {
|
|
381
|
+
console.log(`[LOG] ${message}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Injectable with scope specification
|
|
386
|
+
@Injectable(InjectionScope.Request)
|
|
387
|
+
class RequestContext {
|
|
388
|
+
requestId = Math.random();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Complex service with dependencies
|
|
392
|
+
@Injectable()
|
|
393
|
+
class ApiService {
|
|
394
|
+
constructor(
|
|
395
|
+
private logger: LoggerService,
|
|
396
|
+
private context: RequestContext
|
|
397
|
+
) {}
|
|
398
|
+
|
|
399
|
+
async fetchData() {
|
|
400
|
+
this.logger.log(`Fetching data for request ${this.context.requestId}`);
|
|
401
|
+
return { data: 'example' };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
> [!IMPORTANT]
|
|
407
|
+
> The `@Injectable()` decorator is **required** for any class that:
|
|
408
|
+
>
|
|
409
|
+
> - Has constructor dependencies
|
|
410
|
+
> - Needs to be resolved from a module container
|
|
411
|
+
> - Should be managed by the dependency injection system
|
|
412
|
+
|
|
413
|
+
### Modules
|
|
414
|
+
|
|
415
|
+
Modules are the fundamental building blocks of xInjection. Each module encapsulates providers with explicit control over imports and exports.
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
import { ProviderModule } from '@adimm/x-injection';
|
|
419
|
+
|
|
420
|
+
// Define services
|
|
421
|
+
@Injectable()
|
|
422
|
+
class DatabaseService {
|
|
423
|
+
query(sql: string) {
|
|
424
|
+
return [{ id: 1, name: 'Result' }];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
@Injectable()
|
|
429
|
+
class InternalCacheService {
|
|
430
|
+
// Internal-only service
|
|
431
|
+
private cache = new Map();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
@Injectable()
|
|
435
|
+
class UserRepository {
|
|
436
|
+
constructor(
|
|
437
|
+
private db: DatabaseService,
|
|
438
|
+
private cache: InternalCacheService
|
|
439
|
+
) {}
|
|
440
|
+
|
|
441
|
+
findById(id: string) {
|
|
442
|
+
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Create module with encapsulation
|
|
447
|
+
const DatabaseModule = ProviderModule.create({
|
|
448
|
+
id: 'DatabaseModule',
|
|
449
|
+
providers: [DatabaseService, InternalCacheService, UserRepository],
|
|
450
|
+
exports: [UserRepository], // Only UserRepository accessible to importers
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Use the module
|
|
454
|
+
const ApiModule = ProviderModule.create({
|
|
455
|
+
id: 'ApiModule',
|
|
456
|
+
imports: [DatabaseModule],
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// ✅ Works - UserRepository is exported
|
|
460
|
+
const userRepo = ApiModule.get(UserRepository);
|
|
461
|
+
|
|
462
|
+
// ❌ Throws error - InternalCacheService not exported
|
|
463
|
+
// const cache = ApiModule.get(InternalCacheService);
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Key Module Methods:**
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
const MyModule = ProviderModule.create({ id: 'MyModule' });
|
|
470
|
+
|
|
471
|
+
// Resolution
|
|
472
|
+
MyModule.get(ServiceClass); // Get provider instance
|
|
473
|
+
MyModule.getMany(Service1, Service2); // Get multiple providers
|
|
474
|
+
|
|
475
|
+
// Queries
|
|
476
|
+
MyModule.hasProvider(ServiceClass); // Check if provider exists
|
|
477
|
+
MyModule.isImportingModule('ModuleId'); // Check if importing module
|
|
478
|
+
MyModule.isExportingProvider(ServiceClass); // Check if exporting provider
|
|
479
|
+
MyModule.isExportingModule('ModuleId'); // Check if exporting module
|
|
480
|
+
|
|
481
|
+
// Dynamic updates
|
|
482
|
+
MyModule.update.addProvider(NewService); // Add provider
|
|
483
|
+
MyModule.update.addImport(OtherModule); // Add import
|
|
484
|
+
MyModule.update.removeProvider(ServiceClass); // Remove provider
|
|
485
|
+
MyModule.update.removeImport(OtherModule); // Remove import
|
|
486
|
+
MyModule.update.removeFromExports(Service); // Remove from exports
|
|
487
|
+
|
|
488
|
+
// Lifecycle
|
|
489
|
+
await MyModule.reset(); // Reset module state
|
|
490
|
+
await MyModule.dispose(); // Clean up and dispose
|
|
491
|
+
MyModule.isDisposed; // Check disposal state
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Blueprints
|
|
495
|
+
|
|
496
|
+
Blueprints allow you to define module configurations without instantiating them, enabling lazy loading and template reuse.
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
import { ProviderModule } from '@adimm/x-injection';
|
|
500
|
+
|
|
501
|
+
@Injectable()
|
|
502
|
+
class ConfigService {
|
|
503
|
+
getConfig() {
|
|
504
|
+
return { apiUrl: 'https://api.example.com' };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Define blueprint (not instantiated yet)
|
|
509
|
+
const ConfigModuleBp = ProviderModule.blueprint({
|
|
510
|
+
id: 'ConfigModule',
|
|
511
|
+
providers: [ConfigService],
|
|
512
|
+
exports: [ConfigService],
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Use blueprint in imports (auto-converts to module)
|
|
516
|
+
const ApiModule = ProviderModule.create({
|
|
517
|
+
id: 'ApiModule',
|
|
518
|
+
imports: [ConfigModuleBp], // Instantiated automatically when needed
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Or create module from blueprint explicitly
|
|
522
|
+
const ConfigModule = ProviderModule.create(ConfigModuleBp);
|
|
523
|
+
|
|
524
|
+
// Clone blueprints for testing
|
|
525
|
+
const ConfigModuleMock = ConfigModuleBp.clone().updateDefinition({
|
|
526
|
+
id: 'ConfigModuleMock',
|
|
527
|
+
providers: [{ provide: ConfigService, useValue: { getConfig: () => ({ apiUrl: 'mock' }) } }],
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Benefits of Blueprints:**
|
|
532
|
+
|
|
533
|
+
- **Deferred Instantiation** - Only create modules when needed
|
|
534
|
+
- **Reusable Templates** - Define once, use in multiple places
|
|
535
|
+
- **Testing** - Clone and modify for test scenarios
|
|
536
|
+
- **Scoped Singletons** - Each importer gets its own module instance
|
|
537
|
+
|
|
538
|
+
> [!TIP]
|
|
539
|
+
> Use blueprints when you need the same module configuration in multiple places, or when you want to delay module creation until runtime.
|
|
540
|
+
|
|
541
|
+
### AppModule
|
|
542
|
+
|
|
543
|
+
The `AppModule` is a special global root module that's automatically available throughout your application. Global modules are auto-imported into `AppModule`.
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
import { AppModule, ProviderModule } from '@adimm/x-injection';
|
|
547
|
+
|
|
548
|
+
@Injectable()
|
|
549
|
+
class LoggerService {
|
|
550
|
+
log(message: string) {
|
|
551
|
+
console.log(message);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Add global providers to AppModule
|
|
556
|
+
AppModule.update.addProvider(LoggerService);
|
|
557
|
+
|
|
558
|
+
// Access from any module without explicit import
|
|
559
|
+
const FeatureModule = ProviderModule.create({
|
|
560
|
+
id: 'FeatureModule',
|
|
561
|
+
// No need to import AppModule
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const logger = FeatureModule.get(LoggerService);
|
|
565
|
+
logger.log('Hello from FeatureModule!');
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**Global Module Pattern:**
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
// Create a global module (auto-imports into AppModule)
|
|
572
|
+
const LoggerModule = ProviderModule.create({
|
|
573
|
+
id: 'LoggerModule',
|
|
574
|
+
isGlobal: true,
|
|
575
|
+
providers: [LoggerService],
|
|
576
|
+
exports: [LoggerService],
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Now all modules have access to LoggerService
|
|
580
|
+
const AnyModule = ProviderModule.create({
|
|
581
|
+
id: 'AnyModule',
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const logger = AnyModule.get(LoggerService); // Works!
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
> [!WARNING]
|
|
588
|
+
>
|
|
589
|
+
> - Cannot create a module with `id: 'AppModule'` - this is reserved
|
|
590
|
+
> - Cannot import `AppModule` into other modules
|
|
591
|
+
> - Use global modules sparingly to avoid implicit dependencies
|
|
592
|
+
|
|
593
|
+
## OOP-Style Modules with ProviderModuleClass
|
|
121
594
|
|
|
122
595
|
For developers who prefer class-based architecture, xInjection provides `ProviderModuleClass` - a composition-based wrapper that prevents naming conflicts between your custom methods and the DI container methods.
|
|
123
596
|
|
|
@@ -153,6 +626,7 @@ class AuthModule extends ProviderModuleClass {
|
|
|
153
626
|
});
|
|
154
627
|
}
|
|
155
628
|
|
|
629
|
+
// Custom business logic methods
|
|
156
630
|
authenticateUser(userId: string): string {
|
|
157
631
|
const authService = this.module.get(AuthService);
|
|
158
632
|
return authService.login(userId);
|
|
@@ -162,29 +636,54 @@ class AuthModule extends ProviderModuleClass {
|
|
|
162
636
|
const userService = this.module.get(UserService);
|
|
163
637
|
return userService.get(userId);
|
|
164
638
|
}
|
|
639
|
+
|
|
640
|
+
// Custom method named 'get' - no conflict!
|
|
641
|
+
get(): string {
|
|
642
|
+
return 'custom-get-value';
|
|
643
|
+
}
|
|
165
644
|
}
|
|
166
645
|
|
|
167
646
|
// Instantiate and use
|
|
168
647
|
const authModule = new AuthModule();
|
|
648
|
+
|
|
649
|
+
// Use custom methods
|
|
169
650
|
console.log(authModule.authenticateUser('123')); // "Logged in as John Doe"
|
|
651
|
+
console.log(authModule.get()); // "custom-get-value"
|
|
170
652
|
|
|
171
|
-
//
|
|
653
|
+
// Access DI container through .module property
|
|
172
654
|
const authService = authModule.module.get(AuthService);
|
|
173
655
|
authModule.module.update.addProvider(NewService);
|
|
174
656
|
```
|
|
175
657
|
|
|
658
|
+
> [!IMPORTANT]
|
|
659
|
+
> All `ProviderModule` methods are available through the `.module` property to prevent naming conflicts with your custom methods.
|
|
660
|
+
|
|
176
661
|
### Advanced OOP Patterns
|
|
177
662
|
|
|
178
663
|
**Module with Initialization Logic:**
|
|
179
664
|
|
|
180
665
|
```ts
|
|
666
|
+
@Injectable()
|
|
667
|
+
class DatabaseService {
|
|
668
|
+
private connected = false;
|
|
669
|
+
|
|
670
|
+
async connect(): Promise<void> {
|
|
671
|
+
// Connection logic
|
|
672
|
+
this.connected = true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
isConnected(): boolean {
|
|
676
|
+
return this.connected;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
181
680
|
class DatabaseModule extends ProviderModuleClass {
|
|
182
|
-
private
|
|
681
|
+
private isModuleConnected = false;
|
|
183
682
|
|
|
184
683
|
constructor() {
|
|
185
684
|
super({
|
|
186
685
|
id: 'DatabaseModule',
|
|
187
|
-
providers: [DatabaseService
|
|
686
|
+
providers: [DatabaseService],
|
|
188
687
|
exports: [DatabaseService],
|
|
189
688
|
onReady: async (module) => {
|
|
190
689
|
console.log('DatabaseModule ready!');
|
|
@@ -195,11 +694,11 @@ class DatabaseModule extends ProviderModuleClass {
|
|
|
195
694
|
async connect(): Promise<void> {
|
|
196
695
|
const dbService = this.module.get(DatabaseService);
|
|
197
696
|
await dbService.connect();
|
|
198
|
-
this.
|
|
697
|
+
this.isModuleConnected = true;
|
|
199
698
|
}
|
|
200
699
|
|
|
201
700
|
getConnectionStatus(): boolean {
|
|
202
|
-
return this.
|
|
701
|
+
return this.isModuleConnected;
|
|
203
702
|
}
|
|
204
703
|
}
|
|
205
704
|
|
|
@@ -211,17 +710,30 @@ console.log(dbModule.getConnectionStatus()); // true
|
|
|
211
710
|
**Module with Computed Properties:**
|
|
212
711
|
|
|
213
712
|
```ts
|
|
713
|
+
@Injectable()
|
|
714
|
+
class ApiService {
|
|
715
|
+
makeRequest() {
|
|
716
|
+
return 'response';
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
@Injectable()
|
|
721
|
+
class HttpClient {
|
|
722
|
+
get(url: string) {
|
|
723
|
+
return `GET ${url}`;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
214
727
|
class ApiModule extends ProviderModuleClass {
|
|
215
728
|
constructor() {
|
|
216
729
|
super({
|
|
217
730
|
id: 'ApiModule',
|
|
218
|
-
imports: [ConfigModule, LoggerModule],
|
|
219
731
|
providers: [ApiService, HttpClient],
|
|
220
732
|
exports: [ApiService],
|
|
221
733
|
});
|
|
222
734
|
}
|
|
223
735
|
|
|
224
|
-
// Computed
|
|
736
|
+
// Computed properties - lazy evaluation
|
|
225
737
|
get apiService(): ApiService {
|
|
226
738
|
return this.module.get(ApiService);
|
|
227
739
|
}
|
|
@@ -233,245 +745,431 @@ class ApiModule extends ProviderModuleClass {
|
|
|
233
745
|
// Business logic using multiple services
|
|
234
746
|
async makeAuthenticatedRequest(url: string, token: string) {
|
|
235
747
|
const client = this.httpClient;
|
|
236
|
-
return client.
|
|
237
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
238
|
-
});
|
|
748
|
+
return client.get(url) + ` with token ${token}`;
|
|
239
749
|
}
|
|
240
750
|
}
|
|
241
751
|
|
|
242
752
|
const apiModule = new ApiModule();
|
|
243
|
-
const response = await apiModule.makeAuthenticatedRequest('/users', '
|
|
753
|
+
const response = await apiModule.makeAuthenticatedRequest('/users', 'token123');
|
|
244
754
|
```
|
|
245
755
|
|
|
246
|
-
|
|
756
|
+
### When to Use OOP vs Functional
|
|
247
757
|
|
|
248
|
-
|
|
249
|
-
class BaseModule extends ProviderModuleClass {
|
|
250
|
-
protected logAction(action: string): void {
|
|
251
|
-
const logger = this.module.get(LoggerService);
|
|
252
|
-
logger.log(`[${String(this.module.id)}] ${action}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
758
|
+
**Use OOP-style (`extends ProviderModuleClass`) when:**
|
|
255
759
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
});
|
|
263
|
-
}
|
|
760
|
+
- You need custom business logic methods on the module itself
|
|
761
|
+
- You prefer class-based architecture and inheritance patterns
|
|
762
|
+
- You want computed properties or getters for providers
|
|
763
|
+
- You need initialization logic or state management in the module
|
|
764
|
+
- You're building a complex module with multiple related operations
|
|
765
|
+
- You want to prevent naming conflicts (e.g., custom `get()` method)
|
|
264
766
|
|
|
265
|
-
|
|
266
|
-
this.logAction(`Creating user: ${name}`);
|
|
267
|
-
const userService = this.module.get(UserService);
|
|
268
|
-
return userService.create(name);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
deleteUser(id: string) {
|
|
272
|
-
this.logAction(`Deleting user: ${id}`);
|
|
273
|
-
const userService = this.module.get(UserService);
|
|
274
|
-
return userService.delete(id);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### When to Use OOP vs Functional
|
|
280
|
-
|
|
281
|
-
**Use OOP-style (`extends ProviderModuleClass`) when:**
|
|
282
|
-
|
|
283
|
-
- You need custom business logic methods on the module itself
|
|
284
|
-
- You prefer class-based architecture
|
|
285
|
-
- You want computed properties or getters for providers
|
|
286
|
-
- You need initialization logic or state management in the module
|
|
287
|
-
- You're building a complex module with multiple related operations
|
|
288
|
-
|
|
289
|
-
**Use Functional-style (`ProviderModule.create()`) when:**
|
|
767
|
+
**Use Functional-style (`ProviderModule.create()`) when:**
|
|
290
768
|
|
|
291
769
|
- You only need dependency injection without custom logic
|
|
292
|
-
- You prefer functional composition
|
|
293
|
-
- You want
|
|
770
|
+
- You prefer functional composition and simplicity
|
|
771
|
+
- You want more concise code
|
|
294
772
|
- You're creating straightforward provider containers
|
|
773
|
+
- You don't need module-level state or behavior
|
|
295
774
|
|
|
296
|
-
**Key Point:** Both styles are fully compatible and can be mixed within the same application. `ProviderModuleClass` uses composition (contains a `ProviderModule` as `this.module`),
|
|
775
|
+
**Key Point:** Both styles are fully compatible and can be mixed within the same application. `ProviderModuleClass` uses composition (contains a `ProviderModule` as `this.module`), providing identical DI functionality while preventing method name conflicts.
|
|
297
776
|
|
|
298
|
-
##
|
|
777
|
+
## Provider Tokens
|
|
778
|
+
|
|
779
|
+
xInjection supports four types of provider tokens, each serving different use cases.
|
|
299
780
|
|
|
300
|
-
###
|
|
781
|
+
### 1. Class Token
|
|
301
782
|
|
|
302
|
-
The
|
|
783
|
+
The simplest form - just provide the class directly.
|
|
303
784
|
|
|
304
785
|
```ts
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
786
|
+
@Injectable()
|
|
787
|
+
class UserService {
|
|
788
|
+
getUsers() {
|
|
789
|
+
return [{ id: '1', name: 'Alice' }];
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const MyModule = ProviderModule.create({
|
|
794
|
+
id: 'MyModule',
|
|
795
|
+
providers: [UserService], // Class token
|
|
310
796
|
});
|
|
797
|
+
|
|
798
|
+
const userService = MyModule.get(UserService);
|
|
311
799
|
```
|
|
312
800
|
|
|
313
|
-
|
|
801
|
+
### 2. Class Token with Substitution
|
|
314
802
|
|
|
315
|
-
|
|
316
|
-
- `Module.update.addProvider()` - Dynamically add providers
|
|
317
|
-
- `Module.update.addImport()` - Import other modules at runtime
|
|
318
|
-
- `Module.dispose()` - Clean up module resources
|
|
803
|
+
Use one class as the token but instantiate a different class. Perfect for polymorphism and testing.
|
|
319
804
|
|
|
320
|
-
|
|
805
|
+
```ts
|
|
806
|
+
@Injectable()
|
|
807
|
+
abstract class PaymentGateway {
|
|
808
|
+
abstract charge(amount: number): Promise<void>;
|
|
809
|
+
}
|
|
321
810
|
|
|
322
|
-
|
|
811
|
+
@Injectable()
|
|
812
|
+
class StripePaymentGateway extends PaymentGateway {
|
|
813
|
+
async charge(amount: number) {
|
|
814
|
+
console.log(`Charging $${amount} via Stripe`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
323
817
|
|
|
324
|
-
|
|
818
|
+
@Injectable()
|
|
819
|
+
class MockPaymentGateway extends PaymentGateway {
|
|
820
|
+
async charge(amount: number) {
|
|
821
|
+
console.log(`Mock charge: $${amount}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
325
824
|
|
|
326
|
-
|
|
327
|
-
|
|
825
|
+
// Production
|
|
826
|
+
const ProductionModule = ProviderModule.create({
|
|
827
|
+
id: 'ProductionModule',
|
|
828
|
+
providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }],
|
|
829
|
+
});
|
|
328
830
|
|
|
329
|
-
//
|
|
330
|
-
|
|
831
|
+
// Testing
|
|
832
|
+
const TestModule = ProviderModule.create({
|
|
833
|
+
id: 'TestModule',
|
|
834
|
+
providers: [{ provide: PaymentGateway, useClass: MockPaymentGateway }],
|
|
835
|
+
});
|
|
331
836
|
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
const logger = anyModule.get(LoggerService);
|
|
837
|
+
const prodGateway = ProductionModule.get(PaymentGateway); // StripePaymentGateway
|
|
838
|
+
const testGateway = TestModule.get(PaymentGateway); // MockPaymentGateway
|
|
335
839
|
```
|
|
336
840
|
|
|
337
|
-
###
|
|
841
|
+
### 3. Value Token
|
|
338
842
|
|
|
339
|
-
|
|
843
|
+
Provide constant values or pre-instantiated objects.
|
|
340
844
|
|
|
341
845
|
```ts
|
|
342
|
-
//
|
|
343
|
-
const
|
|
344
|
-
id: '
|
|
345
|
-
providers: [
|
|
346
|
-
|
|
846
|
+
// Configuration values
|
|
847
|
+
const ConfigModule = ProviderModule.create({
|
|
848
|
+
id: 'ConfigModule',
|
|
849
|
+
providers: [
|
|
850
|
+
{ provide: 'API_KEY', useValue: 'secret-key-123' },
|
|
851
|
+
{ provide: 'API_URL', useValue: 'https://api.example.com' },
|
|
852
|
+
{ provide: 'MAX_RETRIES', useValue: 3 },
|
|
853
|
+
],
|
|
854
|
+
exports: ['API_KEY', 'API_URL', 'MAX_RETRIES'],
|
|
347
855
|
});
|
|
348
856
|
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
imports: [DatabaseModuleBp],
|
|
353
|
-
});
|
|
857
|
+
const apiKey = ConfigModule.get('API_KEY'); // 'secret-key-123'
|
|
858
|
+
const apiUrl = ConfigModule.get('API_URL'); // 'https://api.example.com'
|
|
859
|
+
const maxRetries = ConfigModule.get('MAX_RETRIES'); // 3
|
|
354
860
|
|
|
355
|
-
//
|
|
356
|
-
const
|
|
861
|
+
// Pre-instantiated objects
|
|
862
|
+
const existingLogger = new Logger();
|
|
863
|
+
const LoggerModule = ProviderModule.create({
|
|
864
|
+
id: 'LoggerModule',
|
|
865
|
+
providers: [{ provide: Logger, useValue: existingLogger }],
|
|
866
|
+
});
|
|
357
867
|
```
|
|
358
868
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
- Deferred instantiation for better startup performance
|
|
362
|
-
- Reusable module templates across your application
|
|
363
|
-
- Scoped singletons per importing module
|
|
869
|
+
### 4. Factory Token
|
|
364
870
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
xInjection supports four types of provider tokens:
|
|
368
|
-
|
|
369
|
-
**1. Class Token** (simplest):
|
|
871
|
+
Use a factory function to create providers dynamically. The `inject` parameter specifies dependencies.
|
|
370
872
|
|
|
371
873
|
```ts
|
|
372
874
|
@Injectable()
|
|
373
|
-
class
|
|
875
|
+
class ConfigService {
|
|
876
|
+
dbUrl = 'postgres://localhost:5432/mydb';
|
|
877
|
+
dbPort = 5432;
|
|
878
|
+
}
|
|
374
879
|
|
|
375
|
-
|
|
376
|
-
|
|
880
|
+
interface DatabaseConnection {
|
|
881
|
+
url: string;
|
|
882
|
+
port: number;
|
|
883
|
+
connected: boolean;
|
|
884
|
+
}
|
|
377
885
|
|
|
378
|
-
|
|
886
|
+
const DatabaseModule = ProviderModule.create({
|
|
887
|
+
id: 'DatabaseModule',
|
|
888
|
+
providers: [
|
|
889
|
+
ConfigService,
|
|
890
|
+
{
|
|
891
|
+
provide: 'DATABASE_CONNECTION',
|
|
892
|
+
useFactory: (config: ConfigService) => {
|
|
893
|
+
// Factory receives injected dependencies
|
|
894
|
+
return {
|
|
895
|
+
url: config.dbUrl,
|
|
896
|
+
port: config.dbPort,
|
|
897
|
+
connected: true,
|
|
898
|
+
};
|
|
899
|
+
},
|
|
900
|
+
inject: [ConfigService], // Dependencies to inject into factory
|
|
901
|
+
},
|
|
902
|
+
],
|
|
903
|
+
exports: ['DATABASE_CONNECTION'],
|
|
904
|
+
});
|
|
379
905
|
|
|
380
|
-
|
|
381
|
-
|
|
906
|
+
const connection = DatabaseModule.get<DatabaseConnection>('DATABASE_CONNECTION');
|
|
907
|
+
console.log(connection.url); // 'postgres://localhost:5432/mydb'
|
|
382
908
|
```
|
|
383
909
|
|
|
384
|
-
**
|
|
910
|
+
**Complex Factory Example with Multiple Dependencies:**
|
|
385
911
|
|
|
386
912
|
```ts
|
|
387
|
-
|
|
388
|
-
|
|
913
|
+
@Injectable()
|
|
914
|
+
class LoggerService {
|
|
915
|
+
log(message: string) {
|
|
916
|
+
console.log(message);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
389
919
|
|
|
390
|
-
|
|
920
|
+
@Injectable()
|
|
921
|
+
class MetricsService {
|
|
922
|
+
track(event: string) {
|
|
923
|
+
console.log(`Tracking: ${event}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
391
926
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
927
|
+
interface ApiClient {
|
|
928
|
+
logger: LoggerService;
|
|
929
|
+
metrics: MetricsService;
|
|
930
|
+
baseUrl: string;
|
|
931
|
+
makeRequest(endpoint: string): void;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const ApiModule = ProviderModule.create({
|
|
935
|
+
id: 'ApiModule',
|
|
936
|
+
providers: [
|
|
937
|
+
LoggerService,
|
|
938
|
+
MetricsService,
|
|
939
|
+
{ provide: 'BASE_URL', useValue: 'https://api.example.com' },
|
|
940
|
+
{
|
|
941
|
+
provide: 'API_CLIENT',
|
|
942
|
+
useFactory: (logger: LoggerService, metrics: MetricsService, baseUrl: string): ApiClient => {
|
|
943
|
+
return {
|
|
944
|
+
logger,
|
|
945
|
+
metrics,
|
|
946
|
+
baseUrl,
|
|
947
|
+
makeRequest(endpoint: string) {
|
|
948
|
+
this.logger.log(`Making request to ${this.baseUrl}${endpoint}`);
|
|
949
|
+
this.metrics.track('api_request');
|
|
950
|
+
},
|
|
951
|
+
};
|
|
952
|
+
},
|
|
953
|
+
inject: [LoggerService, MetricsService, 'BASE_URL'],
|
|
954
|
+
},
|
|
955
|
+
],
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
const apiClient = ApiModule.get<ApiClient>('API_CLIENT');
|
|
959
|
+
apiClient.makeRequest('/users');
|
|
400
960
|
```
|
|
401
961
|
|
|
962
|
+
> [!TIP]
|
|
963
|
+
> Use factory tokens when:
|
|
964
|
+
>
|
|
965
|
+
> - Provider creation requires complex logic
|
|
966
|
+
> - You need to inject dependencies into the factory
|
|
967
|
+
> - You're creating providers that depend on runtime configuration
|
|
968
|
+
> - You need to create multiple instances with different configurations
|
|
969
|
+
|
|
402
970
|
## Injection Scopes
|
|
403
971
|
|
|
404
|
-
Control provider lifecycle with three scope types
|
|
972
|
+
Control provider lifecycle with three scope types. Scope priority order: **token scope > decorator scope > module default scope**.
|
|
405
973
|
|
|
406
974
|
### Singleton (Default)
|
|
407
975
|
|
|
408
|
-
Cached after first resolution - same instance every time
|
|
976
|
+
Cached after first resolution - same instance returned every time.
|
|
409
977
|
|
|
410
978
|
```ts
|
|
411
979
|
@Injectable() // Singleton by default
|
|
412
|
-
class DatabaseService {
|
|
980
|
+
class DatabaseService {
|
|
981
|
+
connectionId = Math.random();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const MyModule = ProviderModule.create({
|
|
985
|
+
id: 'MyModule',
|
|
986
|
+
providers: [DatabaseService],
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
const db1 = MyModule.get(DatabaseService);
|
|
990
|
+
const db2 = MyModule.get(DatabaseService);
|
|
413
991
|
|
|
414
|
-
|
|
992
|
+
console.log(db1 === db2); // true
|
|
993
|
+
console.log(db1.connectionId === db2.connectionId); // true
|
|
415
994
|
```
|
|
416
995
|
|
|
417
996
|
### Transient
|
|
418
997
|
|
|
419
|
-
New instance on every resolution
|
|
998
|
+
New instance created on every resolution.
|
|
420
999
|
|
|
421
1000
|
```ts
|
|
422
1001
|
@Injectable(InjectionScope.Transient)
|
|
423
|
-
class RequestLogger {
|
|
1002
|
+
class RequestLogger {
|
|
1003
|
+
requestId = Math.random();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const MyModule = ProviderModule.create({
|
|
1007
|
+
id: 'MyModule',
|
|
1008
|
+
providers: [RequestLogger],
|
|
1009
|
+
});
|
|
424
1010
|
|
|
425
|
-
|
|
1011
|
+
const logger1 = MyModule.get(RequestLogger);
|
|
1012
|
+
const logger2 = MyModule.get(RequestLogger);
|
|
1013
|
+
|
|
1014
|
+
console.log(logger1 === logger2); // false
|
|
1015
|
+
console.log(logger1.requestId === logger2.requestId); // false
|
|
426
1016
|
```
|
|
427
1017
|
|
|
428
1018
|
### Request
|
|
429
1019
|
|
|
430
|
-
Single instance per resolution tree (
|
|
1020
|
+
Single instance per resolution tree. All dependencies resolved in the same `get()` call share the same instance.
|
|
431
1021
|
|
|
432
1022
|
```ts
|
|
433
1023
|
@Injectable(InjectionScope.Request)
|
|
434
|
-
class RequestContext {
|
|
1024
|
+
class RequestContext {
|
|
1025
|
+
requestId = Math.random();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
@Injectable(InjectionScope.Transient)
|
|
1029
|
+
class ServiceA {
|
|
1030
|
+
constructor(public ctx: RequestContext) {}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
@Injectable(InjectionScope.Transient)
|
|
1034
|
+
class ServiceB {
|
|
1035
|
+
constructor(public ctx: RequestContext) {}
|
|
1036
|
+
}
|
|
435
1037
|
|
|
436
1038
|
@Injectable(InjectionScope.Transient)
|
|
437
1039
|
class Controller {
|
|
438
1040
|
constructor(
|
|
439
|
-
public
|
|
440
|
-
public
|
|
1041
|
+
public serviceA: ServiceA,
|
|
1042
|
+
public serviceB: ServiceB
|
|
441
1043
|
) {}
|
|
442
1044
|
}
|
|
443
1045
|
|
|
444
|
-
const
|
|
445
|
-
|
|
1046
|
+
const MyModule = ProviderModule.create({
|
|
1047
|
+
id: 'MyModule',
|
|
1048
|
+
providers: [RequestContext, ServiceA, ServiceB, Controller],
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
// First resolution tree
|
|
1052
|
+
const controller1 = MyModule.get(Controller);
|
|
1053
|
+
console.log(controller1.serviceA.ctx === controller1.serviceB.ctx); // true
|
|
1054
|
+
// ServiceA and ServiceB share the same RequestContext
|
|
1055
|
+
|
|
1056
|
+
// Second resolution tree
|
|
1057
|
+
const controller2 = MyModule.get(Controller);
|
|
1058
|
+
console.log(controller2.serviceA.ctx === controller2.serviceB.ctx); // true
|
|
1059
|
+
// New resolution, both services get a new shared RequestContext
|
|
1060
|
+
|
|
1061
|
+
// Different resolution trees get different contexts
|
|
1062
|
+
console.log(controller1.serviceA.ctx === controller2.serviceA.ctx); // false
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
**Visual Representation:**
|
|
1066
|
+
|
|
1067
|
+
```
|
|
1068
|
+
First module.get(Controller):
|
|
1069
|
+
Controller (new) ──┬──> ServiceA (new) ──┐
|
|
1070
|
+
│ ├──> RequestContext (SAME instance)
|
|
1071
|
+
└──> ServiceB (new) ──┘
|
|
1072
|
+
|
|
1073
|
+
Second module.get(Controller):
|
|
1074
|
+
Controller (new) ──┬──> ServiceA (new) ──┐
|
|
1075
|
+
│ ├──> RequestContext (NEW instance)
|
|
1076
|
+
└──> ServiceB (new) ──┘
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
### Scope Priority Order
|
|
1080
|
+
|
|
1081
|
+
Scopes are resolved in the following priority order (highest to lowest):
|
|
1082
|
+
|
|
1083
|
+
1. **Token scope** (highest priority)
|
|
1084
|
+
2. **Decorator scope**
|
|
1085
|
+
3. **Module default scope** (lowest priority)
|
|
1086
|
+
|
|
1087
|
+
```ts
|
|
1088
|
+
@Injectable(InjectionScope.Singleton) // Priority 2
|
|
1089
|
+
class MyService {}
|
|
1090
|
+
|
|
1091
|
+
const MyModule = ProviderModule.create({
|
|
1092
|
+
id: 'MyModule',
|
|
1093
|
+
defaultScope: InjectionScope.Singleton, // Priority 3 (lowest)
|
|
1094
|
+
providers: [
|
|
1095
|
+
{
|
|
1096
|
+
provide: MyService,
|
|
1097
|
+
useClass: MyService,
|
|
1098
|
+
scope: InjectionScope.Transient, // Priority 1 (highest) - WINS!
|
|
1099
|
+
},
|
|
1100
|
+
],
|
|
1101
|
+
});
|
|
446
1102
|
|
|
447
|
-
|
|
448
|
-
|
|
1103
|
+
// Token scope wins: new instance every time
|
|
1104
|
+
const s1 = MyModule.get(MyService);
|
|
1105
|
+
const s2 = MyModule.get(MyService);
|
|
1106
|
+
console.log(s1 === s2); // false
|
|
449
1107
|
```
|
|
450
1108
|
|
|
451
|
-
**
|
|
1109
|
+
**Examples of Each Priority:**
|
|
452
1110
|
|
|
453
1111
|
```ts
|
|
454
|
-
// 1
|
|
455
|
-
|
|
1112
|
+
// Priority 1: Token scope
|
|
1113
|
+
const Module1 = ProviderModule.create({
|
|
1114
|
+
id: 'Module1',
|
|
1115
|
+
defaultScope: InjectionScope.Singleton,
|
|
1116
|
+
providers: [
|
|
1117
|
+
{
|
|
1118
|
+
provide: MyService,
|
|
1119
|
+
useClass: MyService,
|
|
1120
|
+
scope: InjectionScope.Transient, // Token wins
|
|
1121
|
+
},
|
|
1122
|
+
],
|
|
1123
|
+
});
|
|
456
1124
|
|
|
457
|
-
// 2
|
|
1125
|
+
// Priority 2: Decorator scope (no token scope)
|
|
458
1126
|
@Injectable(InjectionScope.Request)
|
|
459
|
-
class
|
|
1127
|
+
class DecoratedService {}
|
|
460
1128
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
1129
|
+
const Module2 = ProviderModule.create({
|
|
1130
|
+
id: 'Module2',
|
|
1131
|
+
defaultScope: InjectionScope.Singleton,
|
|
1132
|
+
providers: [DecoratedService], // Decorator wins
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// Priority 3: Module default (no token or decorator scope)
|
|
1136
|
+
@Injectable() // No scope specified
|
|
1137
|
+
class PlainService {}
|
|
1138
|
+
|
|
1139
|
+
const Module3 = ProviderModule.create({
|
|
1140
|
+
id: 'Module3',
|
|
1141
|
+
defaultScope: InjectionScope.Transient, // Module default wins
|
|
1142
|
+
providers: [PlainService],
|
|
465
1143
|
});
|
|
466
1144
|
```
|
|
467
1145
|
|
|
1146
|
+
> [!IMPORTANT]
|
|
1147
|
+
> Request scope is useful for scenarios like:
|
|
1148
|
+
>
|
|
1149
|
+
> - HTTP request tracking (same request ID across all services in one request)
|
|
1150
|
+
> - Transaction contexts (same database transaction across all repositories)
|
|
1151
|
+
> - User context (same user data across all services in one operation)
|
|
1152
|
+
|
|
468
1153
|
## Module System
|
|
469
1154
|
|
|
470
1155
|
### Import/Export Pattern
|
|
471
1156
|
|
|
472
|
-
Modules explicitly control dependency boundaries through imports and exports
|
|
1157
|
+
Modules explicitly control dependency boundaries through imports and exports, providing encapsulation and clear interfaces.
|
|
473
1158
|
|
|
474
1159
|
```ts
|
|
1160
|
+
@Injectable()
|
|
1161
|
+
class DatabaseService {
|
|
1162
|
+
query(sql: string) {
|
|
1163
|
+
return [{ result: 'data' }];
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
@Injectable()
|
|
1168
|
+
class InternalCacheService {
|
|
1169
|
+
// Private to DatabaseModule
|
|
1170
|
+
cache = new Map();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
475
1173
|
const DatabaseModule = ProviderModule.create({
|
|
476
1174
|
id: 'DatabaseModule',
|
|
477
1175
|
providers: [DatabaseService, InternalCacheService],
|
|
@@ -480,186 +1178,1633 @@ const DatabaseModule = ProviderModule.create({
|
|
|
480
1178
|
|
|
481
1179
|
const ApiModule = ProviderModule.create({
|
|
482
1180
|
id: 'ApiModule',
|
|
483
|
-
imports: [DatabaseModule],
|
|
1181
|
+
imports: [DatabaseModule],
|
|
484
1182
|
providers: [ApiService],
|
|
485
1183
|
});
|
|
486
1184
|
|
|
487
|
-
// ✅ Works
|
|
1185
|
+
// ✅ Works - DatabaseService is exported
|
|
488
1186
|
const dbService = ApiModule.get(DatabaseService);
|
|
489
1187
|
|
|
490
1188
|
// ❌ Error - InternalCacheService not exported
|
|
491
|
-
const cache = ApiModule.get(InternalCacheService);
|
|
1189
|
+
// const cache = ApiModule.get(InternalCacheService);
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
**Nested Imports:**
|
|
1193
|
+
|
|
1194
|
+
```ts
|
|
1195
|
+
const LayerA = ProviderModule.create({
|
|
1196
|
+
id: 'LayerA',
|
|
1197
|
+
providers: [ServiceA],
|
|
1198
|
+
exports: [ServiceA],
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
const LayerB = ProviderModule.create({
|
|
1202
|
+
id: 'LayerB',
|
|
1203
|
+
imports: [LayerA],
|
|
1204
|
+
providers: [ServiceB],
|
|
1205
|
+
exports: [ServiceB, LayerA], // Re-export LayerA
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
const LayerC = ProviderModule.create({
|
|
1209
|
+
id: 'LayerC',
|
|
1210
|
+
imports: [LayerB],
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
// ✅ Works - ServiceA accessible through LayerB's re-export
|
|
1214
|
+
const serviceA = LayerC.get(ServiceA);
|
|
1215
|
+
|
|
1216
|
+
// ✅ Works - ServiceB exported by LayerB
|
|
1217
|
+
const serviceB = LayerC.get(ServiceB);
|
|
492
1218
|
```
|
|
493
1219
|
|
|
494
1220
|
### Re-exporting Modules
|
|
495
1221
|
|
|
496
|
-
Modules can re-export imported modules to create aggregation modules
|
|
1222
|
+
Modules can re-export imported modules to create aggregation modules.
|
|
497
1223
|
|
|
498
1224
|
```ts
|
|
1225
|
+
const DatabaseModule = ProviderModule.create({
|
|
1226
|
+
id: 'DatabaseModule',
|
|
1227
|
+
providers: [DatabaseService],
|
|
1228
|
+
exports: [DatabaseService],
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
const ConfigModule = ProviderModule.create({
|
|
1232
|
+
id: 'ConfigModule',
|
|
1233
|
+
providers: [ConfigService],
|
|
1234
|
+
exports: [ConfigService],
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
const LoggerModule = ProviderModule.create({
|
|
1238
|
+
id: 'LoggerModule',
|
|
1239
|
+
providers: [LoggerService],
|
|
1240
|
+
exports: [LoggerService],
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
// CoreModule aggregates common modules
|
|
499
1244
|
const CoreModule = ProviderModule.create({
|
|
500
1245
|
id: 'CoreModule',
|
|
501
|
-
imports: [DatabaseModule, ConfigModule],
|
|
502
|
-
exports: [DatabaseModule, ConfigModule], // Re-export
|
|
1246
|
+
imports: [DatabaseModule, ConfigModule, LoggerModule],
|
|
1247
|
+
exports: [DatabaseModule, ConfigModule, LoggerModule], // Re-export all
|
|
503
1248
|
});
|
|
504
1249
|
|
|
505
|
-
// Consumers get
|
|
506
|
-
const
|
|
507
|
-
|
|
1250
|
+
// Consumers import CoreModule and get all three modules
|
|
1251
|
+
const FeatureModule = ProviderModule.create({
|
|
1252
|
+
id: 'FeatureModule',
|
|
1253
|
+
imports: [CoreModule], // Just import one module
|
|
508
1254
|
});
|
|
1255
|
+
|
|
1256
|
+
// Access all re-exported providers
|
|
1257
|
+
const db = FeatureModule.get(DatabaseService);
|
|
1258
|
+
const config = FeatureModule.get(ConfigService);
|
|
1259
|
+
const logger = FeatureModule.get(LoggerService);
|
|
509
1260
|
```
|
|
510
1261
|
|
|
1262
|
+
> [!TIP]
|
|
1263
|
+
> Create "barrel" or "core" modules that re-export commonly used modules to simplify imports throughout your application.
|
|
1264
|
+
|
|
511
1265
|
### Dynamic Module Updates
|
|
512
1266
|
|
|
513
|
-
Modules support runtime modifications
|
|
1267
|
+
Modules support runtime modifications for flexibility. Use sparingly as it can impact performance.
|
|
514
1268
|
|
|
515
1269
|
```ts
|
|
516
|
-
const
|
|
1270
|
+
const DynamicModule = ProviderModule.create({
|
|
1271
|
+
id: 'DynamicModule',
|
|
1272
|
+
providers: [ServiceA],
|
|
1273
|
+
});
|
|
517
1274
|
|
|
518
1275
|
// Add providers dynamically
|
|
519
|
-
|
|
520
|
-
|
|
1276
|
+
DynamicModule.update.addProvider(ServiceB);
|
|
1277
|
+
DynamicModule.update.addProvider(ServiceC, true); // true = also export
|
|
521
1278
|
|
|
522
1279
|
// Add imports dynamically
|
|
523
|
-
|
|
1280
|
+
const DatabaseModule = ProviderModule.create({
|
|
1281
|
+
id: 'DatabaseModule',
|
|
1282
|
+
providers: [DatabaseService],
|
|
1283
|
+
exports: [DatabaseService],
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
DynamicModule.update.addImport(DatabaseModule, true); // true = also export
|
|
1287
|
+
|
|
1288
|
+
// Check what's available
|
|
1289
|
+
console.log(DynamicModule.hasProvider(ServiceB)); // true
|
|
1290
|
+
console.log(DynamicModule.isImportingModule('DatabaseModule')); // true
|
|
1291
|
+
console.log(DynamicModule.isExportingProvider(ServiceC)); // true
|
|
1292
|
+
|
|
1293
|
+
// Remove providers and imports
|
|
1294
|
+
DynamicModule.update.removeProvider(ServiceB);
|
|
1295
|
+
DynamicModule.update.removeImport(DatabaseModule);
|
|
1296
|
+
DynamicModule.update.removeFromExports(ServiceC);
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
**Dynamic Import Propagation:**
|
|
1300
|
+
|
|
1301
|
+
```ts
|
|
1302
|
+
const ModuleA = ProviderModule.create({
|
|
1303
|
+
id: 'ModuleA',
|
|
1304
|
+
providers: [ServiceA],
|
|
1305
|
+
exports: [ServiceA],
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
const ModuleB = ProviderModule.create({
|
|
1309
|
+
id: 'ModuleB',
|
|
1310
|
+
imports: [ModuleA],
|
|
1311
|
+
exports: [ModuleA],
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
const ModuleC = ProviderModule.create({
|
|
1315
|
+
id: 'ModuleC',
|
|
1316
|
+
providers: [ServiceC],
|
|
1317
|
+
exports: [ServiceC],
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
// Initially, ModuleB doesn't have ServiceC
|
|
1321
|
+
console.log(ModuleB.hasProvider(ServiceC)); // false
|
|
1322
|
+
|
|
1323
|
+
// Dynamically import ModuleC into ModuleA and export it
|
|
1324
|
+
ModuleA.update.addImport(ModuleC, true);
|
|
1325
|
+
|
|
1326
|
+
// Now ModuleB automatically has ServiceC (import propagation!)
|
|
1327
|
+
console.log(ModuleB.hasProvider(ServiceC)); // true
|
|
524
1328
|
```
|
|
525
1329
|
|
|
526
|
-
|
|
1330
|
+
> [!WARNING]
|
|
1331
|
+
> Dynamic module updates:
|
|
1332
|
+
>
|
|
1333
|
+
> - Can impact performance if used frequently
|
|
1334
|
+
> - Should be used primarily for testing or plugin systems
|
|
1335
|
+
> - May make dependency graphs harder to understand
|
|
1336
|
+
> - Are propagated automatically to importing modules
|
|
527
1337
|
|
|
528
1338
|
### Global Modules
|
|
529
1339
|
|
|
530
|
-
Mark modules as global to auto-import into `AppModule
|
|
1340
|
+
Mark modules as global to auto-import into `AppModule`, making them available everywhere.
|
|
531
1341
|
|
|
532
1342
|
```ts
|
|
1343
|
+
@Injectable()
|
|
1344
|
+
class LoggerService {
|
|
1345
|
+
log(message: string) {
|
|
1346
|
+
console.log(`[LOG] ${message}`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Create global module
|
|
533
1351
|
const LoggerModule = ProviderModule.create({
|
|
534
1352
|
id: 'LoggerModule',
|
|
535
|
-
isGlobal: true,
|
|
1353
|
+
isGlobal: true, // Auto-imports into AppModule
|
|
536
1354
|
providers: [LoggerService],
|
|
537
1355
|
exports: [LoggerService],
|
|
538
1356
|
});
|
|
539
1357
|
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
> [!WARNING]
|
|
546
|
-
> These features provide deep customization but can add complexity. Use them only when necessary.
|
|
1358
|
+
// Now any module can access LoggerService without explicit import
|
|
1359
|
+
const FeatureModule = ProviderModule.create({
|
|
1360
|
+
id: 'FeatureModule',
|
|
1361
|
+
// No imports needed!
|
|
1362
|
+
});
|
|
547
1363
|
|
|
548
|
-
|
|
1364
|
+
const logger = FeatureModule.get(LoggerService); // Works!
|
|
1365
|
+
logger.log('Hello from FeatureModule');
|
|
1366
|
+
```
|
|
549
1367
|
|
|
550
|
-
|
|
1368
|
+
**Global Module with Blueprint:**
|
|
551
1369
|
|
|
552
1370
|
```ts
|
|
553
|
-
|
|
1371
|
+
@Injectable()
|
|
1372
|
+
class ConfigService {
|
|
1373
|
+
apiUrl = 'https://api.example.com';
|
|
1374
|
+
}
|
|
554
1375
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1376
|
+
// Blueprint with global flag
|
|
1377
|
+
const ConfigModuleBp = ProviderModule.blueprint({
|
|
1378
|
+
id: 'ConfigModule',
|
|
1379
|
+
isGlobal: true,
|
|
1380
|
+
providers: [ConfigService],
|
|
1381
|
+
exports: [ConfigService],
|
|
558
1382
|
});
|
|
559
1383
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
if (type === DefinitionEventType.Import) {
|
|
565
|
-
console.log('Module imported:', change);
|
|
566
|
-
}
|
|
567
|
-
});
|
|
1384
|
+
// Automatically imports into AppModule
|
|
1385
|
+
console.log(AppModule.isImportingModule('ConfigModule')); // true
|
|
1386
|
+
console.log(AppModule.hasProvider(ConfigService)); // true
|
|
568
1387
|
|
|
569
|
-
//
|
|
570
|
-
|
|
1388
|
+
// Available in all modules
|
|
1389
|
+
const AnyModule = ProviderModule.create({ id: 'AnyModule' });
|
|
1390
|
+
const config = AnyModule.get(ConfigService); // Works!
|
|
571
1391
|
```
|
|
572
1392
|
|
|
573
|
-
|
|
1393
|
+
> [!CAUTION]
|
|
1394
|
+
> Use global modules sparingly:
|
|
1395
|
+
>
|
|
1396
|
+
> - They create implicit dependencies that can make code harder to understand
|
|
1397
|
+
> - They reduce encapsulation and explicit dependency graphs
|
|
1398
|
+
> - Best used for true cross-cutting concerns (logging, configuration, telemetry)
|
|
1399
|
+
> - Prefer explicit imports when possible for better maintainability
|
|
574
1400
|
|
|
575
|
-
|
|
576
|
-
> Always unsubscribe to prevent memory leaks. Events fire **after** middlewares.
|
|
1401
|
+
## Dependency Injection
|
|
577
1402
|
|
|
578
|
-
###
|
|
1403
|
+
### Constructor Injection
|
|
579
1404
|
|
|
580
|
-
|
|
1405
|
+
The primary way to inject dependencies. TypeScript metadata handles it automatically with `@Injectable()`.
|
|
581
1406
|
|
|
582
1407
|
```ts
|
|
583
|
-
|
|
1408
|
+
@Injectable()
|
|
1409
|
+
class DatabaseService {
|
|
1410
|
+
query(sql: string) {
|
|
1411
|
+
return [{ data: 'result' }];
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
584
1414
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1415
|
+
@Injectable()
|
|
1416
|
+
class LoggerService {
|
|
1417
|
+
log(message: string) {
|
|
1418
|
+
console.log(message);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
589
1421
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
//
|
|
593
|
-
|
|
1422
|
+
@Injectable()
|
|
1423
|
+
class UserRepository {
|
|
1424
|
+
// Dependencies automatically injected via constructor
|
|
1425
|
+
constructor(
|
|
1426
|
+
private db: DatabaseService,
|
|
1427
|
+
private logger: LoggerService
|
|
1428
|
+
) {}
|
|
594
1429
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
1430
|
+
findAll() {
|
|
1431
|
+
this.logger.log('Finding all users');
|
|
1432
|
+
return this.db.query('SELECT * FROM users');
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
598
1435
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
value: provider,
|
|
603
|
-
};
|
|
1436
|
+
const UserModule = ProviderModule.create({
|
|
1437
|
+
id: 'UserModule',
|
|
1438
|
+
providers: [DatabaseService, LoggerService, UserRepository],
|
|
604
1439
|
});
|
|
605
1440
|
|
|
606
|
-
|
|
607
|
-
|
|
1441
|
+
// UserRepository automatically receives DatabaseService and LoggerService
|
|
1442
|
+
const userRepo = UserModule.get(UserRepository);
|
|
608
1443
|
```
|
|
609
1444
|
|
|
610
|
-
|
|
1445
|
+
### @Inject Decorator
|
|
1446
|
+
|
|
1447
|
+
Use `@Inject` for explicit injection when automatic resolution doesn't work (e.g., string tokens, interfaces).
|
|
611
1448
|
|
|
612
1449
|
```ts
|
|
613
|
-
|
|
614
|
-
// Restrict access based on importer
|
|
615
|
-
if (importerModule.id === 'UntrustedModule' && exportToken === SensitiveService) {
|
|
616
|
-
return false; // Deny access
|
|
617
|
-
}
|
|
618
|
-
return true; // Allow
|
|
619
|
-
});
|
|
620
|
-
```
|
|
1450
|
+
import { Inject, Injectable } from '@adimm/x-injection';
|
|
621
1451
|
|
|
622
|
-
|
|
1452
|
+
@Injectable()
|
|
1453
|
+
class ApiService {
|
|
1454
|
+
constructor(
|
|
1455
|
+
@Inject('API_KEY') private apiKey: string,
|
|
1456
|
+
@Inject('API_URL') private apiUrl: string,
|
|
1457
|
+
@Inject('MAX_RETRIES') private maxRetries: number
|
|
1458
|
+
) {}
|
|
1459
|
+
|
|
1460
|
+
makeRequest() {
|
|
1461
|
+
console.log(`Calling ${this.apiUrl} with key ${this.apiKey}`);
|
|
1462
|
+
console.log(`Max retries: ${this.maxRetries}`);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const ApiModule = ProviderModule.create({
|
|
1467
|
+
id: 'ApiModule',
|
|
1468
|
+
providers: [
|
|
1469
|
+
{ provide: 'API_KEY', useValue: 'secret-123' },
|
|
1470
|
+
{ provide: 'API_URL', useValue: 'https://api.example.com' },
|
|
1471
|
+
{ provide: 'MAX_RETRIES', useValue: 3 },
|
|
1472
|
+
ApiService,
|
|
1473
|
+
],
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
const apiService = ApiModule.get(ApiService);
|
|
1477
|
+
apiService.makeRequest();
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
**Injecting Abstract Classes:**
|
|
1481
|
+
|
|
1482
|
+
```ts
|
|
1483
|
+
@Injectable()
|
|
1484
|
+
abstract class PaymentGateway {
|
|
1485
|
+
abstract charge(amount: number): Promise<void>;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
@Injectable()
|
|
1489
|
+
class StripePaymentGateway extends PaymentGateway {
|
|
1490
|
+
async charge(amount: number) {
|
|
1491
|
+
console.log(`Stripe: Charging $${amount}`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
@Injectable()
|
|
1496
|
+
class PaymentService {
|
|
1497
|
+
constructor(@Inject(PaymentGateway) private gateway: PaymentGateway) {}
|
|
1498
|
+
|
|
1499
|
+
async processPayment(amount: number) {
|
|
1500
|
+
await this.gateway.charge(amount);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const PaymentModule = ProviderModule.create({
|
|
1505
|
+
id: 'PaymentModule',
|
|
1506
|
+
providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }, PaymentService],
|
|
1507
|
+
});
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
### @MultiInject Decorator
|
|
1511
|
+
|
|
1512
|
+
Inject multiple providers bound to the same token as an array.
|
|
1513
|
+
|
|
1514
|
+
```ts
|
|
1515
|
+
import { Injectable, MultiInject } from '@adimm/x-injection';
|
|
1516
|
+
|
|
1517
|
+
@Injectable()
|
|
1518
|
+
class EmailNotifier {
|
|
1519
|
+
notify() {
|
|
1520
|
+
console.log('Email notification sent');
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
@Injectable()
|
|
1525
|
+
class SmsNotifier {
|
|
1526
|
+
notify() {
|
|
1527
|
+
console.log('SMS notification sent');
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
@Injectable()
|
|
1532
|
+
class PushNotifier {
|
|
1533
|
+
notify() {
|
|
1534
|
+
console.log('Push notification sent');
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
abstract class Notifier {
|
|
1539
|
+
abstract notify(): void;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
@Injectable()
|
|
1543
|
+
class NotificationService {
|
|
1544
|
+
constructor(@MultiInject(Notifier) private notifiers: Notifier[]) {}
|
|
1545
|
+
|
|
1546
|
+
notifyAll() {
|
|
1547
|
+
this.notifiers.forEach((notifier) => notifier.notify());
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
const NotificationModule = ProviderModule.create({
|
|
1552
|
+
id: 'NotificationModule',
|
|
1553
|
+
providers: [
|
|
1554
|
+
{ provide: Notifier, useClass: EmailNotifier },
|
|
1555
|
+
{ provide: Notifier, useClass: SmsNotifier },
|
|
1556
|
+
{ provide: Notifier, useClass: PushNotifier },
|
|
1557
|
+
NotificationService,
|
|
1558
|
+
],
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
const service = NotificationModule.get(NotificationService);
|
|
1562
|
+
service.notifyAll();
|
|
1563
|
+
// Output:
|
|
1564
|
+
// Email notification sent
|
|
1565
|
+
// SMS notification sent
|
|
1566
|
+
// Push notification sent
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
**Alternative with module.get():**
|
|
1570
|
+
|
|
1571
|
+
```ts
|
|
1572
|
+
const MyModule = ProviderModule.create({
|
|
1573
|
+
id: 'MyModule',
|
|
1574
|
+
providers: [
|
|
1575
|
+
{ provide: 'Handler', useValue: 'Handler1' },
|
|
1576
|
+
{ provide: 'Handler', useValue: 'Handler2' },
|
|
1577
|
+
{ provide: 'Handler', useValue: 'Handler3' },
|
|
1578
|
+
],
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
// Get all providers bound to 'Handler'
|
|
1582
|
+
const handlers = MyModule.get('Handler', false, true); // Third param = asList
|
|
1583
|
+
console.log(handlers); // ['Handler1', 'Handler2', 'Handler3']
|
|
1584
|
+
```
|
|
1585
|
+
|
|
1586
|
+
### Optional Dependencies
|
|
1587
|
+
|
|
1588
|
+
Use the `isOptional` flag to handle missing dependencies gracefully.
|
|
1589
|
+
|
|
1590
|
+
```ts
|
|
1591
|
+
@Injectable()
|
|
1592
|
+
class ServiceA {
|
|
1593
|
+
value = 'A';
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
@Injectable()
|
|
1597
|
+
class ServiceB {
|
|
1598
|
+
constructor(
|
|
1599
|
+
private serviceA: ServiceA,
|
|
1600
|
+
@Inject('OPTIONAL_CONFIG') private config?: any
|
|
1601
|
+
) {}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const MyModule = ProviderModule.create({
|
|
1605
|
+
id: 'MyModule',
|
|
1606
|
+
providers: [ServiceA, ServiceB],
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
// Get with optional flag
|
|
1610
|
+
const optionalService = MyModule.get('NOT_EXISTS', true); // isOptional = true
|
|
1611
|
+
console.log(optionalService); // undefined (no error thrown)
|
|
1612
|
+
|
|
1613
|
+
// Without optional flag (throws error)
|
|
1614
|
+
try {
|
|
1615
|
+
const service = MyModule.get('NOT_EXISTS'); // Throws!
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
console.error('Provider not found');
|
|
1618
|
+
}
|
|
1619
|
+
```
|
|
1620
|
+
|
|
1621
|
+
> [!TIP]
|
|
1622
|
+
> Use `@Inject` when:
|
|
1623
|
+
>
|
|
1624
|
+
> - Injecting string tokens or symbols
|
|
1625
|
+
> - Injecting abstract classes
|
|
1626
|
+
> - TypeScript's automatic injection doesn't work (interfaces, etc.)
|
|
1627
|
+
>
|
|
1628
|
+
> Use `@MultiInject` when:
|
|
1629
|
+
>
|
|
1630
|
+
> - You want to collect all providers bound to a single token
|
|
1631
|
+
> - Implementing plugin systems
|
|
1632
|
+
> - Working with strategy patterns
|
|
1633
|
+
|
|
1634
|
+
## Lifecycle Hooks
|
|
1635
|
+
|
|
1636
|
+
Lifecycle hooks allow you to execute code at specific points in a module's lifecycle.
|
|
1637
|
+
|
|
1638
|
+
### onReady Hook
|
|
1639
|
+
|
|
1640
|
+
Invoked immediately after module creation. Perfect for initialization logic.
|
|
1641
|
+
|
|
1642
|
+
```ts
|
|
1643
|
+
@Injectable()
|
|
1644
|
+
class DatabaseService {
|
|
1645
|
+
connected = false;
|
|
1646
|
+
|
|
1647
|
+
async connect() {
|
|
1648
|
+
console.log('Connecting to database...');
|
|
1649
|
+
this.connected = true;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const DatabaseModule = ProviderModule.create({
|
|
1654
|
+
id: 'DatabaseModule',
|
|
1655
|
+
providers: [DatabaseService],
|
|
1656
|
+
onReady: async (module) => {
|
|
1657
|
+
console.log('DatabaseModule is ready!');
|
|
1658
|
+
|
|
1659
|
+
// Initialize services
|
|
1660
|
+
const db = module.get(DatabaseService);
|
|
1661
|
+
await db.connect();
|
|
1662
|
+
|
|
1663
|
+
console.log('Database connected:', db.connected);
|
|
1664
|
+
},
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
// Output:
|
|
1668
|
+
// DatabaseModule is ready!
|
|
1669
|
+
// Connecting to database...
|
|
1670
|
+
// Database connected: true
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
### onReset Hook
|
|
1674
|
+
|
|
1675
|
+
Invoked when `module.reset()` is called. Provides `before` and `after` callbacks for cleanup and reinitialization.
|
|
1676
|
+
|
|
1677
|
+
```ts
|
|
1678
|
+
@Injectable()
|
|
1679
|
+
class CacheService {
|
|
1680
|
+
cache = new Map();
|
|
1681
|
+
|
|
1682
|
+
clear() {
|
|
1683
|
+
this.cache.clear();
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const CacheModule = ProviderModule.create({
|
|
1688
|
+
id: 'CacheModule',
|
|
1689
|
+
providers: [CacheService],
|
|
1690
|
+
onReset: () => {
|
|
1691
|
+
return {
|
|
1692
|
+
before: async (mod) => {
|
|
1693
|
+
console.log('Before reset - clearing cache');
|
|
1694
|
+
const cache = mod.get(CacheService);
|
|
1695
|
+
cache.clear();
|
|
1696
|
+
},
|
|
1697
|
+
after: async () => {
|
|
1698
|
+
console.log('After reset - cache reinitialized');
|
|
1699
|
+
},
|
|
1700
|
+
};
|
|
1701
|
+
},
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
// Trigger reset
|
|
1705
|
+
await CacheModule.reset();
|
|
1706
|
+
// Output:
|
|
1707
|
+
// Before reset - clearing cache
|
|
1708
|
+
// After reset - cache reinitialized
|
|
1709
|
+
```
|
|
1710
|
+
|
|
1711
|
+
### onDispose Hook
|
|
1712
|
+
|
|
1713
|
+
Invoked when `module.dispose()` is called. Perfect for cleanup tasks like closing connections.
|
|
1714
|
+
|
|
1715
|
+
```ts
|
|
1716
|
+
@Injectable()
|
|
1717
|
+
class DatabaseService {
|
|
1718
|
+
connected = true;
|
|
1719
|
+
|
|
1720
|
+
async disconnect() {
|
|
1721
|
+
console.log('Disconnecting from database...');
|
|
1722
|
+
this.connected = false;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
@Injectable()
|
|
1727
|
+
class FileService {
|
|
1728
|
+
async closeFiles() {
|
|
1729
|
+
console.log('Closing open files...');
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
const AppModule = ProviderModule.create({
|
|
1734
|
+
id: 'AppModule',
|
|
1735
|
+
providers: [DatabaseService, FileService],
|
|
1736
|
+
onDispose: () => {
|
|
1737
|
+
return {
|
|
1738
|
+
before: async (mod) => {
|
|
1739
|
+
console.log('Cleanup started');
|
|
1740
|
+
const db = mod.get(DatabaseService);
|
|
1741
|
+
const files = mod.get(FileService);
|
|
1742
|
+
|
|
1743
|
+
await db.disconnect();
|
|
1744
|
+
await files.closeFiles();
|
|
1745
|
+
},
|
|
1746
|
+
after: async () => {
|
|
1747
|
+
console.log('Cleanup completed');
|
|
1748
|
+
},
|
|
1749
|
+
};
|
|
1750
|
+
},
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
// Dispose module
|
|
1754
|
+
await AppModule.dispose();
|
|
1755
|
+
// Output:
|
|
1756
|
+
// Cleanup started
|
|
1757
|
+
// Disconnecting from database...
|
|
1758
|
+
// Closing open files...
|
|
1759
|
+
// Cleanup completed
|
|
1760
|
+
|
|
1761
|
+
// Module is now disposed
|
|
1762
|
+
console.log(AppModule.isDisposed); // true
|
|
1763
|
+
|
|
1764
|
+
// Subsequent operations throw error
|
|
1765
|
+
try {
|
|
1766
|
+
AppModule.get(DatabaseService);
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
console.error('Cannot access disposed module');
|
|
1769
|
+
}
|
|
1770
|
+
```
|
|
1771
|
+
|
|
1772
|
+
**Complete Lifecycle Example:**
|
|
1773
|
+
|
|
1774
|
+
```ts
|
|
1775
|
+
@Injectable()
|
|
1776
|
+
class ResourceService {
|
|
1777
|
+
initialized = false;
|
|
1778
|
+
|
|
1779
|
+
async initialize() {
|
|
1780
|
+
console.log('Initializing resource...');
|
|
1781
|
+
this.initialized = true;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
async cleanup() {
|
|
1785
|
+
console.log('Cleaning up resource...');
|
|
1786
|
+
this.initialized = false;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
const ResourceModule = ProviderModule.create({
|
|
1791
|
+
id: 'ResourceModule',
|
|
1792
|
+
providers: [ResourceService],
|
|
1793
|
+
|
|
1794
|
+
onReady: async (module) => {
|
|
1795
|
+
console.log('[READY] Module created');
|
|
1796
|
+
const service = module.get(ResourceService);
|
|
1797
|
+
await service.initialize();
|
|
1798
|
+
},
|
|
1799
|
+
|
|
1800
|
+
onReset: () => {
|
|
1801
|
+
console.log('[RESET] Resetting module');
|
|
1802
|
+
return {
|
|
1803
|
+
before: async (mod) => {
|
|
1804
|
+
console.log('[RESET:BEFORE] Cleaning up before reset');
|
|
1805
|
+
const service = mod.get(ResourceService);
|
|
1806
|
+
await service.cleanup();
|
|
1807
|
+
},
|
|
1808
|
+
after: async () => {
|
|
1809
|
+
console.log('[RESET:AFTER] Reinitializing after reset');
|
|
1810
|
+
const service = mod.get(ResourceService);
|
|
1811
|
+
await service.initialize();
|
|
1812
|
+
},
|
|
1813
|
+
};
|
|
1814
|
+
},
|
|
1815
|
+
|
|
1816
|
+
onDispose: () => {
|
|
1817
|
+
console.log('[DISPOSE] Disposing module');
|
|
1818
|
+
return {
|
|
1819
|
+
before: async (mod) => {
|
|
1820
|
+
console.log('[DISPOSE:BEFORE] Final cleanup');
|
|
1821
|
+
const service = mod.get(ResourceService);
|
|
1822
|
+
await service.cleanup();
|
|
1823
|
+
},
|
|
1824
|
+
after: async () => {
|
|
1825
|
+
console.log('[DISPOSE:AFTER] Module fully disposed');
|
|
1826
|
+
},
|
|
1827
|
+
};
|
|
1828
|
+
},
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
// Usage
|
|
1832
|
+
await ResourceModule.reset();
|
|
1833
|
+
await ResourceModule.dispose();
|
|
1834
|
+
```
|
|
1835
|
+
|
|
1836
|
+
> [!IMPORTANT]
|
|
1837
|
+
> Lifecycle hook execution order:
|
|
1838
|
+
>
|
|
1839
|
+
> 1. **onReady** - Immediately after module creation
|
|
1840
|
+
> 2. **onReset** (before) → module reset → **onReset** (after)
|
|
1841
|
+
> 3. **onDispose** (before) → module disposal → **onDispose** (after)
|
|
1842
|
+
|
|
1843
|
+
> [!WARNING]
|
|
1844
|
+
> After calling `dispose()`:
|
|
1845
|
+
>
|
|
1846
|
+
> - All module operations will throw errors
|
|
1847
|
+
> - The module cannot be reused
|
|
1848
|
+
> - Internal resources are cleaned up
|
|
1849
|
+
> - Use for application shutdown or when modules are truly finished
|
|
1850
|
+
|
|
1851
|
+
## Events System
|
|
1852
|
+
|
|
1853
|
+
The events system allows you to observe and react to module changes in real-time.
|
|
1854
|
+
|
|
1855
|
+
### Subscribing to Events
|
|
1856
|
+
|
|
1857
|
+
```ts
|
|
1858
|
+
import { DefinitionEventType } from '@adimm/x-injection';
|
|
1859
|
+
|
|
1860
|
+
const MyModule = ProviderModule.create({
|
|
1861
|
+
id: 'MyModule',
|
|
1862
|
+
providers: [ServiceA],
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
// Subscribe to all events
|
|
1866
|
+
const unsubscribe = MyModule.update.subscribe(({ type, change }) => {
|
|
1867
|
+
console.log(`Event: ${DefinitionEventType[type]}`, change);
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
// Trigger events
|
|
1871
|
+
MyModule.update.addProvider(ServiceB); // Event: Provider
|
|
1872
|
+
MyModule.update.addImport(OtherModule); // Event: Import
|
|
1873
|
+
const service = MyModule.get(ServiceA); // Event: GetProvider
|
|
1874
|
+
|
|
1875
|
+
// Clean up
|
|
1876
|
+
unsubscribe();
|
|
1877
|
+
```
|
|
1878
|
+
|
|
1879
|
+
### Available Event Types
|
|
1880
|
+
|
|
1881
|
+
```ts
|
|
1882
|
+
enum DefinitionEventType {
|
|
1883
|
+
Noop, // No operation
|
|
1884
|
+
Import, // Module/blueprint added
|
|
1885
|
+
Provider, // Provider added
|
|
1886
|
+
GetProvider, // Provider resolved
|
|
1887
|
+
Export, // Export added
|
|
1888
|
+
ExportModule, // Module added to exports
|
|
1889
|
+
ExportProvider, // Provider added to exports
|
|
1890
|
+
ImportRemoved, // Module removed
|
|
1891
|
+
ProviderRemoved, // Provider removed
|
|
1892
|
+
ExportRemoved, // Export removed
|
|
1893
|
+
ExportModuleRemoved, // Module removed from exports
|
|
1894
|
+
ExportProviderRemoved, // Provider removed from exports
|
|
1895
|
+
}
|
|
1896
|
+
```
|
|
1897
|
+
|
|
1898
|
+
### Event Use Cases
|
|
1899
|
+
|
|
1900
|
+
**Monitoring Provider Resolution:**
|
|
1901
|
+
|
|
1902
|
+
```ts
|
|
1903
|
+
const MonitoredModule = ProviderModule.create({
|
|
1904
|
+
id: 'MonitoredModule',
|
|
1905
|
+
providers: [DatabaseService, CacheService],
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
MonitoredModule.update.subscribe(({ type, change }) => {
|
|
1909
|
+
if (type === DefinitionEventType.GetProvider) {
|
|
1910
|
+
console.log('Provider accessed:', change.constructor.name);
|
|
1911
|
+
console.log('Access time:', new Date().toISOString());
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
// Logs access
|
|
1916
|
+
const db = MonitoredModule.get(DatabaseService);
|
|
1917
|
+
// Output: Provider accessed: DatabaseService
|
|
1918
|
+
// Access time: 2024-01-15T10:30:00.000Z
|
|
1919
|
+
```
|
|
1920
|
+
|
|
1921
|
+
**Tracking Module Composition:**
|
|
1922
|
+
|
|
1923
|
+
```ts
|
|
1924
|
+
const RootModule = ProviderModule.create({
|
|
1925
|
+
id: 'RootModule',
|
|
1926
|
+
});
|
|
1927
|
+
|
|
1928
|
+
const compositionLog: string[] = [];
|
|
1929
|
+
|
|
1930
|
+
RootModule.update.subscribe(({ type, change }) => {
|
|
1931
|
+
switch (type) {
|
|
1932
|
+
case DefinitionEventType.Import:
|
|
1933
|
+
compositionLog.push(`Imported: ${change.id}`);
|
|
1934
|
+
break;
|
|
1935
|
+
case DefinitionEventType.Provider:
|
|
1936
|
+
const providerName = typeof change === 'function' ? change.name : change.provide;
|
|
1937
|
+
compositionLog.push(`Added provider: ${providerName}`);
|
|
1938
|
+
break;
|
|
1939
|
+
case DefinitionEventType.Export:
|
|
1940
|
+
compositionLog.push(`Exported: ${JSON.stringify(change)}`);
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
RootModule.update.addImport(DatabaseModule);
|
|
1946
|
+
RootModule.update.addProvider(ServiceA);
|
|
1947
|
+
RootModule.update.addProvider(ServiceB, true);
|
|
1948
|
+
|
|
1949
|
+
console.log(compositionLog);
|
|
1950
|
+
// [
|
|
1951
|
+
// 'Imported: DatabaseModule',
|
|
1952
|
+
// 'Added provider: ServiceA',
|
|
1953
|
+
// 'Added provider: ServiceB',
|
|
1954
|
+
// 'Exported: ServiceB'
|
|
1955
|
+
// ]
|
|
1956
|
+
```
|
|
1957
|
+
|
|
1958
|
+
**Debugging Dynamic Changes:**
|
|
1959
|
+
|
|
1960
|
+
```ts
|
|
1961
|
+
const DebugModule = ProviderModule.create({
|
|
1962
|
+
id: 'DebugModule',
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
DebugModule.update.subscribe(({ type, change }) => {
|
|
1966
|
+
const eventName = DefinitionEventType[type];
|
|
1967
|
+
|
|
1968
|
+
if (type === DefinitionEventType.ImportRemoved) {
|
|
1969
|
+
console.warn(`⚠️ Module removed: ${change.id}`);
|
|
1970
|
+
} else if (type === DefinitionEventType.ProviderRemoved) {
|
|
1971
|
+
console.warn(`⚠️ Provider removed:`, change);
|
|
1972
|
+
} else {
|
|
1973
|
+
console.log(`✅ ${eventName}:`, change);
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
DebugModule.update.addProvider(ServiceA);
|
|
1978
|
+
DebugModule.update.removeProvider(ServiceA);
|
|
1979
|
+
```
|
|
1980
|
+
|
|
1981
|
+
**Building a Module Activity Logger:**
|
|
1982
|
+
|
|
1983
|
+
```ts
|
|
1984
|
+
class ModuleActivityLogger {
|
|
1985
|
+
private events: Array<{ timestamp: number; type: string; change: any }> = [];
|
|
1986
|
+
|
|
1987
|
+
constructor(module: ProviderModule) {
|
|
1988
|
+
module.update.subscribe(({ type, change }) => {
|
|
1989
|
+
this.events.push({
|
|
1990
|
+
timestamp: Date.now(),
|
|
1991
|
+
type: DefinitionEventType[type],
|
|
1992
|
+
change,
|
|
1993
|
+
});
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
getReport() {
|
|
1998
|
+
return {
|
|
1999
|
+
totalEvents: this.events.length,
|
|
2000
|
+
events: this.events,
|
|
2001
|
+
summary: this.events.reduce(
|
|
2002
|
+
(acc, event) => {
|
|
2003
|
+
acc[event.type] = (acc[event.type] || 0) + 1;
|
|
2004
|
+
return acc;
|
|
2005
|
+
},
|
|
2006
|
+
{} as Record<string, number>
|
|
2007
|
+
),
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
const TrackedModule = ProviderModule.create({ id: 'TrackedModule' });
|
|
2013
|
+
const logger = new ModuleActivityLogger(TrackedModule);
|
|
2014
|
+
|
|
2015
|
+
TrackedModule.update.addProvider(ServiceA);
|
|
2016
|
+
TrackedModule.update.addProvider(ServiceB);
|
|
2017
|
+
TrackedModule.get(ServiceA);
|
|
2018
|
+
TrackedModule.get(ServiceB);
|
|
2019
|
+
|
|
2020
|
+
console.log(logger.getReport());
|
|
2021
|
+
// {
|
|
2022
|
+
// totalEvents: 4,
|
|
2023
|
+
// events: [...],
|
|
2024
|
+
// summary: { Provider: 2, GetProvider: 2 }
|
|
2025
|
+
// }
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
> [!WARNING]
|
|
2029
|
+
>
|
|
2030
|
+
> - Always call `unsubscribe()` to prevent memory leaks
|
|
2031
|
+
> - Events fire **after** middlewares have executed
|
|
2032
|
+
> - Event handlers are synchronous - avoid heavy operations
|
|
2033
|
+
> - High-frequency events (like `GetProvider`) can impact performance
|
|
2034
|
+
|
|
2035
|
+
## Middlewares
|
|
2036
|
+
|
|
2037
|
+
Middlewares intercept and transform module operations before they complete. They provide powerful customization capabilities.
|
|
2038
|
+
|
|
2039
|
+
### BeforeGet Middleware
|
|
2040
|
+
|
|
2041
|
+
Transform provider values before they're returned to consumers.
|
|
2042
|
+
|
|
2043
|
+
```ts
|
|
2044
|
+
import { MiddlewareType } from '@adimm/x-injection';
|
|
2045
|
+
|
|
2046
|
+
@Injectable()
|
|
2047
|
+
class UserService {
|
|
2048
|
+
getUser() {
|
|
2049
|
+
return { id: 1, name: 'Alice' };
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
const MyModule = ProviderModule.create({
|
|
2054
|
+
id: 'MyModule',
|
|
2055
|
+
providers: [UserService],
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
// Wrap resolved providers with metadata
|
|
2059
|
+
MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
|
|
2060
|
+
// Return true to pass through unchanged
|
|
2061
|
+
if (!(provider instanceof UserService)) return true;
|
|
2062
|
+
|
|
2063
|
+
// Transform the value
|
|
2064
|
+
return {
|
|
2065
|
+
timestamp: Date.now(),
|
|
2066
|
+
instance: provider,
|
|
2067
|
+
metadata: { cached: false },
|
|
2068
|
+
};
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
const result = MyModule.get(UserService);
|
|
2072
|
+
console.log(result);
|
|
2073
|
+
// {
|
|
2074
|
+
// timestamp: 1705320000000,
|
|
2075
|
+
// instance: UserService { ... },
|
|
2076
|
+
// metadata: { cached: false }
|
|
2077
|
+
// }
|
|
2078
|
+
```
|
|
2079
|
+
|
|
2080
|
+
**Conditional Transformation:**
|
|
2081
|
+
|
|
2082
|
+
```ts
|
|
2083
|
+
@Injectable()
|
|
2084
|
+
class ServiceA {}
|
|
2085
|
+
|
|
2086
|
+
@Injectable()
|
|
2087
|
+
class ServiceB {}
|
|
2088
|
+
|
|
2089
|
+
MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token) => {
|
|
2090
|
+
// Only transform ServiceA
|
|
2091
|
+
if (provider instanceof ServiceA) {
|
|
2092
|
+
return { wrapped: provider, type: 'A' };
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// Pass through everything else unchanged
|
|
2096
|
+
return true;
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
const serviceA = MyModule.get(ServiceA); // { wrapped: ServiceA, type: 'A' }
|
|
2100
|
+
const serviceB = MyModule.get(ServiceB); // ServiceB (unchanged)
|
|
2101
|
+
```
|
|
2102
|
+
|
|
2103
|
+
**Using inject() to avoid infinite loops:**
|
|
2104
|
+
|
|
2105
|
+
```ts
|
|
2106
|
+
@Injectable()
|
|
2107
|
+
class LoggerService {
|
|
2108
|
+
log(message: string) {
|
|
2109
|
+
console.log(message);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
@Injectable()
|
|
2114
|
+
class PaymentService {}
|
|
2115
|
+
|
|
2116
|
+
MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
|
|
2117
|
+
if (!(provider instanceof PaymentService)) return true;
|
|
2118
|
+
|
|
2119
|
+
// Use inject() instead of module.get() to avoid infinite loop
|
|
2120
|
+
const logger = inject(LoggerService);
|
|
2121
|
+
logger.log('Payment service accessed');
|
|
2122
|
+
|
|
2123
|
+
return provider; // Or transform it
|
|
2124
|
+
});
|
|
2125
|
+
```
|
|
2126
|
+
|
|
2127
|
+
### BeforeAddProvider Middleware
|
|
2128
|
+
|
|
2129
|
+
Block specific providers:
|
|
2130
|
+
|
|
2131
|
+
```ts
|
|
2132
|
+
MyModule.middlewares.add(MiddlewareType.BeforeAddProvider, (provider) => {
|
|
2133
|
+
// Block ServiceB from being added
|
|
2134
|
+
if ((provider as any).name === 'ServiceB') {
|
|
2135
|
+
return false; // Abort
|
|
2136
|
+
}
|
|
2137
|
+
return true; // Allow
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
MyModule.update.addProvider(ServiceA);
|
|
2141
|
+
MyModule.update.addProvider(ServiceB); // Silently rejected
|
|
2142
|
+
MyModule.update.addProvider(ServiceC);
|
|
2143
|
+
|
|
2144
|
+
console.log(MyModule.hasProvider(ServiceA)); // true
|
|
2145
|
+
console.log(MyModule.hasProvider(ServiceB)); // false
|
|
2146
|
+
console.log(MyModule.hasProvider(ServiceC)); // true
|
|
2147
|
+
```
|
|
2148
|
+
|
|
2149
|
+
### BeforeAddImport Middleware
|
|
2150
|
+
|
|
2151
|
+
Intercept modules before they're imported.
|
|
2152
|
+
|
|
2153
|
+
```ts
|
|
2154
|
+
const Module1 = ProviderModule.create({ id: 'Module1' });
|
|
2155
|
+
const Module2 = ProviderModule.create({ id: 'Module2' });
|
|
2156
|
+
const RestrictedModule = ProviderModule.create({ id: 'RestrictedModule' });
|
|
2157
|
+
|
|
2158
|
+
const MainModule = ProviderModule.create({ id: 'MainModule' });
|
|
2159
|
+
|
|
2160
|
+
// Block specific modules
|
|
2161
|
+
MainModule.middlewares.add(MiddlewareType.BeforeAddImport, (module) => {
|
|
2162
|
+
if (module.id === 'RestrictedModule') {
|
|
2163
|
+
console.warn(`❌ Cannot import ${module.id}`);
|
|
2164
|
+
return false; // Block
|
|
2165
|
+
}
|
|
2166
|
+
return true; // Allow
|
|
2167
|
+
});
|
|
2168
|
+
|
|
2169
|
+
MainModule.update.addImport(Module1); // ✅ Allowed
|
|
2170
|
+
MainModule.update.addImport(Module2); // ✅ Allowed
|
|
2171
|
+
MainModule.update.addImport(RestrictedModule); // ❌ Blocked
|
|
2172
|
+
|
|
2173
|
+
console.log(MainModule.isImportingModule('Module1')); // true
|
|
2174
|
+
console.log(MainModule.isImportingModule('RestrictedModule')); // false
|
|
2175
|
+
```
|
|
2176
|
+
|
|
2177
|
+
**Auto-add providers to imported modules:**
|
|
2178
|
+
|
|
2179
|
+
```ts
|
|
2180
|
+
MyModule.middlewares.add(MiddlewareType.BeforeAddImport, (importedModule) => {
|
|
2181
|
+
// Add logger to every imported module
|
|
2182
|
+
importedModule.update.addProvider(LoggerService, true);
|
|
2183
|
+
return importedModule; // Return modified module
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
MyModule.update.addImport(FeatureModule);
|
|
2187
|
+
// FeatureModule now has LoggerService
|
|
2188
|
+
```
|
|
2189
|
+
|
|
2190
|
+
### OnExportAccess Middleware
|
|
2191
|
+
|
|
2192
|
+
Control which importing modules can access exports.
|
|
2193
|
+
|
|
2194
|
+
```ts
|
|
2195
|
+
@Injectable()
|
|
2196
|
+
class SensitiveService {}
|
|
2197
|
+
|
|
2198
|
+
@Injectable()
|
|
2199
|
+
class PublicService {}
|
|
2200
|
+
|
|
2201
|
+
const SecureModule = ProviderModule.create({
|
|
2202
|
+
id: 'SecureModule',
|
|
2203
|
+
providers: [SensitiveService, PublicService],
|
|
2204
|
+
exports: [SensitiveService, PublicService],
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
// Restrict access based on importer
|
|
2208
|
+
SecureModule.middlewares.add(MiddlewareType.OnExportAccess, (importerModule, exportToken) => {
|
|
2209
|
+
// Block untrusted modules from accessing SensitiveService
|
|
2210
|
+
if (importerModule.id === 'UntrustedModule' && exportToken === SensitiveService) {
|
|
2211
|
+
console.warn(`❌ ${importerModule.id} denied access to SensitiveService`);
|
|
2212
|
+
return false; // Deny
|
|
2213
|
+
}
|
|
2214
|
+
return true; // Allow
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
const TrustedModule = ProviderModule.create({
|
|
2218
|
+
id: 'TrustedModule',
|
|
2219
|
+
imports: [SecureModule],
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
const UntrustedModule = ProviderModule.create({
|
|
2223
|
+
id: 'UntrustedModule',
|
|
2224
|
+
imports: [SecureModule],
|
|
2225
|
+
});
|
|
2226
|
+
|
|
2227
|
+
// Trusted module can access both
|
|
2228
|
+
console.log(TrustedModule.hasProvider(SensitiveService)); // true
|
|
2229
|
+
console.log(TrustedModule.hasProvider(PublicService)); // true
|
|
2230
|
+
|
|
2231
|
+
// Untrusted module blocked from SensitiveService
|
|
2232
|
+
console.log(UntrustedModule.hasProvider(SensitiveService)); // false
|
|
2233
|
+
console.log(UntrustedModule.hasProvider(PublicService)); // true
|
|
2234
|
+
```
|
|
2235
|
+
|
|
2236
|
+
**Complete access control:**
|
|
2237
|
+
|
|
2238
|
+
```ts
|
|
2239
|
+
SecureModule.middlewares.add(MiddlewareType.OnExportAccess, (importer, exportToken) => {
|
|
2240
|
+
const allowlist = ['TrustedModule1', 'TrustedModule2'];
|
|
2241
|
+
|
|
2242
|
+
if (!allowlist.includes(String(importer.id))) {
|
|
2243
|
+
console.warn(`Access denied for ${importer.id}`);
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
return true;
|
|
2248
|
+
});
|
|
2249
|
+
```
|
|
2250
|
+
|
|
2251
|
+
### BeforeRemoveImport Middleware
|
|
2252
|
+
|
|
2253
|
+
Prevent specific modules from being removed.
|
|
2254
|
+
|
|
2255
|
+
```ts
|
|
2256
|
+
const PermanentModule = ProviderModule.create({ id: 'PermanentModule' });
|
|
2257
|
+
const TemporaryModule = ProviderModule.create({ id: 'TemporaryModule' });
|
|
2258
|
+
|
|
2259
|
+
const MainModule = ProviderModule.create({ id: 'MainModule' });
|
|
2260
|
+
|
|
2261
|
+
// Protect PermanentModule
|
|
2262
|
+
MainModule.middlewares.add(MiddlewareType.BeforeRemoveImport, (module) => {
|
|
2263
|
+
if (module.id === 'PermanentModule') {
|
|
2264
|
+
console.warn(`⚠️ Cannot remove ${module.id}`);
|
|
2265
|
+
return false; // Block removal
|
|
2266
|
+
}
|
|
2267
|
+
return true; // Allow removal
|
|
2268
|
+
});
|
|
2269
|
+
|
|
2270
|
+
MainModule.update.addImport(PermanentModule);
|
|
2271
|
+
MainModule.update.addImport(TemporaryModule);
|
|
2272
|
+
|
|
2273
|
+
// Try to remove
|
|
2274
|
+
MainModule.update.removeImport(PermanentModule); // ❌ Blocked
|
|
2275
|
+
MainModule.update.removeImport(TemporaryModule); // ✅ Removed
|
|
2276
|
+
|
|
2277
|
+
console.log(MainModule.isImportingModule('PermanentModule')); // true
|
|
2278
|
+
console.log(MainModule.isImportingModule('TemporaryModule')); // false
|
|
2279
|
+
```
|
|
2280
|
+
|
|
2281
|
+
### BeforeRemoveProvider Middleware
|
|
2282
|
+
|
|
2283
|
+
Prevent specific providers from being removed.
|
|
2284
|
+
|
|
2285
|
+
```ts
|
|
2286
|
+
MyModule.middlewares.add(MiddlewareType.BeforeRemoveProvider, (provider) => {
|
|
2287
|
+
// Block removal of critical services
|
|
2288
|
+
if (provider === DatabaseService) {
|
|
2289
|
+
console.warn('⚠️ Cannot remove DatabaseService');
|
|
2290
|
+
return false;
|
|
2291
|
+
}
|
|
2292
|
+
return true;
|
|
2293
|
+
});
|
|
2294
|
+
|
|
2295
|
+
MyModule.update.addProvider(DatabaseService);
|
|
2296
|
+
MyModule.update.addProvider(CacheService);
|
|
2297
|
+
|
|
2298
|
+
MyModule.update.removeProvider(DatabaseService); // ❌ Blocked
|
|
2299
|
+
MyModule.update.removeProvider(CacheService); // ✅ Removed
|
|
2300
|
+
|
|
2301
|
+
console.log(MyModule.hasProvider(DatabaseService)); // true
|
|
2302
|
+
console.log(MyModule.hasProvider(CacheService)); // false
|
|
2303
|
+
```
|
|
2304
|
+
|
|
2305
|
+
### BeforeRemoveExport Middleware
|
|
2306
|
+
|
|
2307
|
+
Prevent specific exports from being removed.
|
|
2308
|
+
|
|
2309
|
+
```ts
|
|
2310
|
+
import { ProviderModuleHelpers } from '@adimm/x-injection';
|
|
2311
|
+
|
|
2312
|
+
const MyModule = ProviderModule.create({
|
|
2313
|
+
id: 'MyModule',
|
|
2314
|
+
providers: [ServiceA, ServiceB],
|
|
2315
|
+
exports: [ServiceA, ServiceB],
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
MyModule.middlewares.add(MiddlewareType.BeforeRemoveExport, (exportDef) => {
|
|
2319
|
+
// Check if it's a module or provider
|
|
2320
|
+
if (ProviderModuleHelpers.isModule(exportDef)) {
|
|
2321
|
+
// Block module removal
|
|
2322
|
+
return exportDef.id !== 'ProtectedModule';
|
|
2323
|
+
} else {
|
|
2324
|
+
// Block ServiceA removal
|
|
2325
|
+
return exportDef !== ServiceA;
|
|
2326
|
+
}
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
MyModule.update.removeFromExports(ServiceA); // ❌ Blocked
|
|
2330
|
+
MyModule.update.removeFromExports(ServiceB); // ✅ Removed
|
|
2331
|
+
|
|
2332
|
+
console.log(MyModule.isExportingProvider(ServiceA)); // true
|
|
2333
|
+
console.log(MyModule.isExportingProvider(ServiceB)); // false
|
|
2334
|
+
```
|
|
2335
|
+
|
|
2336
|
+
### All Available Middleware Types
|
|
2337
|
+
|
|
2338
|
+
```ts
|
|
2339
|
+
enum MiddlewareType {
|
|
2340
|
+
BeforeAddImport, // Before importing a module
|
|
2341
|
+
BeforeAddProvider, // Before adding a provider
|
|
2342
|
+
BeforeGet, // Before returning provider to consumer
|
|
2343
|
+
BeforeRemoveImport, // Before removing an import
|
|
2344
|
+
BeforeRemoveProvider, // Before removing a provider
|
|
2345
|
+
BeforeRemoveExport, // Before removing an export
|
|
2346
|
+
OnExportAccess, // When importer accesses exports
|
|
2347
|
+
}
|
|
2348
|
+
```
|
|
2349
|
+
|
|
2350
|
+
**Middleware Return Values:**
|
|
2351
|
+
|
|
2352
|
+
- `false` - Abort the operation (block it)
|
|
2353
|
+
- `true` - Pass through unchanged
|
|
2354
|
+
- Modified value - Transform and continue
|
|
2355
|
+
- For `BeforeGet`: Can return any value (transformation)
|
|
623
2356
|
|
|
624
2357
|
> [!CAUTION]
|
|
2358
|
+
> Middleware best practices:
|
|
625
2359
|
>
|
|
626
2360
|
> - Returning `false` aborts the chain (no value returned)
|
|
627
|
-
> - Returning `true` passes value unchanged
|
|
628
2361
|
> - Middlewares execute in registration order
|
|
629
2362
|
> - Always handle errors in middleware chains
|
|
2363
|
+
> - Use `inject()` parameter in BeforeGet to avoid infinite loops
|
|
2364
|
+
> - Be careful with performance - middlewares run on every operation
|
|
2365
|
+
> - Events fire **after** middlewares complete
|
|
630
2366
|
|
|
631
2367
|
## Testing
|
|
632
2368
|
|
|
633
|
-
|
|
2369
|
+
xInjection makes testing easy through blueprint cloning and provider substitution.
|
|
2370
|
+
|
|
2371
|
+
### Blueprint Cloning
|
|
2372
|
+
|
|
2373
|
+
Clone blueprints to create test-specific configurations without affecting production code.
|
|
2374
|
+
|
|
2375
|
+
```ts
|
|
2376
|
+
// Production blueprint
|
|
2377
|
+
const DatabaseModuleBp = ProviderModule.blueprint({
|
|
2378
|
+
id: 'DatabaseModule',
|
|
2379
|
+
providers: [DatabaseService, ConnectionPool],
|
|
2380
|
+
exports: [DatabaseService],
|
|
2381
|
+
});
|
|
2382
|
+
|
|
2383
|
+
// Test blueprint - clone and modify
|
|
2384
|
+
const DatabaseModuleMock = DatabaseModuleBp.clone().updateDefinition({
|
|
2385
|
+
id: 'DatabaseModuleMock',
|
|
2386
|
+
providers: [
|
|
2387
|
+
{ provide: DatabaseService, useClass: MockDatabaseService },
|
|
2388
|
+
{ provide: ConnectionPool, useClass: MockConnectionPool },
|
|
2389
|
+
],
|
|
2390
|
+
});
|
|
2391
|
+
|
|
2392
|
+
// Use in tests
|
|
2393
|
+
const TestModule = ProviderModule.create({
|
|
2394
|
+
id: 'TestModule',
|
|
2395
|
+
imports: [DatabaseModuleMock],
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
const db = TestModule.get(DatabaseService); // MockDatabaseService
|
|
2399
|
+
```
|
|
2400
|
+
|
|
2401
|
+
**Deep Blueprint Cloning:**
|
|
2402
|
+
|
|
2403
|
+
```ts
|
|
2404
|
+
const OriginalBp = ProviderModule.blueprint({
|
|
2405
|
+
id: 'Original',
|
|
2406
|
+
providers: [ServiceA, ServiceB, ServiceC],
|
|
2407
|
+
exports: [ServiceA, ServiceB],
|
|
2408
|
+
onReady: (module) => console.log('Original ready'),
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
// Clone and completely override
|
|
2412
|
+
const ClonedBp = OriginalBp.clone().updateDefinition({
|
|
2413
|
+
id: 'Cloned',
|
|
2414
|
+
providers: [MockServiceA, MockServiceB], // Different providers
|
|
2415
|
+
exports: [MockServiceA], // Different exports
|
|
2416
|
+
onReady: undefined, // Remove lifecycle hooks
|
|
2417
|
+
});
|
|
2418
|
+
|
|
2419
|
+
// Original blueprint unchanged
|
|
2420
|
+
console.log(OriginalBp.providers?.length); // 3
|
|
2421
|
+
console.log(ClonedBp.providers?.length); // 2
|
|
2422
|
+
```
|
|
2423
|
+
|
|
2424
|
+
### Provider Substitution
|
|
2425
|
+
|
|
2426
|
+
Replace real services with mocks for testing.
|
|
634
2427
|
|
|
635
2428
|
```ts
|
|
2429
|
+
// Production services
|
|
2430
|
+
@Injectable()
|
|
2431
|
+
class ApiService {
|
|
2432
|
+
async fetchData() {
|
|
2433
|
+
return fetch('https://api.example.com/data').then((r) => r.json());
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
@Injectable()
|
|
2438
|
+
class UserService {
|
|
2439
|
+
constructor(private api: ApiService) {}
|
|
2440
|
+
|
|
2441
|
+
async getUsers() {
|
|
2442
|
+
return this.api.fetchData();
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// Mock service
|
|
2447
|
+
class MockApiService {
|
|
2448
|
+
async fetchData() {
|
|
2449
|
+
return { users: [{ id: 1, name: 'Mock User' }] };
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
636
2453
|
// Production module
|
|
637
|
-
const
|
|
638
|
-
id: '
|
|
639
|
-
providers: [
|
|
640
|
-
|
|
2454
|
+
const ProductionModule = ProviderModule.create({
|
|
2455
|
+
id: 'ProductionModule',
|
|
2456
|
+
providers: [ApiService, UserService],
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
// Test module with substitution
|
|
2460
|
+
const TestModule = ProviderModule.create({
|
|
2461
|
+
id: 'TestModule',
|
|
2462
|
+
providers: [
|
|
2463
|
+
{ provide: ApiService, useClass: MockApiService },
|
|
2464
|
+
UserService, // Uses MockApiService automatically
|
|
2465
|
+
],
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
const userService = TestModule.get(UserService);
|
|
2469
|
+
const users = await userService.getUsers();
|
|
2470
|
+
console.log(users); // Mock data
|
|
2471
|
+
```
|
|
2472
|
+
|
|
2473
|
+
### Mocking Services
|
|
2474
|
+
|
|
2475
|
+
**Using useValue for simple mocks:**
|
|
2476
|
+
|
|
2477
|
+
```ts
|
|
2478
|
+
const mockPaymentGateway = {
|
|
2479
|
+
charge: jest.fn().mockResolvedValue({ success: true }),
|
|
2480
|
+
refund: jest.fn().mockResolvedValue({ success: true }),
|
|
2481
|
+
};
|
|
2482
|
+
|
|
2483
|
+
const TestModule = ProviderModule.create({
|
|
2484
|
+
id: 'TestModule',
|
|
2485
|
+
providers: [{ provide: PaymentGateway, useValue: mockPaymentGateway }, PaymentService],
|
|
641
2486
|
});
|
|
642
2487
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
2488
|
+
const paymentService = TestModule.get(PaymentService);
|
|
2489
|
+
await paymentService.processPayment(100);
|
|
2490
|
+
|
|
2491
|
+
expect(mockPaymentGateway.charge).toHaveBeenCalledWith(100);
|
|
2492
|
+
```
|
|
2493
|
+
|
|
2494
|
+
**Using useFactory for complex mocks:**
|
|
2495
|
+
|
|
2496
|
+
```ts
|
|
2497
|
+
const TestModule = ProviderModule.create({
|
|
2498
|
+
id: 'TestModule',
|
|
646
2499
|
providers: [
|
|
647
|
-
{ provide: UserService, useClass: MockUserService },
|
|
648
2500
|
{
|
|
649
|
-
provide:
|
|
650
|
-
|
|
651
|
-
|
|
2501
|
+
provide: 'DATABASE_CONNECTION',
|
|
2502
|
+
useFactory: () => {
|
|
2503
|
+
return {
|
|
2504
|
+
query: jest.fn().mockResolvedValue([{ id: 1, name: 'Test' }]),
|
|
2505
|
+
connect: jest.fn().mockResolvedValue(true),
|
|
2506
|
+
disconnect: jest.fn().mockResolvedValue(true),
|
|
2507
|
+
};
|
|
652
2508
|
},
|
|
653
2509
|
},
|
|
654
2510
|
],
|
|
655
2511
|
});
|
|
656
2512
|
|
|
657
|
-
|
|
658
|
-
const
|
|
659
|
-
|
|
2513
|
+
const db = TestModule.get('DATABASE_CONNECTION');
|
|
2514
|
+
const results = await db.query('SELECT * FROM users');
|
|
2515
|
+
expect(results).toEqual([{ id: 1, name: 'Test' }]);
|
|
2516
|
+
```
|
|
2517
|
+
|
|
2518
|
+
**Complete Testing Example:**
|
|
2519
|
+
|
|
2520
|
+
```ts
|
|
2521
|
+
// Production code
|
|
2522
|
+
@Injectable()
|
|
2523
|
+
class EmailService {
|
|
2524
|
+
async sendEmail(to: string, subject: string, body: string) {
|
|
2525
|
+
// Real email sending logic
|
|
2526
|
+
console.log(`Sending email to ${to}`);
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
@Injectable()
|
|
2531
|
+
class UserNotificationService {
|
|
2532
|
+
constructor(private emailService: EmailService) {}
|
|
2533
|
+
|
|
2534
|
+
async notifyUser(userId: string, message: string) {
|
|
2535
|
+
await this.emailService.sendEmail(`user${userId}@example.com`, 'Notification', message);
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// Test code
|
|
2540
|
+
describe('UserNotificationService', () => {
|
|
2541
|
+
it('should send email notification', async () => {
|
|
2542
|
+
const mockEmailService = {
|
|
2543
|
+
sendEmail: jest.fn().mockResolvedValue(undefined),
|
|
2544
|
+
};
|
|
2545
|
+
|
|
2546
|
+
const TestModule = ProviderModule.create({
|
|
2547
|
+
id: 'TestModule',
|
|
2548
|
+
providers: [{ provide: EmailService, useValue: mockEmailService }, UserNotificationService],
|
|
2549
|
+
});
|
|
2550
|
+
|
|
2551
|
+
const notificationService = TestModule.get(UserNotificationService);
|
|
2552
|
+
await notificationService.notifyUser('123', 'Test message');
|
|
2553
|
+
|
|
2554
|
+
expect(mockEmailService.sendEmail).toHaveBeenCalledWith('user123@example.com', 'Notification', 'Test message');
|
|
2555
|
+
});
|
|
2556
|
+
});
|
|
2557
|
+
```
|
|
2558
|
+
|
|
2559
|
+
**Testing with Multiple Module Layers:**
|
|
2560
|
+
|
|
2561
|
+
```ts
|
|
2562
|
+
// Create mock blueprint
|
|
2563
|
+
const MockDataModuleBp = ProviderModule.blueprint({
|
|
2564
|
+
id: 'MockDataModule',
|
|
2565
|
+
providers: [
|
|
2566
|
+
{ provide: DatabaseService, useClass: MockDatabaseService },
|
|
2567
|
+
{ provide: CacheService, useClass: MockCacheService },
|
|
2568
|
+
],
|
|
2569
|
+
exports: [DatabaseService, CacheService],
|
|
2570
|
+
});
|
|
2571
|
+
|
|
2572
|
+
// Use mock in feature module tests
|
|
2573
|
+
const FeatureModuleTest = ProviderModule.create({
|
|
2574
|
+
id: 'FeatureModuleTest',
|
|
2575
|
+
imports: [MockDataModuleBp],
|
|
2576
|
+
providers: [FeatureService],
|
|
2577
|
+
});
|
|
2578
|
+
|
|
2579
|
+
const featureService = FeatureModuleTest.get(FeatureService);
|
|
2580
|
+
// FeatureService receives mock dependencies
|
|
2581
|
+
```
|
|
2582
|
+
|
|
2583
|
+
> [!TIP]
|
|
2584
|
+
> Testing strategies:
|
|
2585
|
+
>
|
|
2586
|
+
> - Use `blueprint.clone()` to create test variations without modifying originals
|
|
2587
|
+
> - Use `useValue` for simple mocks with jest.fn()
|
|
2588
|
+
> - Use `useClass` for class-based mocks with behavior
|
|
2589
|
+
> - Use `useFactory` for complex mock setup
|
|
2590
|
+
> - Test module isolation by mocking all external dependencies
|
|
2591
|
+
> - Verify mock calls with jest expectations
|
|
2592
|
+
|
|
2593
|
+
## Advanced Module API
|
|
2594
|
+
|
|
2595
|
+
### Query Methods
|
|
2596
|
+
|
|
2597
|
+
Check module state and relationships.
|
|
2598
|
+
|
|
2599
|
+
```ts
|
|
2600
|
+
const MyModule = ProviderModule.create({
|
|
2601
|
+
id: 'MyModule',
|
|
2602
|
+
imports: [DatabaseModule, ConfigModule],
|
|
2603
|
+
providers: [ServiceA, ServiceB],
|
|
2604
|
+
exports: [ServiceA, DatabaseModule],
|
|
2605
|
+
});
|
|
2606
|
+
|
|
2607
|
+
// Provider queries
|
|
2608
|
+
MyModule.hasProvider(ServiceA); // true
|
|
2609
|
+
MyModule.hasProvider(ServiceC); // false
|
|
2610
|
+
MyModule.hasProvider(DatabaseService); // true (from import)
|
|
2611
|
+
|
|
2612
|
+
// Import queries
|
|
2613
|
+
MyModule.isImportingModule('DatabaseModule'); // true
|
|
2614
|
+
MyModule.isImportingModule(ConfigModule); // true (by reference)
|
|
2615
|
+
MyModule.isImportingModule('NonExistent'); // false
|
|
2616
|
+
|
|
2617
|
+
// Export queries
|
|
2618
|
+
MyModule.isExportingProvider(ServiceA); // true
|
|
2619
|
+
MyModule.isExportingProvider(ServiceB); // false
|
|
2620
|
+
MyModule.isExportingModule('DatabaseModule'); // true
|
|
2621
|
+
MyModule.isExportingModule(ConfigModule); // false
|
|
2622
|
+
|
|
2623
|
+
// State queries
|
|
2624
|
+
MyModule.isDisposed; // false
|
|
2625
|
+
MyModule.id; // 'MyModule'
|
|
2626
|
+
```
|
|
2627
|
+
|
|
2628
|
+
**Using Symbol Identifiers:**
|
|
2629
|
+
|
|
2630
|
+
```ts
|
|
2631
|
+
const MODULE_ID = Symbol('FeatureModule');
|
|
2632
|
+
|
|
2633
|
+
const FeatureModule = ProviderModule.create({
|
|
2634
|
+
id: MODULE_ID,
|
|
2635
|
+
providers: [FeatureService],
|
|
2636
|
+
exports: [FeatureService],
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
const AppModule = ProviderModule.create({
|
|
2640
|
+
id: 'AppModule',
|
|
2641
|
+
imports: [FeatureModule],
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2644
|
+
// Query using Symbol
|
|
2645
|
+
console.log(AppModule.isImportingModule(MODULE_ID)); // true
|
|
2646
|
+
```
|
|
2647
|
+
|
|
2648
|
+
### Multiple Provider Binding
|
|
2649
|
+
|
|
2650
|
+
Bind multiple providers to the same token and retrieve them as a list.
|
|
2651
|
+
|
|
2652
|
+
```ts
|
|
2653
|
+
@Injectable()
|
|
2654
|
+
abstract class Plugin {
|
|
2655
|
+
abstract execute(): void;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
@Injectable()
|
|
2659
|
+
class PluginA extends Plugin {
|
|
2660
|
+
execute() {
|
|
2661
|
+
console.log('Plugin A executing');
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
@Injectable()
|
|
2666
|
+
class PluginB extends Plugin {
|
|
2667
|
+
execute() {
|
|
2668
|
+
console.log('Plugin B executing');
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
@Injectable()
|
|
2673
|
+
class PluginC extends Plugin {
|
|
2674
|
+
execute() {
|
|
2675
|
+
console.log('Plugin C executing');
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
const PluginModule = ProviderModule.create({
|
|
2680
|
+
id: 'PluginModule',
|
|
2681
|
+
providers: [
|
|
2682
|
+
{ provide: Plugin, useClass: PluginA },
|
|
2683
|
+
{ provide: Plugin, useClass: PluginB },
|
|
2684
|
+
{ provide: Plugin, useClass: PluginC },
|
|
2685
|
+
],
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
// Get all plugins as array (third parameter = asList)
|
|
2689
|
+
const plugins = PluginModule.get(Plugin, false, true);
|
|
2690
|
+
console.log(plugins.length); // 3
|
|
2691
|
+
|
|
2692
|
+
// Execute all plugins
|
|
2693
|
+
plugins.forEach((plugin) => plugin.execute());
|
|
2694
|
+
// Output:
|
|
2695
|
+
// Plugin A executing
|
|
2696
|
+
// Plugin B executing
|
|
2697
|
+
// Plugin C executing
|
|
2698
|
+
```
|
|
2699
|
+
|
|
2700
|
+
**String Token Example:**
|
|
2701
|
+
|
|
2702
|
+
```ts
|
|
2703
|
+
const MyModule = ProviderModule.create({
|
|
2704
|
+
id: 'MyModule',
|
|
2705
|
+
providers: [
|
|
2706
|
+
{ provide: 'Handler', useValue: 'Handler1' },
|
|
2707
|
+
{ provide: 'Handler', useValue: 'Handler2' },
|
|
2708
|
+
{ provide: 'Handler', useValue: 'Handler3' },
|
|
2709
|
+
],
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
const handlers = MyModule.get('Handler', false, true);
|
|
2713
|
+
console.log(handlers); // ['Handler1', 'Handler2', 'Handler3']
|
|
2714
|
+
```
|
|
2715
|
+
|
|
2716
|
+
### Batch Resolution with getMany()
|
|
2717
|
+
|
|
2718
|
+
Resolve multiple providers in a single call.
|
|
2719
|
+
|
|
2720
|
+
```ts
|
|
2721
|
+
@Injectable()
|
|
2722
|
+
class ServiceA {
|
|
2723
|
+
name = 'A';
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
@Injectable()
|
|
2727
|
+
class ServiceB {
|
|
2728
|
+
name = 'B';
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
@Injectable()
|
|
2732
|
+
class ServiceC {
|
|
2733
|
+
name = 'C';
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
const MyModule = ProviderModule.create({
|
|
2737
|
+
id: 'MyModule',
|
|
2738
|
+
providers: [
|
|
2739
|
+
ServiceA,
|
|
2740
|
+
ServiceB,
|
|
2741
|
+
ServiceC,
|
|
2742
|
+
{ provide: 'CONFIG_A', useValue: 'config-a' },
|
|
2743
|
+
{ provide: 'CONFIG_B', useValue: 'config-b' },
|
|
2744
|
+
],
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
// Simple getMany
|
|
2748
|
+
const [serviceA, serviceB, configA] = MyModule.getMany(ServiceA, ServiceB, 'CONFIG_A');
|
|
2749
|
+
|
|
2750
|
+
console.log(serviceA.name); // 'A'
|
|
2751
|
+
console.log(serviceB.name); // 'B'
|
|
2752
|
+
console.log(configA); // 'config-a'
|
|
2753
|
+
```
|
|
2754
|
+
|
|
2755
|
+
**With Options:**
|
|
2756
|
+
|
|
2757
|
+
```ts
|
|
2758
|
+
// Optional providers
|
|
2759
|
+
const [serviceA, missing, serviceC] = MyModule.getMany(
|
|
2760
|
+
ServiceA,
|
|
2761
|
+
{ provider: 'NON_EXISTENT', isOptional: true },
|
|
2762
|
+
ServiceC
|
|
2763
|
+
);
|
|
2764
|
+
|
|
2765
|
+
console.log(serviceA); // ServiceA instance
|
|
2766
|
+
console.log(missing); // undefined (no error)
|
|
2767
|
+
console.log(serviceC); // ServiceC instance
|
|
2768
|
+
|
|
2769
|
+
// Get as list (multiple bindings)
|
|
2770
|
+
const HandlerModule = ProviderModule.create({
|
|
2771
|
+
id: 'HandlerModule',
|
|
2772
|
+
providers: [
|
|
2773
|
+
{ provide: 'Handler', useValue: 'H1' },
|
|
2774
|
+
{ provide: 'Handler', useValue: 'H2' },
|
|
2775
|
+
{ provide: 'Handler', useValue: 'H3' },
|
|
2776
|
+
],
|
|
2777
|
+
});
|
|
2778
|
+
|
|
2779
|
+
const [handlers] = HandlerModule.getMany({
|
|
2780
|
+
provider: 'Handler',
|
|
2781
|
+
asList: true,
|
|
660
2782
|
});
|
|
2783
|
+
|
|
2784
|
+
console.log(handlers); // ['H1', 'H2', 'H3']
|
|
2785
|
+
```
|
|
2786
|
+
|
|
2787
|
+
**Complex Example:**
|
|
2788
|
+
|
|
2789
|
+
```ts
|
|
2790
|
+
const [database, cache, optionalLogger, allPlugins, config] = MyModule.getMany(
|
|
2791
|
+
DatabaseService,
|
|
2792
|
+
CacheService,
|
|
2793
|
+
{ provider: LoggerService, isOptional: true },
|
|
2794
|
+
{ provider: Plugin, asList: true },
|
|
2795
|
+
'APP_CONFIG'
|
|
2796
|
+
);
|
|
2797
|
+
|
|
2798
|
+
// All providers resolved in one call
|
|
2799
|
+
// optionalLogger is undefined if not available
|
|
2800
|
+
// allPlugins is an array of all Plugin bindings
|
|
661
2801
|
```
|
|
662
2802
|
|
|
2803
|
+
> [!IMPORTANT] > `getMany()` parameter types:
|
|
2804
|
+
>
|
|
2805
|
+
> - **Simple**: Just pass the token directly
|
|
2806
|
+
> - **With options**: Use object with `provider`, `isOptional`, and/or `asList`
|
|
2807
|
+
|
|
663
2808
|
## Resources
|
|
664
2809
|
|
|
665
2810
|
📚 **[Full API Documentation](https://adimarianmutu.github.io/x-injection/index.html)** - Complete TypeDoc reference
|
|
@@ -668,14 +2813,23 @@ const testModule = ProviderModule.create({
|
|
|
668
2813
|
|
|
669
2814
|
💡 **[GitHub Issues](https://github.com/AdiMarianMutu/x-injection/issues)** - Bug reports and feature requests
|
|
670
2815
|
|
|
2816
|
+
🌟 **[GitHub Repository](https://github.com/AdiMarianMutu/x-injection)** - Source code and examples
|
|
2817
|
+
|
|
671
2818
|
## Contributing
|
|
672
2819
|
|
|
673
|
-
Contributions welcome! Please ensure code follows the project style guidelines.
|
|
2820
|
+
Contributions are welcome! Please ensure code follows the project style guidelines and includes appropriate tests.
|
|
2821
|
+
|
|
2822
|
+
1. Fork the repository
|
|
2823
|
+
2. Create a feature branch
|
|
2824
|
+
3. Make your changes with tests
|
|
2825
|
+
4. Submit a pull request
|
|
674
2826
|
|
|
675
2827
|
## Credits
|
|
676
2828
|
|
|
677
2829
|
**Author:** [Adi-Marian Mutu](https://www.linkedin.com/in/mutu-adi-marian/)
|
|
2830
|
+
|
|
678
2831
|
**Built on:** [InversifyJS](https://github.com/inversify/monorepo)
|
|
2832
|
+
|
|
679
2833
|
**Logo:** [Alexandru Turica](https://www.linkedin.com/in/alexandru-turica-82215522b/)
|
|
680
2834
|
|
|
681
2835
|
## License
|