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