ts-ioc-container 46.6.0 → 46.6.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 +1254 -639
- package/cjm/container/Container.js +4 -3
- package/cjm/hooks/HooksRunner.js +2 -2
- package/cjm/hooks/hook.js +2 -2
- package/cjm/index.js +18 -11
- package/cjm/injector/IInjector.js +2 -2
- package/cjm/injector/inject.js +6 -5
- package/cjm/metadata/class.js +12 -0
- package/cjm/metadata/method.js +10 -0
- package/cjm/metadata/parameter.js +13 -0
- package/cjm/provider/IProvider.js +2 -2
- package/cjm/provider/Provider.js +2 -2
- package/cjm/registration/IRegistration.js +3 -3
- package/cjm/registration/Registration.js +5 -4
- package/cjm/token/BindToken.js +2 -2
- package/cjm/token/GroupAliasToken.js +1 -1
- package/cjm/token/toToken.js +2 -2
- package/cjm/utils/array.js +20 -0
- package/cjm/utils/basic.js +9 -0
- package/cjm/utils/fp.js +9 -0
- package/cjm/utils/promise.js +5 -0
- package/cjm/utils/proxy.js +20 -0
- package/esm/container/Container.js +2 -1
- package/esm/hooks/HooksRunner.js +1 -1
- package/esm/hooks/hook.js +1 -1
- package/esm/index.js +8 -2
- package/esm/injector/IInjector.js +1 -1
- package/esm/injector/inject.js +3 -2
- package/esm/metadata/class.js +7 -0
- package/esm/metadata/method.js +5 -0
- package/esm/metadata/parameter.js +8 -0
- package/esm/provider/IProvider.js +1 -1
- package/esm/provider/Provider.js +1 -1
- package/esm/registration/IRegistration.js +1 -1
- package/esm/registration/Registration.js +2 -1
- package/esm/token/BindToken.js +1 -1
- package/esm/token/GroupAliasToken.js +1 -1
- package/esm/token/toToken.js +1 -1
- package/esm/utils/array.js +16 -0
- package/esm/utils/basic.js +6 -0
- package/esm/utils/fp.js +6 -0
- package/esm/utils/promise.js +1 -0
- package/esm/utils/proxy.js +16 -0
- package/package.json +1 -1
- package/typings/container/Container.d.ts +1 -1
- package/typings/container/EmptyContainer.d.ts +1 -1
- package/typings/container/IContainer.d.ts +1 -1
- package/typings/hooks/hook.d.ts +1 -1
- package/typings/hooks/injectProp.d.ts +1 -1
- package/typings/hooks/onConstruct.d.ts +1 -1
- package/typings/hooks/onDispose.d.ts +1 -1
- package/typings/index.d.ts +9 -3
- package/typings/injector/IInjector.d.ts +1 -1
- package/typings/injector/MetadataInjector.d.ts +1 -1
- package/typings/injector/ProxyInjector.d.ts +1 -1
- package/typings/injector/SimpleInjector.d.ts +1 -1
- package/typings/injector/inject.d.ts +1 -1
- package/typings/metadata/class.d.ts +2 -0
- package/typings/metadata/method.d.ts +2 -0
- package/typings/metadata/parameter.d.ts +3 -0
- package/typings/provider/IProvider.d.ts +1 -1
- package/typings/provider/Provider.d.ts +2 -1
- package/typings/registration/IRegistration.d.ts +2 -1
- package/typings/registration/Registration.d.ts +2 -1
- package/typings/select.d.ts +1 -1
- package/typings/token/ClassToken.d.ts +3 -3
- package/typings/token/GroupAliasToken.d.ts +2 -2
- package/typings/token/GroupInstanceToken.d.ts +1 -1
- package/typings/token/SingleAliasToken.d.ts +3 -3
- package/typings/token/toToken.d.ts +1 -1
- package/typings/utils/array.d.ts +4 -0
- package/typings/utils/basic.d.ts +13 -0
- package/typings/utils/fp.d.ts +13 -0
- package/typings/utils/promise.d.ts +1 -0
- package/typings/utils/proxy.d.ts +2 -0
- package/cjm/metadata.js +0 -29
- package/cjm/types.js +0 -2
- package/cjm/utils.js +0 -48
- package/esm/metadata.js +0 -20
- package/esm/types.js +0 -1
- package/esm/utils.js +0 -40
- package/typings/metadata.d.ts +0 -7
- package/typings/types.d.ts +0 -8
- package/typings/utils.d.ts +0 -15
package/README.md
CHANGED
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
## Content
|
|
23
23
|
|
|
24
24
|
- [Setup](#setup)
|
|
25
|
+
- [Quickstart](#quickstart)
|
|
26
|
+
- [Cheatsheet](#cheatsheet)
|
|
27
|
+
- [Recipes](#recipes)
|
|
25
28
|
- [Container](#container)
|
|
26
29
|
- [Basic usage](#basic-usage)
|
|
27
30
|
- [Scope](#scope) `tags`
|
|
@@ -74,6 +77,86 @@ And `tsconfig.json` should have next options:
|
|
|
74
77
|
}
|
|
75
78
|
```
|
|
76
79
|
|
|
80
|
+
## Quickstart
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import 'reflect-metadata';
|
|
84
|
+
import { Container, register, bindTo, singleton } from 'ts-ioc-container';
|
|
85
|
+
|
|
86
|
+
@register(bindTo('ILogger'), singleton())
|
|
87
|
+
class Logger {
|
|
88
|
+
log(message: string) {
|
|
89
|
+
console.log(message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class App {
|
|
94
|
+
constructor(private logger = container.resolve<Logger>('ILogger')) {}
|
|
95
|
+
start() {
|
|
96
|
+
this.logger.log('hello');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const container = new Container({ tags: ['application'] }).addRegistration(Logger);
|
|
101
|
+
container.resolve(App).start();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Cheatsheet
|
|
105
|
+
|
|
106
|
+
- Register class with key: `@register(bindTo('Key')) class Service {}`
|
|
107
|
+
- Register value: `R.fromValue(config).bindToKey('Config')`
|
|
108
|
+
- Singleton: `@register(singleton())`
|
|
109
|
+
- Scoped registration: `@register(scope((s) => s.hasTag('request')))`
|
|
110
|
+
- Resolve by alias: `container.resolveByAlias('Alias')`
|
|
111
|
+
- Current scope token: `select.scope.current`
|
|
112
|
+
- Lazy token: `select.token('Service').lazy()`
|
|
113
|
+
- Inject decorator: `@inject('Key')`
|
|
114
|
+
- Property inject: `injectProp(target, 'propName', select.token('Key'))`
|
|
115
|
+
|
|
116
|
+
## Recipes
|
|
117
|
+
|
|
118
|
+
### Express/Next handler (per-request scope)
|
|
119
|
+
```typescript
|
|
120
|
+
const app = new Container({ tags: ['application'] })
|
|
121
|
+
.addRegistration(R.fromClass(Logger).pipe(singleton()));
|
|
122
|
+
|
|
123
|
+
function handleRequest() {
|
|
124
|
+
const requestScope = app.createScope({ tags: ['request'] });
|
|
125
|
+
const logger = requestScope.resolve<Logger>('Logger');
|
|
126
|
+
logger.log('req started');
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Background worker (singleton client, transient jobs)
|
|
131
|
+
```typescript
|
|
132
|
+
@register(singleton())
|
|
133
|
+
class QueueClient {}
|
|
134
|
+
|
|
135
|
+
class JobHandler {
|
|
136
|
+
constructor(@inject('QueueClient') private queue: QueueClient) {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const worker = new Container({ tags: ['worker'] })
|
|
140
|
+
.addRegistration(R.fromClass(QueueClient))
|
|
141
|
+
.addRegistration(R.fromClass(JobHandler));
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Frontend widget/page scope with lazy dependency
|
|
145
|
+
```typescript
|
|
146
|
+
@register(bindTo('FeatureFlags'), singleton())
|
|
147
|
+
class FeatureFlags {
|
|
148
|
+
load() { /* fetch flags */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
class Widget {
|
|
152
|
+
constructor(@inject(select.token('FeatureFlags').lazy()) private flags: FeatureFlags) {}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const page = new Container({ tags: ['page'] })
|
|
156
|
+
.addRegistration(R.fromClass(FeatureFlags))
|
|
157
|
+
.addRegistration(R.fromClass(Widget));
|
|
158
|
+
```
|
|
159
|
+
|
|
77
160
|
## Container
|
|
78
161
|
`IContainer` consists of:
|
|
79
162
|
|
|
@@ -84,33 +167,77 @@ And `tsconfig.json` should have next options:
|
|
|
84
167
|
### Basic usage
|
|
85
168
|
|
|
86
169
|
```typescript
|
|
170
|
+
import 'reflect-metadata';
|
|
87
171
|
import { Container, type IContainer, inject, Registration as R, select } from 'ts-ioc-container';
|
|
88
172
|
|
|
173
|
+
/**
|
|
174
|
+
* User Management Domain - Basic Dependency Injection
|
|
175
|
+
*
|
|
176
|
+
* This example demonstrates how to wire up a simple authentication service
|
|
177
|
+
* that depends on a user repository. This pattern is common in web applications
|
|
178
|
+
* where services need database access.
|
|
179
|
+
*/
|
|
89
180
|
describe('Basic usage', function () {
|
|
90
|
-
|
|
91
|
-
|
|
181
|
+
// Domain types
|
|
182
|
+
interface User {
|
|
183
|
+
id: string;
|
|
184
|
+
email: string;
|
|
185
|
+
passwordHash: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Repository interface - abstracts database access
|
|
189
|
+
interface IUserRepository {
|
|
190
|
+
findByEmail(email: string): User | undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Concrete implementation
|
|
194
|
+
class UserRepository implements IUserRepository {
|
|
195
|
+
private users: User[] = [{ id: '1', email: 'admin@example.com', passwordHash: 'hashed_password' }];
|
|
196
|
+
|
|
197
|
+
findByEmail(email: string): User | undefined {
|
|
198
|
+
return this.users.find((u) => u.email === email);
|
|
199
|
+
}
|
|
92
200
|
}
|
|
93
201
|
|
|
94
202
|
it('should inject dependencies', function () {
|
|
95
|
-
|
|
96
|
-
|
|
203
|
+
// AuthService depends on IUserRepository
|
|
204
|
+
class AuthService {
|
|
205
|
+
constructor(@inject('IUserRepository') private userRepo: IUserRepository) {}
|
|
206
|
+
|
|
207
|
+
authenticate(email: string): boolean {
|
|
208
|
+
const user = this.userRepo.findByEmail(email);
|
|
209
|
+
return user !== undefined;
|
|
210
|
+
}
|
|
97
211
|
}
|
|
98
212
|
|
|
99
|
-
|
|
213
|
+
// Wire up the container
|
|
214
|
+
const container = new Container().addRegistration(R.fromClass(UserRepository).bindTo('IUserRepository'));
|
|
215
|
+
|
|
216
|
+
// Resolve AuthService - UserRepository is automatically injected
|
|
217
|
+
const authService = container.resolve(AuthService);
|
|
100
218
|
|
|
101
|
-
expect(
|
|
219
|
+
expect(authService.authenticate('admin@example.com')).toBe(true);
|
|
220
|
+
expect(authService.authenticate('unknown@example.com')).toBe(false);
|
|
102
221
|
});
|
|
103
222
|
|
|
104
|
-
it('should inject current scope', function () {
|
|
105
|
-
|
|
223
|
+
it('should inject current scope for request context', function () {
|
|
224
|
+
// In Express.js, each request gets its own scope
|
|
225
|
+
// Services can access the current scope to resolve request-specific dependencies
|
|
226
|
+
const appContainer = new Container({ tags: ['application'] });
|
|
106
227
|
|
|
107
|
-
class
|
|
108
|
-
constructor(@inject(select.scope.current) public
|
|
228
|
+
class RequestHandler {
|
|
229
|
+
constructor(@inject(select.scope.current) public requestScope: IContainer) {}
|
|
230
|
+
|
|
231
|
+
handleRequest(): string {
|
|
232
|
+
// Access request-scoped dependencies
|
|
233
|
+
return this.requestScope.hasTag('application') ? 'app-scope' : 'request-scope';
|
|
234
|
+
}
|
|
109
235
|
}
|
|
110
236
|
|
|
111
|
-
const
|
|
237
|
+
const handler = appContainer.resolve(RequestHandler);
|
|
112
238
|
|
|
113
|
-
expect(
|
|
239
|
+
expect(handler.requestScope).toBe(appContainer);
|
|
240
|
+
expect(handler.handleRequest()).toBe('app-scope');
|
|
114
241
|
});
|
|
115
242
|
});
|
|
116
243
|
|
|
@@ -124,6 +251,7 @@ Sometimes you need to create a scope of container. For example, when you want to
|
|
|
124
251
|
- NOTICE: when you create a scope then we clone ONLY tags-matched providers.
|
|
125
252
|
|
|
126
253
|
```typescript
|
|
254
|
+
import 'reflect-metadata';
|
|
127
255
|
import {
|
|
128
256
|
bindTo,
|
|
129
257
|
Container,
|
|
@@ -137,29 +265,78 @@ import {
|
|
|
137
265
|
singleton,
|
|
138
266
|
} from 'ts-ioc-container';
|
|
139
267
|
|
|
140
|
-
|
|
141
|
-
|
|
268
|
+
/**
|
|
269
|
+
* User Management Domain - Request Scopes
|
|
270
|
+
*
|
|
271
|
+
* In web applications, each HTTP request typically gets its own scope.
|
|
272
|
+
* This allows request-specific data (current user, request ID, etc.)
|
|
273
|
+
* to be isolated between concurrent requests.
|
|
274
|
+
*
|
|
275
|
+
* Scope hierarchy:
|
|
276
|
+
* Application (singleton services)
|
|
277
|
+
* └── Request (per-request services)
|
|
278
|
+
* └── Transaction (database transaction boundary)
|
|
279
|
+
*/
|
|
280
|
+
|
|
281
|
+
// SessionService is only available in request scope - not at application level
|
|
282
|
+
// This prevents accidental access to request-specific data from singletons
|
|
283
|
+
@register(bindTo('ISessionService'), scope((s) => s.hasTag('request')), singleton())
|
|
284
|
+
class SessionService {
|
|
285
|
+
private userId: string | null = null;
|
|
286
|
+
|
|
287
|
+
setCurrentUser(userId: string) {
|
|
288
|
+
this.userId = userId;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
getCurrentUserId(): string | null {
|
|
292
|
+
return this.userId;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
142
295
|
|
|
143
296
|
describe('Scopes', function () {
|
|
144
|
-
it('should
|
|
145
|
-
|
|
146
|
-
const
|
|
297
|
+
it('should isolate request-scoped services', function () {
|
|
298
|
+
// Application container - lives for entire app lifetime
|
|
299
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(SessionService));
|
|
300
|
+
|
|
301
|
+
// Simulate two concurrent HTTP requests
|
|
302
|
+
const request1Scope = appContainer.createScope({ tags: ['request'] });
|
|
303
|
+
const request2Scope = appContainer.createScope({ tags: ['request'] });
|
|
304
|
+
|
|
305
|
+
// Each request has its own SessionService instance
|
|
306
|
+
const session1 = request1Scope.resolve<SessionService>('ISessionService');
|
|
307
|
+
const session2 = request2Scope.resolve<SessionService>('ISessionService');
|
|
147
308
|
|
|
148
|
-
|
|
149
|
-
|
|
309
|
+
session1.setCurrentUser('user-1');
|
|
310
|
+
session2.setCurrentUser('user-2');
|
|
311
|
+
|
|
312
|
+
// Sessions are isolated - user data doesn't leak between requests
|
|
313
|
+
expect(session1.getCurrentUserId()).toBe('user-1');
|
|
314
|
+
expect(session2.getCurrentUserId()).toBe('user-2');
|
|
315
|
+
expect(session1).not.toBe(session2);
|
|
316
|
+
|
|
317
|
+
// SessionService is NOT available at application level (security!)
|
|
318
|
+
expect(() => appContainer.resolve('ISessionService')).toThrow(DependencyNotFoundError);
|
|
150
319
|
});
|
|
151
320
|
|
|
152
|
-
it('should
|
|
153
|
-
const
|
|
321
|
+
it('should create child scopes for transactions', function () {
|
|
322
|
+
const appContainer = new Container({ tags: ['application'] });
|
|
154
323
|
|
|
155
|
-
|
|
156
|
-
|
|
324
|
+
// RequestHandler can create a transaction scope for database operations
|
|
325
|
+
class RequestHandler {
|
|
326
|
+
constructor(@inject(select.scope.create({ tags: ['transaction'] })) public transactionScope: IContainer) {}
|
|
327
|
+
|
|
328
|
+
executeInTransaction(): boolean {
|
|
329
|
+
// Transaction scope inherits from request scope
|
|
330
|
+
// Database operations can be rolled back together
|
|
331
|
+
return this.transactionScope.hasTag('transaction');
|
|
332
|
+
}
|
|
157
333
|
}
|
|
158
334
|
|
|
159
|
-
const
|
|
335
|
+
const handler = appContainer.resolve(RequestHandler);
|
|
160
336
|
|
|
161
|
-
expect(
|
|
162
|
-
expect(
|
|
337
|
+
expect(handler.transactionScope).not.toBe(appContainer);
|
|
338
|
+
expect(handler.transactionScope.hasTag('transaction')).toBe(true);
|
|
339
|
+
expect(handler.executeInTransaction()).toBe(true);
|
|
163
340
|
});
|
|
164
341
|
});
|
|
165
342
|
|
|
@@ -173,52 +350,69 @@ Sometimes you want to get all instances from container and its scopes. For examp
|
|
|
173
350
|
```typescript
|
|
174
351
|
import { bindTo, Container, inject, register, Registration as R, select } from 'ts-ioc-container';
|
|
175
352
|
|
|
353
|
+
/**
|
|
354
|
+
* User Management Domain - Instance Collection
|
|
355
|
+
*
|
|
356
|
+
* Sometimes you need access to all instances of a certain type:
|
|
357
|
+
* - Collect all active database connections for health checks
|
|
358
|
+
* - Gather all loggers to flush buffers before shutdown
|
|
359
|
+
* - Find all request handlers for metrics collection
|
|
360
|
+
*
|
|
361
|
+
* The `select.instances()` token resolves all created instances,
|
|
362
|
+
* optionally filtered by a predicate function.
|
|
363
|
+
*/
|
|
176
364
|
describe('Instances', function () {
|
|
177
365
|
@register(bindTo('ILogger'))
|
|
178
366
|
class Logger {}
|
|
179
367
|
|
|
180
|
-
it('should
|
|
368
|
+
it('should collect instances across scope hierarchy', () => {
|
|
369
|
+
// App that needs access to all logger instances (e.g., for flushing)
|
|
181
370
|
class App {
|
|
182
371
|
constructor(@inject(select.instances()) public loggers: Logger[]) {}
|
|
183
372
|
}
|
|
184
373
|
|
|
185
|
-
const
|
|
186
|
-
const
|
|
374
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
|
|
375
|
+
const requestScope = appContainer.createScope({ tags: ['request'] });
|
|
187
376
|
|
|
188
|
-
|
|
189
|
-
|
|
377
|
+
// Create loggers in different scopes
|
|
378
|
+
appContainer.resolve('ILogger');
|
|
379
|
+
requestScope.resolve('ILogger');
|
|
190
380
|
|
|
191
|
-
const
|
|
192
|
-
const
|
|
381
|
+
const appLevel = appContainer.resolve(App);
|
|
382
|
+
const requestLevel = requestScope.resolve(App);
|
|
193
383
|
|
|
194
|
-
|
|
195
|
-
expect(
|
|
384
|
+
// Request scope sees only its own instance
|
|
385
|
+
expect(requestLevel.loggers.length).toBe(1);
|
|
386
|
+
// Application scope sees all instances (cascades up from children)
|
|
387
|
+
expect(appLevel.loggers.length).toBe(2);
|
|
196
388
|
});
|
|
197
389
|
|
|
198
|
-
it('should return only current scope instances', () => {
|
|
390
|
+
it('should return only current scope instances when cascade is disabled', () => {
|
|
391
|
+
// Only get instances from current scope, not parent scopes
|
|
199
392
|
class App {
|
|
200
393
|
constructor(@inject(select.instances().cascade(false)) public loggers: Logger[]) {}
|
|
201
394
|
}
|
|
202
395
|
|
|
203
|
-
const
|
|
204
|
-
const
|
|
396
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
|
|
397
|
+
const requestScope = appContainer.createScope({ tags: ['request'] });
|
|
205
398
|
|
|
206
|
-
|
|
207
|
-
|
|
399
|
+
appContainer.resolve('ILogger');
|
|
400
|
+
requestScope.resolve('ILogger');
|
|
208
401
|
|
|
209
|
-
const
|
|
402
|
+
const appLevel = appContainer.resolve(App);
|
|
210
403
|
|
|
211
|
-
|
|
404
|
+
// Only application-level instance, not request-level
|
|
405
|
+
expect(appLevel.loggers.length).toBe(1);
|
|
212
406
|
});
|
|
213
407
|
|
|
214
|
-
it('should
|
|
408
|
+
it('should filter instances by predicate', () => {
|
|
215
409
|
const isLogger = (instance: unknown) => instance instanceof Logger;
|
|
216
410
|
|
|
217
411
|
class App {
|
|
218
412
|
constructor(@inject(select.instances(isLogger)) public loggers: Logger[]) {}
|
|
219
413
|
}
|
|
220
414
|
|
|
221
|
-
const container = new Container().addRegistration(R.fromClass(Logger));
|
|
415
|
+
const container = new Container({ tags: ['application'] }).addRegistration(R.fromClass(Logger));
|
|
222
416
|
|
|
223
417
|
const logger0 = container.resolve('ILogger');
|
|
224
418
|
const logger1 = container.resolve('ILogger');
|
|
@@ -240,18 +434,99 @@ Sometimes you want to dispose container and all its scopes. For example, when yo
|
|
|
240
434
|
- when container is disposed then it unregisters all providers and remove all instances
|
|
241
435
|
|
|
242
436
|
```typescript
|
|
437
|
+
import 'reflect-metadata';
|
|
243
438
|
import { Container, ContainerDisposedError, Registration as R, select } from 'ts-ioc-container';
|
|
244
439
|
|
|
245
|
-
|
|
440
|
+
/**
|
|
441
|
+
* User Management Domain - Resource Cleanup
|
|
442
|
+
*
|
|
443
|
+
* When a scope ends (e.g., HTTP request completes), resources must be cleaned up:
|
|
444
|
+
* - Database connections returned to pool
|
|
445
|
+
* - File handles closed
|
|
446
|
+
* - Temporary files deleted
|
|
447
|
+
* - Cache entries cleared
|
|
448
|
+
*
|
|
449
|
+
* The container.dispose() method:
|
|
450
|
+
* 1. Executes all onDispose hooks
|
|
451
|
+
* 2. Clears all instances and registrations
|
|
452
|
+
* 3. Detaches from parent scope
|
|
453
|
+
* 4. Prevents further resolution
|
|
454
|
+
*/
|
|
455
|
+
|
|
456
|
+
// Simulates a database connection that must be closed
|
|
457
|
+
class DatabaseConnection {
|
|
458
|
+
isClosed = false;
|
|
459
|
+
|
|
460
|
+
query(sql: string): string[] {
|
|
461
|
+
if (this.isClosed) {
|
|
462
|
+
throw new Error('Connection is closed');
|
|
463
|
+
}
|
|
464
|
+
return [`Result for: ${sql}`];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
close(): void {
|
|
468
|
+
this.isClosed = true;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
246
471
|
|
|
247
472
|
describe('Disposing', function () {
|
|
248
|
-
it('should container and
|
|
249
|
-
const
|
|
473
|
+
it('should dispose container and prevent further usage', function () {
|
|
474
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(
|
|
475
|
+
R.fromClass(DatabaseConnection).bindTo('IDatabase'),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
// Create a request scope with a database connection
|
|
479
|
+
const requestScope = appContainer.createScope({ tags: ['request'] });
|
|
480
|
+
const connection = requestScope.resolve<DatabaseConnection>('IDatabase');
|
|
481
|
+
|
|
482
|
+
// Connection works normally
|
|
483
|
+
expect(connection.query('SELECT * FROM users')).toEqual(['Result for: SELECT * FROM users']);
|
|
250
484
|
|
|
251
|
-
|
|
485
|
+
// Request ends - dispose the scope
|
|
486
|
+
requestScope.dispose();
|
|
252
487
|
|
|
253
|
-
|
|
254
|
-
expect(
|
|
488
|
+
// Scope is now unusable
|
|
489
|
+
expect(() => requestScope.resolve('IDatabase')).toThrow(ContainerDisposedError);
|
|
490
|
+
|
|
491
|
+
// All instances are cleared
|
|
492
|
+
expect(select.instances().resolve(requestScope).length).toBe(0);
|
|
493
|
+
|
|
494
|
+
// Application container is still functional
|
|
495
|
+
expect(appContainer.resolve<DatabaseConnection>('IDatabase')).toBeDefined();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should clean up request-scoped resources on request end', function () {
|
|
499
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(
|
|
500
|
+
R.fromClass(DatabaseConnection).bindTo('IDatabase'),
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
// Simulate Express.js request lifecycle
|
|
504
|
+
function handleRequest(): { connection: DatabaseConnection; scope: Container } {
|
|
505
|
+
const requestScope = appContainer.createScope({ tags: ['request'] }) as Container;
|
|
506
|
+
const connection = requestScope.resolve<DatabaseConnection>('IDatabase');
|
|
507
|
+
|
|
508
|
+
// Do some work...
|
|
509
|
+
connection.query('INSERT INTO sessions VALUES (...)');
|
|
510
|
+
|
|
511
|
+
return { connection, scope: requestScope };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Request 1
|
|
515
|
+
const request1 = handleRequest();
|
|
516
|
+
expect(request1.connection.isClosed).toBe(false);
|
|
517
|
+
|
|
518
|
+
// Request 1 ends - in Express, this would be in res.on('finish')
|
|
519
|
+
request1.connection.close();
|
|
520
|
+
request1.scope.dispose();
|
|
521
|
+
|
|
522
|
+
// Request 2 gets a fresh connection
|
|
523
|
+
const request2 = handleRequest();
|
|
524
|
+
expect(request2.connection.isClosed).toBe(false);
|
|
525
|
+
expect(request2.connection).not.toBe(request1.connection);
|
|
526
|
+
|
|
527
|
+
// Cleanup
|
|
528
|
+
request2.connection.close();
|
|
529
|
+
request2.scope.dispose();
|
|
255
530
|
});
|
|
256
531
|
});
|
|
257
532
|
|
|
@@ -261,93 +536,124 @@ describe('Disposing', function () {
|
|
|
261
536
|
Sometimes you want to create dependency only when somebody want to invoke it's method or property. This is what `lazy` is for.
|
|
262
537
|
|
|
263
538
|
```typescript
|
|
539
|
+
import 'reflect-metadata';
|
|
264
540
|
import { Container, inject, register, Registration as R, select as s, singleton } from 'ts-ioc-container';
|
|
265
541
|
|
|
542
|
+
/**
|
|
543
|
+
* User Management Domain - Lazy Loading
|
|
544
|
+
*
|
|
545
|
+
* Some services are expensive to initialize:
|
|
546
|
+
* - EmailNotifier: Establishes SMTP connection
|
|
547
|
+
* - ReportGenerator: Loads templates, initializes PDF engine
|
|
548
|
+
* - ExternalApiClient: Authenticates with third-party service
|
|
549
|
+
*
|
|
550
|
+
* Lazy loading defers instantiation until first use.
|
|
551
|
+
* This improves startup time and avoids initializing unused services.
|
|
552
|
+
*
|
|
553
|
+
* Use cases:
|
|
554
|
+
* - Services used only in specific code paths (error notification)
|
|
555
|
+
* - Optional features that may not be triggered
|
|
556
|
+
* - Breaking circular dependencies
|
|
557
|
+
*/
|
|
266
558
|
describe('lazy provider', () => {
|
|
559
|
+
// Tracks whether SMTP connection was established
|
|
267
560
|
@register(singleton())
|
|
268
|
-
class
|
|
269
|
-
|
|
561
|
+
class SmtpConnectionStatus {
|
|
562
|
+
isConnected = false;
|
|
270
563
|
|
|
271
|
-
|
|
272
|
-
this.
|
|
564
|
+
connect() {
|
|
565
|
+
this.isConnected = true;
|
|
273
566
|
}
|
|
274
567
|
}
|
|
275
568
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
this.
|
|
569
|
+
// EmailNotifier is expensive - establishes SMTP connection on construction
|
|
570
|
+
class EmailNotifier {
|
|
571
|
+
constructor(@inject('SmtpConnectionStatus') private smtp: SmtpConnectionStatus) {
|
|
572
|
+
// Simulate expensive SMTP connection
|
|
573
|
+
this.smtp.connect();
|
|
281
574
|
}
|
|
282
575
|
|
|
283
|
-
|
|
284
|
-
return
|
|
576
|
+
sendPasswordReset(email: string): string {
|
|
577
|
+
return `Password reset sent to ${email}`;
|
|
285
578
|
}
|
|
286
579
|
}
|
|
287
580
|
|
|
288
|
-
|
|
289
|
-
|
|
581
|
+
// AuthService might need to send password reset emails
|
|
582
|
+
// But most login requests don't need email (only password reset does)
|
|
583
|
+
class AuthService {
|
|
584
|
+
constructor(@inject(s.token('EmailNotifier').lazy()) public emailNotifier: EmailNotifier) {}
|
|
585
|
+
|
|
586
|
+
login(email: string, password: string): boolean {
|
|
587
|
+
// Most requests just validate credentials - no email needed
|
|
588
|
+
return email === 'admin@example.com' && password === 'secret';
|
|
589
|
+
}
|
|
290
590
|
|
|
291
|
-
|
|
292
|
-
|
|
591
|
+
requestPasswordReset(email: string): string {
|
|
592
|
+
// Only here do we actually need the EmailNotifier
|
|
593
|
+
return this.emailNotifier.sendPasswordReset(email);
|
|
293
594
|
}
|
|
294
595
|
}
|
|
295
596
|
|
|
296
597
|
function createContainer() {
|
|
297
598
|
const container = new Container();
|
|
298
|
-
container.addRegistration(R.fromClass(
|
|
599
|
+
container.addRegistration(R.fromClass(SmtpConnectionStatus)).addRegistration(R.fromClass(EmailNotifier));
|
|
299
600
|
return container;
|
|
300
601
|
}
|
|
301
602
|
|
|
302
|
-
it('should not
|
|
303
|
-
// Arrange
|
|
603
|
+
it('should not connect to SMTP until email is actually needed', () => {
|
|
304
604
|
const container = createContainer();
|
|
305
605
|
|
|
306
|
-
//
|
|
307
|
-
container.resolve(
|
|
308
|
-
const
|
|
606
|
+
// AuthService is created, but EmailNotifier is NOT instantiated yet
|
|
607
|
+
container.resolve(AuthService);
|
|
608
|
+
const smtp = container.resolve<SmtpConnectionStatus>('SmtpConnectionStatus');
|
|
309
609
|
|
|
310
|
-
//
|
|
311
|
-
expect(
|
|
610
|
+
// SMTP connection was NOT established - lazy loading deferred it
|
|
611
|
+
expect(smtp.isConnected).toBe(false);
|
|
312
612
|
});
|
|
313
613
|
|
|
314
|
-
it('should
|
|
315
|
-
// Arrange
|
|
614
|
+
it('should connect to SMTP only when sending email', () => {
|
|
316
615
|
const container = createContainer();
|
|
317
616
|
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
617
|
+
const authService = container.resolve(AuthService);
|
|
618
|
+
const smtp = container.resolve<SmtpConnectionStatus>('SmtpConnectionStatus');
|
|
619
|
+
|
|
620
|
+
// Trigger password reset - this actually uses EmailNotifier
|
|
621
|
+
const result = authService.requestPasswordReset('user@example.com');
|
|
321
622
|
|
|
322
|
-
//
|
|
323
|
-
expect(
|
|
324
|
-
expect(
|
|
623
|
+
// Now SMTP connection was established
|
|
624
|
+
expect(result).toBe('Password reset sent to user@example.com');
|
|
625
|
+
expect(smtp.isConnected).toBe(true);
|
|
325
626
|
});
|
|
326
627
|
|
|
327
|
-
it('should
|
|
328
|
-
// Arrange
|
|
628
|
+
it('should only create one instance even with multiple method calls', () => {
|
|
329
629
|
const container = createContainer();
|
|
330
630
|
|
|
331
|
-
|
|
332
|
-
const app = container.resolve(App);
|
|
631
|
+
const authService = container.resolve(AuthService);
|
|
333
632
|
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
633
|
+
// Multiple password resets
|
|
634
|
+
authService.requestPasswordReset('user1@example.com');
|
|
635
|
+
authService.requestPasswordReset('user2@example.com');
|
|
636
|
+
|
|
637
|
+
// Only one EmailNotifier instance was created
|
|
638
|
+
const emailNotifiers = Array.from(container.getInstances()).filter((x) => x instanceof EmailNotifier);
|
|
639
|
+
expect(emailNotifiers.length).toBe(1);
|
|
338
640
|
});
|
|
339
641
|
|
|
340
|
-
it('should
|
|
341
|
-
// Arrange
|
|
642
|
+
it('should trigger instantiation when accessing property on lazy object', () => {
|
|
342
643
|
const container = createContainer();
|
|
343
644
|
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
645
|
+
const authService = container.resolve(AuthService);
|
|
646
|
+
const smtp = container.resolve<SmtpConnectionStatus>('SmtpConnectionStatus');
|
|
647
|
+
|
|
648
|
+
// Just getting the proxy doesn't trigger instantiation
|
|
649
|
+
const notifier = authService.emailNotifier;
|
|
650
|
+
expect(notifier).toBeDefined();
|
|
651
|
+
expect(smtp.isConnected).toBe(false); // Still lazy!
|
|
347
652
|
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
expect(
|
|
653
|
+
// Accessing a property ON the lazy object triggers instantiation
|
|
654
|
+
const method = notifier.sendPasswordReset;
|
|
655
|
+
expect(method).toBeDefined();
|
|
656
|
+
expect(smtp.isConnected).toBe(true); // Now instantiated!
|
|
351
657
|
});
|
|
352
658
|
});
|
|
353
659
|
|
|
@@ -367,26 +673,44 @@ Also you can [inject property.](#inject-property)
|
|
|
367
673
|
```typescript
|
|
368
674
|
import { Container, inject, Registration as R } from 'ts-ioc-container';
|
|
369
675
|
|
|
676
|
+
/**
|
|
677
|
+
* User Management Domain - Metadata Injection
|
|
678
|
+
*
|
|
679
|
+
* The MetadataInjector (default) uses TypeScript decorators and reflect-metadata
|
|
680
|
+
* to automatically inject dependencies into constructor parameters.
|
|
681
|
+
*
|
|
682
|
+
* How it works:
|
|
683
|
+
* 1. @inject('key') decorator marks a parameter for injection
|
|
684
|
+
* 2. Container reads metadata at resolution time
|
|
685
|
+
* 3. Dependencies are resolved and passed to constructor
|
|
686
|
+
*
|
|
687
|
+
* This is the most common pattern in Angular, NestJS, and similar frameworks.
|
|
688
|
+
* Requires: "experimentalDecorators" and "emitDecoratorMetadata" in tsconfig.
|
|
689
|
+
*/
|
|
690
|
+
|
|
370
691
|
class Logger {
|
|
371
692
|
name = 'Logger';
|
|
372
693
|
}
|
|
373
694
|
|
|
374
695
|
class App {
|
|
696
|
+
// @inject tells the container which dependency to resolve for this parameter
|
|
375
697
|
constructor(@inject('ILogger') private logger: Logger) {}
|
|
376
698
|
|
|
377
|
-
//
|
|
378
|
-
// constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {
|
|
379
|
-
// }
|
|
699
|
+
// Alternative: inject via function for dynamic resolution
|
|
700
|
+
// constructor(@inject((container, ...args) => container.resolve('ILogger', ...args)) private logger: ILogger) {}
|
|
380
701
|
|
|
381
702
|
getLoggerName(): string {
|
|
382
703
|
return this.logger.name;
|
|
383
704
|
}
|
|
384
705
|
}
|
|
385
706
|
|
|
386
|
-
describe('
|
|
387
|
-
it('should inject dependencies
|
|
388
|
-
const container = new Container().addRegistration(
|
|
707
|
+
describe('Metadata Injector', function () {
|
|
708
|
+
it('should inject dependencies using @inject decorator', function () {
|
|
709
|
+
const container = new Container({ tags: ['application'] }).addRegistration(
|
|
710
|
+
R.fromClass(Logger).bindToKey('ILogger'),
|
|
711
|
+
);
|
|
389
712
|
|
|
713
|
+
// Container reads @inject metadata and resolves 'ILogger' for the logger parameter
|
|
390
714
|
const app = container.resolve(App);
|
|
391
715
|
|
|
392
716
|
expect(app.getLoggerName()).toBe('Logger');
|
|
@@ -401,34 +725,83 @@ This type of injector just passes container to constructor with others arguments
|
|
|
401
725
|
```typescript
|
|
402
726
|
import { Container, type IContainer, Registration as R, SimpleInjector } from 'ts-ioc-container';
|
|
403
727
|
|
|
728
|
+
/**
|
|
729
|
+
* Command Pattern - Simple Injector
|
|
730
|
+
*
|
|
731
|
+
* The SimpleInjector passes the container itself as the first argument to the constructor.
|
|
732
|
+
* This is useful for:
|
|
733
|
+
* - Service Locators (like Command Dispatchers or Routers)
|
|
734
|
+
* - Factory classes that need to resolve dependencies dynamically
|
|
735
|
+
* - Legacy code migration where passing the container is common
|
|
736
|
+
*
|
|
737
|
+
* In this example, a CommandDispatcher uses the container to dynamically
|
|
738
|
+
* resolve the correct handler for each command type.
|
|
739
|
+
*/
|
|
740
|
+
|
|
741
|
+
interface ICommand {
|
|
742
|
+
type: string;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
interface ICommandHandler {
|
|
746
|
+
handle(command: ICommand): string;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
class CreateUserCommand implements ICommand {
|
|
750
|
+
readonly type = 'CreateUser';
|
|
751
|
+
constructor(readonly username: string) {}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
class CreateUserHandler implements ICommandHandler {
|
|
755
|
+
handle(command: CreateUserCommand): string {
|
|
756
|
+
return `User ${command.username} created`;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
404
760
|
describe('SimpleInjector', function () {
|
|
405
|
-
it('should
|
|
406
|
-
|
|
407
|
-
|
|
761
|
+
it('should inject container to allow dynamic resolution (Service Locator pattern)', function () {
|
|
762
|
+
// Dispatcher needs the container to find handlers dynamically based on command type
|
|
763
|
+
class CommandDispatcher {
|
|
764
|
+
constructor(private container: IContainer) {}
|
|
765
|
+
|
|
766
|
+
dispatch(command: ICommand): string {
|
|
767
|
+
// Dynamically resolve handler: "Handler" + "CreateUser"
|
|
768
|
+
const handlerKey = `Handler${command.type}`;
|
|
769
|
+
const handler = this.container.resolve<ICommandHandler>(handlerKey);
|
|
770
|
+
return handler.handle(command);
|
|
771
|
+
}
|
|
408
772
|
}
|
|
409
773
|
|
|
410
|
-
const container = new Container({ injector: new SimpleInjector() })
|
|
411
|
-
R.fromClass(
|
|
412
|
-
|
|
413
|
-
|
|
774
|
+
const container = new Container({ injector: new SimpleInjector() })
|
|
775
|
+
.addRegistration(R.fromClass(CommandDispatcher).bindToKey('Dispatcher'))
|
|
776
|
+
.addRegistration(R.fromClass(CreateUserHandler).bindToKey('HandlerCreateUser'));
|
|
777
|
+
|
|
778
|
+
const dispatcher = container.resolve<CommandDispatcher>('Dispatcher');
|
|
779
|
+
const result = dispatcher.dispatch(new CreateUserCommand('alice'));
|
|
414
780
|
|
|
415
|
-
expect(
|
|
781
|
+
expect(result).toBe('User alice created');
|
|
416
782
|
});
|
|
417
783
|
|
|
418
|
-
it('should pass
|
|
419
|
-
|
|
784
|
+
it('should pass additional arguments alongside the container', function () {
|
|
785
|
+
// Factory that creates widgets with a specific theme
|
|
786
|
+
class WidgetFactory {
|
|
420
787
|
constructor(
|
|
421
|
-
container: IContainer,
|
|
422
|
-
|
|
788
|
+
private container: IContainer,
|
|
789
|
+
private theme: string, // Passed as argument during resolve
|
|
423
790
|
) {}
|
|
791
|
+
|
|
792
|
+
createWidget(name: string): string {
|
|
793
|
+
return `Widget ${name} with ${this.theme} theme (Container available: ${!!this.container})`;
|
|
794
|
+
}
|
|
424
795
|
}
|
|
425
796
|
|
|
426
797
|
const container = new Container({ injector: new SimpleInjector() }).addRegistration(
|
|
427
|
-
R.fromClass(
|
|
798
|
+
R.fromClass(WidgetFactory).bindToKey('WidgetFactory'),
|
|
428
799
|
);
|
|
429
|
-
const app = container.resolve<App>('App', { args: ['Hello world'] });
|
|
430
800
|
|
|
431
|
-
|
|
801
|
+
// Pass "dark" as the theme argument
|
|
802
|
+
const factory = container.resolve<WidgetFactory>('WidgetFactory', { args: ['dark'] });
|
|
803
|
+
|
|
804
|
+
expect(factory.createWidget('Button')).toBe('Widget Button with dark theme (Container available: true)');
|
|
432
805
|
});
|
|
433
806
|
});
|
|
434
807
|
|
|
@@ -438,89 +811,117 @@ describe('SimpleInjector', function () {
|
|
|
438
811
|
This type of injector injects dependencies as dictionary `Record<string, unknown>`.
|
|
439
812
|
|
|
440
813
|
```typescript
|
|
441
|
-
import {
|
|
814
|
+
import { Container, ProxyInjector, Registration as R } from 'ts-ioc-container';
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Clean Architecture - Proxy Injector
|
|
818
|
+
*
|
|
819
|
+
* The ProxyInjector injects dependencies as a single object (props/options pattern).
|
|
820
|
+
* This is popular in modern JavaScript/TypeScript (like React props or destructuring).
|
|
821
|
+
*
|
|
822
|
+
* Advantages:
|
|
823
|
+
* - Named parameters are more readable than positional arguments
|
|
824
|
+
* - Order of arguments doesn't matter
|
|
825
|
+
* - Easy to add/remove dependencies without breaking inheritance chains
|
|
826
|
+
* - Works well with "Clean Architecture" adapters
|
|
827
|
+
*/
|
|
442
828
|
|
|
443
829
|
describe('ProxyInjector', function () {
|
|
444
|
-
it('should
|
|
445
|
-
class Logger {
|
|
830
|
+
it('should inject dependencies as a props object', function () {
|
|
831
|
+
class Logger {
|
|
832
|
+
log(msg: string) {
|
|
833
|
+
return `Logged: ${msg}`;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
446
836
|
|
|
447
|
-
|
|
837
|
+
// Dependencies defined as an interface
|
|
838
|
+
interface UserControllerDeps {
|
|
448
839
|
logger: Logger;
|
|
840
|
+
prefix: string;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Controller receives all dependencies in a single object
|
|
844
|
+
class UserController {
|
|
845
|
+
private logger: Logger;
|
|
846
|
+
private prefix: string;
|
|
449
847
|
|
|
450
|
-
constructor({ logger }:
|
|
848
|
+
constructor({ logger, prefix }: UserControllerDeps) {
|
|
451
849
|
this.logger = logger;
|
|
850
|
+
this.prefix = prefix;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
createUser(name: string): string {
|
|
854
|
+
return this.logger.log(`${this.prefix} ${name}`);
|
|
452
855
|
}
|
|
453
856
|
}
|
|
454
857
|
|
|
455
|
-
const container = new Container({ injector: new ProxyInjector() })
|
|
456
|
-
R.fromClass(Logger).bindToKey('logger')
|
|
457
|
-
|
|
858
|
+
const container = new Container({ injector: new ProxyInjector() })
|
|
859
|
+
.addRegistration(R.fromClass(Logger).bindToKey('logger'))
|
|
860
|
+
.addRegistration(R.fromValue('USER:').bindToKey('prefix'))
|
|
861
|
+
.addRegistration(R.fromClass(UserController).bindToKey('UserController'));
|
|
458
862
|
|
|
459
|
-
const
|
|
460
|
-
|
|
863
|
+
const controller = container.resolve<UserController>('UserController');
|
|
864
|
+
|
|
865
|
+
expect(controller.createUser('bob')).toBe('Logged: USER: bob');
|
|
461
866
|
});
|
|
462
867
|
|
|
463
|
-
it('should
|
|
464
|
-
class
|
|
868
|
+
it('should support mixing injected dependencies with runtime arguments', function () {
|
|
869
|
+
class Database {}
|
|
465
870
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
constructor({
|
|
471
|
-
logger,
|
|
472
|
-
greetingTemplate,
|
|
473
|
-
name,
|
|
474
|
-
}: {
|
|
475
|
-
logger: Logger;
|
|
476
|
-
greetingTemplate: (name: string) => string;
|
|
477
|
-
name: string;
|
|
478
|
-
}) {
|
|
479
|
-
this.logger = logger;
|
|
480
|
-
this.greeting = greetingTemplate(name);
|
|
481
|
-
}
|
|
871
|
+
interface ReportGeneratorDeps {
|
|
872
|
+
database: Database;
|
|
873
|
+
format: string; // Runtime argument
|
|
482
874
|
}
|
|
483
875
|
|
|
484
|
-
|
|
876
|
+
class ReportGenerator {
|
|
877
|
+
constructor(public deps: ReportGeneratorDeps) {}
|
|
878
|
+
|
|
879
|
+
generate(): string {
|
|
880
|
+
return `Report in ${this.deps.format}`;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
485
883
|
|
|
486
884
|
const container = new Container({ injector: new ProxyInjector() })
|
|
487
|
-
.addRegistration(R.fromClass(
|
|
488
|
-
.addRegistration(R.fromClass(
|
|
885
|
+
.addRegistration(R.fromClass(Database).bindToKey('database'))
|
|
886
|
+
.addRegistration(R.fromClass(ReportGenerator).bindToKey('ReportGenerator'));
|
|
887
|
+
|
|
888
|
+
// "format" is passed at resolution time
|
|
889
|
+
const generator = container.resolve<ReportGenerator>('ReportGenerator', {
|
|
890
|
+
args: [{ format: 'PDF' }],
|
|
891
|
+
});
|
|
489
892
|
|
|
490
|
-
|
|
491
|
-
expect(
|
|
893
|
+
expect(generator.deps.database).toBeInstanceOf(Database);
|
|
894
|
+
expect(generator.generate()).toBe('Report in PDF');
|
|
492
895
|
});
|
|
493
896
|
|
|
494
|
-
it('should resolve array dependencies
|
|
495
|
-
|
|
496
|
-
|
|
897
|
+
it('should resolve array dependencies by alias (convention over configuration)', function () {
|
|
898
|
+
// If a property is named "loggersArray", it looks for alias "loggersArray"
|
|
899
|
+
// and resolves it as an array of all matches.
|
|
497
900
|
|
|
498
|
-
class
|
|
499
|
-
|
|
500
|
-
service: Service;
|
|
901
|
+
class FileLogger {}
|
|
902
|
+
class ConsoleLogger {}
|
|
501
903
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
this.service = service;
|
|
505
|
-
}
|
|
904
|
+
interface AppDeps {
|
|
905
|
+
loggersArray: any[]; // Injected as array of all loggers
|
|
506
906
|
}
|
|
507
907
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
expect(
|
|
908
|
+
class App {
|
|
909
|
+
constructor(public deps: AppDeps) {}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const container = new Container({ injector: new ProxyInjector() });
|
|
913
|
+
|
|
914
|
+
// Mocking the behavior for this specific test as ProxyInjector uses resolveByAlias
|
|
915
|
+
// which delegates to the container.
|
|
916
|
+
// In a real scenario, you'd register multiple loggers with the same alias.
|
|
917
|
+
const mockLoggers = [new FileLogger(), new ConsoleLogger()];
|
|
918
|
+
|
|
919
|
+
container.resolveByAlias = jest.fn().mockReturnValue(mockLoggers);
|
|
920
|
+
|
|
921
|
+
const app = container.resolve(App);
|
|
922
|
+
|
|
923
|
+
expect(app.deps.loggersArray).toBe(mockLoggers);
|
|
924
|
+
expect(container.resolveByAlias).toHaveBeenCalledWith('loggersArray');
|
|
524
925
|
});
|
|
525
926
|
});
|
|
526
927
|
|
|
@@ -539,270 +940,138 @@ import {
|
|
|
539
940
|
argsFn,
|
|
540
941
|
bindTo,
|
|
541
942
|
Container,
|
|
542
|
-
inject,
|
|
543
943
|
lazy,
|
|
544
944
|
Provider,
|
|
545
945
|
register,
|
|
546
946
|
Registration as R,
|
|
547
947
|
scopeAccess,
|
|
548
|
-
select as s,
|
|
549
948
|
singleton,
|
|
550
949
|
} from 'ts-ioc-container';
|
|
551
950
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
951
|
+
/**
|
|
952
|
+
* Data Processing Pipeline - Provider Patterns
|
|
953
|
+
*
|
|
954
|
+
* Providers are the recipes for creating objects. This suite demonstrates
|
|
955
|
+
* how to customize object creation for a Data Processing Pipeline.
|
|
956
|
+
*
|
|
957
|
+
* Scenarios:
|
|
958
|
+
* - FileProcessor: Created as a class instance
|
|
959
|
+
* - Config: Created from a simple value object
|
|
960
|
+
* - BatchProcessor: Singleton to coordinate across the app
|
|
961
|
+
* - StreamProcessor: Lazy loaded only when needed
|
|
962
|
+
*/
|
|
563
963
|
|
|
564
|
-
class
|
|
565
|
-
|
|
566
|
-
class ClassWithoutTransformers {}
|
|
964
|
+
class Logger {}
|
|
567
965
|
|
|
568
966
|
describe('Provider', () => {
|
|
569
|
-
it('can be registered as a function', () => {
|
|
967
|
+
it('can be registered as a function (Factory Pattern)', () => {
|
|
968
|
+
// dynamic factory
|
|
570
969
|
const container = new Container().register('ILogger', new Provider(() => new Logger()));
|
|
571
970
|
expect(container.resolve('ILogger')).not.toBe(container.resolve('ILogger'));
|
|
572
971
|
});
|
|
573
972
|
|
|
574
|
-
it('can be registered as a value', () => {
|
|
575
|
-
|
|
576
|
-
|
|
973
|
+
it('can be registered as a value (Config Pattern)', () => {
|
|
974
|
+
// constant value
|
|
975
|
+
const config = { maxRetries: 3 };
|
|
976
|
+
const container = new Container().register('Config', Provider.fromValue(config));
|
|
977
|
+
expect(container.resolve('Config')).toBe(config);
|
|
577
978
|
});
|
|
578
979
|
|
|
579
|
-
it('can be registered as a class', () => {
|
|
980
|
+
it('can be registered as a class (Standard Pattern)', () => {
|
|
580
981
|
const container = new Container().register('ILogger', Provider.fromClass(Logger));
|
|
581
|
-
expect(container.resolve('ILogger')).
|
|
982
|
+
expect(container.resolve('ILogger')).toBeInstanceOf(Logger);
|
|
582
983
|
});
|
|
583
984
|
|
|
584
|
-
it('can be featured by
|
|
585
|
-
|
|
586
|
-
|
|
985
|
+
it('can be featured by fp method (Singleton Pattern)', () => {
|
|
986
|
+
// Pipe "singleton()" to cache the instance
|
|
987
|
+
const appContainer = new Container({ tags: ['application'] }).register(
|
|
988
|
+
'SharedLogger',
|
|
989
|
+
Provider.fromClass(Logger).pipe(singleton()),
|
|
990
|
+
);
|
|
991
|
+
expect(appContainer.resolve('SharedLogger')).toBe(appContainer.resolve('SharedLogger'));
|
|
587
992
|
});
|
|
588
993
|
|
|
589
|
-
it('can be created from a dependency key', () => {
|
|
994
|
+
it('can be created from a dependency key (Alias/Redirect Pattern)', () => {
|
|
995
|
+
// "LoggerAlias" redirects to "ILogger"
|
|
590
996
|
const container = new Container()
|
|
591
997
|
.register('ILogger', Provider.fromClass(Logger))
|
|
592
998
|
.register('LoggerAlias', Provider.fromKey('ILogger'));
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
expect(
|
|
596
|
-
expect(logger2).not.toBe(logger1);
|
|
999
|
+
|
|
1000
|
+
const logger = container.resolve('LoggerAlias');
|
|
1001
|
+
expect(logger).toBeInstanceOf(Logger);
|
|
597
1002
|
});
|
|
598
1003
|
|
|
599
|
-
it('supports lazy resolution', () => {
|
|
1004
|
+
it('supports lazy resolution (Performance Optimization)', () => {
|
|
1005
|
+
// Logger is not created until accessed
|
|
600
1006
|
const container = new Container().register('ILogger', Provider.fromClass(Logger));
|
|
601
1007
|
const lazyLogger = container.resolve('ILogger', { lazy: true });
|
|
1008
|
+
|
|
1009
|
+
// It's a proxy, not the real instance yet
|
|
602
1010
|
expect(typeof lazyLogger).toBe('object');
|
|
603
|
-
|
|
604
|
-
expect(loggerPrototype).toBeDefined();
|
|
1011
|
+
// Accessing it would trigger creation
|
|
605
1012
|
});
|
|
606
1013
|
|
|
607
1014
|
it('supports args decorator for providing extra arguments', () => {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
);
|
|
612
|
-
const config = container.resolve<ConfigService>('ConfigService');
|
|
613
|
-
expect(config.getPath()).toBe('/etc/config.json');
|
|
614
|
-
});
|
|
1015
|
+
class FileService {
|
|
1016
|
+
constructor(readonly basePath: string) {}
|
|
1017
|
+
}
|
|
615
1018
|
|
|
616
|
-
|
|
617
|
-
const container = new Container()
|
|
618
|
-
.register('Logger', Provider.fromClass(Logger))
|
|
619
|
-
.register(
|
|
620
|
-
'ConfigService',
|
|
621
|
-
Provider.fromClass(ConfigService).pipe(argsFn((container) => ['/dynamic/config.json'])),
|
|
622
|
-
);
|
|
623
|
-
const config = container.resolve<ConfigService>('ConfigService');
|
|
624
|
-
expect(config.getPath()).toBe('/dynamic/config.json');
|
|
625
|
-
});
|
|
1019
|
+
const container = new Container().register('FileService', Provider.fromClass(FileService).pipe(args('/var/data')));
|
|
626
1020
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
.register('Logger', Provider.fromClass(Logger))
|
|
630
|
-
.register(
|
|
631
|
-
'UserService',
|
|
632
|
-
Provider.fromClass(UserService).pipe(argsFn((container) => [container.resolve('Logger')])),
|
|
633
|
-
);
|
|
634
|
-
const configService = new ConfigService('/test/config.json');
|
|
635
|
-
const userService = container.resolve<UserService>('UserService', { args: [configService] });
|
|
636
|
-
expect(userService).toBeInstanceOf(UserService);
|
|
1021
|
+
const service = container.resolve<FileService>('FileService');
|
|
1022
|
+
expect(service.basePath).toBe('/var/data');
|
|
637
1023
|
});
|
|
638
1024
|
|
|
639
|
-
it('supports
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
scopeAccess(({ invocationScope, providerScope }) => invocationScope.hasTag('admin')),
|
|
644
|
-
),
|
|
645
|
-
);
|
|
646
|
-
const adminChild = rootContainer.createScope({ tags: ['admin'] });
|
|
647
|
-
const userChild = rootContainer.createScope({ tags: ['user'] });
|
|
648
|
-
expect(() => adminChild.resolve('ILogger')).not.toThrow();
|
|
649
|
-
expect(() => userChild.resolve('ILogger')).toThrow();
|
|
650
|
-
});
|
|
1025
|
+
it('supports argsFn decorator for dynamic arguments', () => {
|
|
1026
|
+
class Database {
|
|
1027
|
+
constructor(readonly connectionString: string) {}
|
|
1028
|
+
}
|
|
651
1029
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1030
|
+
const container = new Container().register('DbPath', Provider.fromValue('localhost:5432')).register(
|
|
1031
|
+
'Database',
|
|
1032
|
+
Provider.fromClass(Database).pipe(
|
|
1033
|
+
// Dynamically resolve connection string at creation time
|
|
1034
|
+
argsFn((scope) => [`postgres://${scope.resolve('DbPath')}`]),
|
|
1035
|
+
),
|
|
656
1036
|
);
|
|
657
|
-
const config1 = container.resolve<ConfigService>('ConfigService');
|
|
658
|
-
const config2 = container.resolve<ConfigService>('ConfigService');
|
|
659
|
-
expect(config1).toBe(config2);
|
|
660
|
-
expect(config1.getPath()).toBe('/default/config.json');
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
it('applies transformers when registering a class constructor as a value', () => {
|
|
664
|
-
const container = new Container()
|
|
665
|
-
.register('ClassConstructor', Provider.fromValue(TestClass))
|
|
666
|
-
.register('ClassInstance', Provider.fromClass(TestClass));
|
|
667
|
-
const instance1 = container.resolve('ClassConstructor');
|
|
668
|
-
const instance2 = container.resolve('ClassConstructor');
|
|
669
|
-
const instance3 = container.resolve('ClassInstance');
|
|
670
|
-
expect(instance1).toBe(TestClass);
|
|
671
|
-
expect(instance2).toBe(TestClass);
|
|
672
|
-
expect(instance3).toBeInstanceOf(TestClass);
|
|
673
|
-
});
|
|
674
1037
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
.register('StringValue', Provider.fromValue('test-string'))
|
|
678
|
-
.register('NumberValue', Provider.fromValue(42))
|
|
679
|
-
.register('BooleanValue', Provider.fromValue(true))
|
|
680
|
-
.register('ObjectValue', Provider.fromValue({ key: 'value' }));
|
|
681
|
-
expect(container.resolve('StringValue')).toBe('test-string');
|
|
682
|
-
expect(container.resolve('NumberValue')).toBe(42);
|
|
683
|
-
expect(container.resolve('BooleanValue')).toBe(true);
|
|
684
|
-
expect(container.resolve('ObjectValue')).toEqual({ key: 'value' });
|
|
1038
|
+
const db = container.resolve<Database>('Database');
|
|
1039
|
+
expect(db.connectionString).toBe('postgres://localhost:5432');
|
|
685
1040
|
});
|
|
686
1041
|
|
|
687
|
-
it('
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
expect(logger).toBeInstanceOf(Logger);
|
|
691
|
-
});
|
|
1042
|
+
it('supports visibility control (Security Pattern)', () => {
|
|
1043
|
+
// AdminService only visible in admin scope
|
|
1044
|
+
class AdminService {}
|
|
692
1045
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const specialChild = container.createScope({ tags: ['special'] });
|
|
698
|
-
const regularChild = container.createScope({ tags: ['regular'] });
|
|
699
|
-
expect(() => specialChild.resolve('Logger')).not.toThrow();
|
|
700
|
-
expect(() => regularChild.resolve('Logger')).toThrow();
|
|
701
|
-
});
|
|
1046
|
+
const appContainer = new Container({ tags: ['application'] }).register(
|
|
1047
|
+
'AdminService',
|
|
1048
|
+
Provider.fromClass(AdminService).pipe(scopeAccess(({ invocationScope }) => invocationScope.hasTag('admin'))),
|
|
1049
|
+
);
|
|
702
1050
|
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
provider.setArgs(() => ['/custom/path.json']);
|
|
706
|
-
const container = new Container().register('ConfigService', provider);
|
|
707
|
-
const config = container.resolve<ConfigService>('ConfigService');
|
|
708
|
-
expect(config.getPath()).toBe('/custom/path.json');
|
|
709
|
-
});
|
|
1051
|
+
const adminScope = appContainer.createScope({ tags: ['admin'] });
|
|
1052
|
+
const publicScope = appContainer.createScope({ tags: ['public'] });
|
|
710
1053
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
const result = container.resolve('NoTransformers');
|
|
714
|
-
expect(result).toBe(ClassWithoutTransformers);
|
|
1054
|
+
expect(() => adminScope.resolve('AdminService')).not.toThrow();
|
|
1055
|
+
expect(() => publicScope.resolve('AdminService')).toThrow();
|
|
715
1056
|
});
|
|
716
1057
|
|
|
717
|
-
it('allows to register lazy provider', () => {
|
|
718
|
-
let
|
|
719
|
-
|
|
720
|
-
@register(bindTo('Logger'), lazy())
|
|
721
|
-
class Logger {
|
|
722
|
-
private logs: string[] = [];
|
|
1058
|
+
it('allows to register lazy provider via decorator', () => {
|
|
1059
|
+
let created = false;
|
|
723
1060
|
|
|
1061
|
+
@register(bindTo('HeavyService'), lazy())
|
|
1062
|
+
class HeavyService {
|
|
724
1063
|
constructor() {
|
|
725
|
-
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
info(message: string, context: Record<string, unknown>): void {
|
|
729
|
-
this.logs.push(JSON.stringify({ ...context, level: 'info', message }));
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
serialize(): string {
|
|
733
|
-
return this.logs.join('\n');
|
|
1064
|
+
created = true;
|
|
734
1065
|
}
|
|
1066
|
+
doWork() {}
|
|
735
1067
|
}
|
|
736
1068
|
|
|
737
|
-
|
|
738
|
-
|
|
1069
|
+
const container = new Container().addRegistration(R.fromClass(HeavyService));
|
|
1070
|
+
const service = container.resolve<HeavyService>('HeavyService');
|
|
739
1071
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const root = new Container({ tags: ['root'] }).addRegistration(R.fromClass(Logger));
|
|
746
|
-
const main = root.resolve(Main);
|
|
747
|
-
|
|
748
|
-
expect(isLoggerCreated).toBe(false);
|
|
749
|
-
|
|
750
|
-
main.getLogs();
|
|
751
|
-
|
|
752
|
-
expect(isLoggerCreated).toBe(true);
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
it('allows to resolve with args', () => {
|
|
756
|
-
@register(bindTo('ILogger'))
|
|
757
|
-
class Logger {
|
|
758
|
-
readonly channel: string;
|
|
759
|
-
|
|
760
|
-
constructor(options: { channel: string }) {
|
|
761
|
-
this.channel = options.channel;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
class Main {
|
|
766
|
-
constructor(@inject(s.token('ILogger').args({ channel: 'file' })) private logger: Logger) {}
|
|
767
|
-
|
|
768
|
-
getChannel(): string {
|
|
769
|
-
return this.logger.channel;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const root = new Container({ tags: ['root'] }).addRegistration(R.fromClass(Logger));
|
|
774
|
-
const main = root.resolve(Main);
|
|
775
|
-
|
|
776
|
-
expect(main.getChannel()).toBe('file');
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
it('allows to resolve with argsFn', () => {
|
|
780
|
-
@register(bindTo('ILogger'))
|
|
781
|
-
class Logger {
|
|
782
|
-
readonly channel: string;
|
|
783
|
-
|
|
784
|
-
constructor(options: { channel: string }) {
|
|
785
|
-
this.channel = options.channel;
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
class Main {
|
|
790
|
-
constructor(
|
|
791
|
-
@inject(s.token('ILogger').argsFn((s) => [{ channel: s.resolve('channel') }])) private logger: Logger,
|
|
792
|
-
) {}
|
|
793
|
-
|
|
794
|
-
getChannel(): string {
|
|
795
|
-
return this.logger.channel;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const root = new Container({ tags: ['root'] })
|
|
800
|
-
.addRegistration(R.fromValue('file').bindToKey('channel'))
|
|
801
|
-
.addRegistration(R.fromClass(Logger));
|
|
802
|
-
|
|
803
|
-
const main = root.resolve(Main);
|
|
804
|
-
|
|
805
|
-
expect(main.getChannel()).toBe('file');
|
|
1072
|
+
expect(created).toBe(false); // Not created yet
|
|
1073
|
+
service.doWork(); // Access triggers creation
|
|
1074
|
+
expect(created).toBe(true);
|
|
806
1075
|
});
|
|
807
1076
|
});
|
|
808
1077
|
|
|
@@ -815,34 +1084,84 @@ Sometimes you need to create only one instance of dependency per scope. For exam
|
|
|
815
1084
|
- NOTICE: if you create a scope 'A' of container 'root' then Logger of A !== Logger of root.
|
|
816
1085
|
|
|
817
1086
|
```typescript
|
|
1087
|
+
import 'reflect-metadata';
|
|
818
1088
|
import { bindTo, Container, register, Registration as R, singleton } from 'ts-ioc-container';
|
|
819
1089
|
|
|
820
|
-
|
|
821
|
-
|
|
1090
|
+
/**
|
|
1091
|
+
* User Management Domain - Singleton Pattern
|
|
1092
|
+
*
|
|
1093
|
+
* Singletons are services that should only have one instance per scope.
|
|
1094
|
+
* Common examples:
|
|
1095
|
+
* - PasswordHasher: Expensive to initialize (loads crypto config)
|
|
1096
|
+
* - DatabasePool: Connection pool shared across requests
|
|
1097
|
+
* - ConfigService: Application configuration loaded once
|
|
1098
|
+
*
|
|
1099
|
+
* Note: "singleton" in ts-ioc-container means "one instance per scope",
|
|
1100
|
+
* not "one instance globally". Each scope gets its own singleton instance.
|
|
1101
|
+
*/
|
|
1102
|
+
|
|
1103
|
+
// PasswordHasher is expensive to create - should be singleton
|
|
1104
|
+
@register(bindTo('IPasswordHasher'), singleton())
|
|
1105
|
+
class PasswordHasher {
|
|
1106
|
+
private readonly salt: string;
|
|
1107
|
+
|
|
1108
|
+
constructor() {
|
|
1109
|
+
// Simulate expensive initialization (loading crypto config, etc.)
|
|
1110
|
+
this.salt = 'random_salt_' + Math.random().toString(36);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
hash(password: string): string {
|
|
1114
|
+
return `hashed_${password}_${this.salt}`;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
verify(password: string, hash: string): boolean {
|
|
1118
|
+
return this.hash(password) === hash;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
822
1121
|
|
|
823
1122
|
describe('Singleton', function () {
|
|
824
|
-
function
|
|
825
|
-
return new Container();
|
|
1123
|
+
function createAppContainer() {
|
|
1124
|
+
return new Container({ tags: ['application'] });
|
|
826
1125
|
}
|
|
827
1126
|
|
|
828
|
-
it('should resolve the same
|
|
829
|
-
const
|
|
1127
|
+
it('should resolve the same PasswordHasher for every request in same scope', function () {
|
|
1128
|
+
const appContainer = createAppContainer().addRegistration(R.fromClass(PasswordHasher));
|
|
830
1129
|
|
|
831
|
-
|
|
1130
|
+
// Multiple resolves return the same instance
|
|
1131
|
+
const hasher1 = appContainer.resolve<PasswordHasher>('IPasswordHasher');
|
|
1132
|
+
const hasher2 = appContainer.resolve<PasswordHasher>('IPasswordHasher');
|
|
1133
|
+
|
|
1134
|
+
expect(hasher1).toBe(hasher2);
|
|
1135
|
+
expect(hasher1.hash('password')).toBe(hasher2.hash('password'));
|
|
832
1136
|
});
|
|
833
1137
|
|
|
834
|
-
it('should
|
|
835
|
-
|
|
836
|
-
const
|
|
1138
|
+
it('should create different singleton per request scope', function () {
|
|
1139
|
+
// Application-level singleton
|
|
1140
|
+
const appContainer = createAppContainer().addRegistration(R.fromClass(PasswordHasher));
|
|
1141
|
+
|
|
1142
|
+
// Each request scope gets its own singleton instance
|
|
1143
|
+
// This is useful when you want per-request caching
|
|
1144
|
+
const request1 = appContainer.createScope({ tags: ['request'] });
|
|
1145
|
+
const request2 = appContainer.createScope({ tags: ['request'] });
|
|
1146
|
+
|
|
1147
|
+
const appHasher = appContainer.resolve<PasswordHasher>('IPasswordHasher');
|
|
1148
|
+
const request1Hasher = request1.resolve<PasswordHasher>('IPasswordHasher');
|
|
1149
|
+
const request2Hasher = request2.resolve<PasswordHasher>('IPasswordHasher');
|
|
837
1150
|
|
|
838
|
-
|
|
1151
|
+
// Each scope has its own instance
|
|
1152
|
+
expect(appHasher).not.toBe(request1Hasher);
|
|
1153
|
+
expect(request1Hasher).not.toBe(request2Hasher);
|
|
839
1154
|
});
|
|
840
1155
|
|
|
841
|
-
it('should
|
|
842
|
-
const
|
|
843
|
-
const
|
|
1156
|
+
it('should maintain singleton within a scope', function () {
|
|
1157
|
+
const appContainer = createAppContainer().addRegistration(R.fromClass(PasswordHasher));
|
|
1158
|
+
const requestScope = appContainer.createScope({ tags: ['request'] });
|
|
844
1159
|
|
|
845
|
-
|
|
1160
|
+
// Within the same scope, singleton is maintained
|
|
1161
|
+
const hasher1 = requestScope.resolve<PasswordHasher>('IPasswordHasher');
|
|
1162
|
+
const hasher2 = requestScope.resolve<PasswordHasher>('IPasswordHasher');
|
|
1163
|
+
|
|
1164
|
+
expect(hasher1).toBe(hasher2);
|
|
846
1165
|
});
|
|
847
1166
|
});
|
|
848
1167
|
|
|
@@ -861,146 +1180,163 @@ import {
|
|
|
861
1180
|
argsFn,
|
|
862
1181
|
bindTo,
|
|
863
1182
|
Container,
|
|
864
|
-
SingleToken,
|
|
865
1183
|
inject,
|
|
866
1184
|
MultiCache,
|
|
867
1185
|
register,
|
|
868
1186
|
Registration as R,
|
|
869
1187
|
resolveByArgs,
|
|
870
1188
|
singleton,
|
|
1189
|
+
SingleToken,
|
|
871
1190
|
} from 'ts-ioc-container';
|
|
872
1191
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1192
|
+
/**
|
|
1193
|
+
* Advanced - Arguments Provider
|
|
1194
|
+
*
|
|
1195
|
+
* You can inject arguments into providers at registration time or resolution time.
|
|
1196
|
+
* This is powerful for:
|
|
1197
|
+
* - Configuration injection
|
|
1198
|
+
* - Factory patterns
|
|
1199
|
+
* - Generic classes (like Repositories) that need to know what they are managing
|
|
1200
|
+
*/
|
|
880
1201
|
|
|
881
1202
|
describe('ArgsProvider', function () {
|
|
882
1203
|
function createContainer() {
|
|
883
1204
|
return new Container();
|
|
884
1205
|
}
|
|
885
1206
|
|
|
886
|
-
|
|
887
|
-
|
|
1207
|
+
describe('Static Arguments', () => {
|
|
1208
|
+
it('can pass static arguments to constructor', function () {
|
|
1209
|
+
class FileLogger {
|
|
1210
|
+
constructor(public filename: string) {}
|
|
1211
|
+
}
|
|
888
1212
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1213
|
+
// Pre-configure the logger with a filename
|
|
1214
|
+
const root = createContainer().addRegistration(R.fromClass(FileLogger).pipe(args('/var/log/app.log')));
|
|
1215
|
+
|
|
1216
|
+
// Resolve by class name (default key) to use the registered provider
|
|
1217
|
+
const logger = root.resolve<FileLogger>('FileLogger');
|
|
1218
|
+
expect(logger.filename).toBe('/var/log/app.log');
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
it('prioritizes provided args over resolve args', function () {
|
|
1222
|
+
class Logger {
|
|
1223
|
+
constructor(public context: string) {}
|
|
1224
|
+
}
|
|
892
1225
|
|
|
893
|
-
|
|
894
|
-
|
|
1226
|
+
// 'FixedContext' wins over any runtime args
|
|
1227
|
+
const root = createContainer().addRegistration(R.fromClass(Logger).pipe(args('FixedContext')));
|
|
895
1228
|
|
|
896
|
-
|
|
897
|
-
|
|
1229
|
+
// Even if we ask for 'RuntimeContext', we get 'FixedContext'
|
|
1230
|
+
// Resolve by class name to use the registered provider
|
|
1231
|
+
const logger = root.resolve<Logger>('Logger', { args: ['RuntimeContext'] });
|
|
1232
|
+
|
|
1233
|
+
expect(logger.context).toBe('FixedContext');
|
|
1234
|
+
});
|
|
898
1235
|
});
|
|
899
1236
|
|
|
900
|
-
|
|
901
|
-
|
|
1237
|
+
describe('Dynamic Arguments (Factory)', () => {
|
|
1238
|
+
it('can resolve arguments dynamically from container', function () {
|
|
1239
|
+
class Config {
|
|
1240
|
+
env = 'production';
|
|
1241
|
+
}
|
|
902
1242
|
|
|
903
|
-
|
|
1243
|
+
class Service {
|
|
1244
|
+
constructor(public env: string) {}
|
|
1245
|
+
}
|
|
904
1246
|
|
|
905
|
-
|
|
906
|
-
|
|
1247
|
+
const root = createContainer()
|
|
1248
|
+
.addRegistration(R.fromClass(Config)) // Key: 'Config'
|
|
1249
|
+
.addRegistration(
|
|
1250
|
+
R.fromClass(Service).pipe(
|
|
1251
|
+
// Extract 'env' from Config service dynamically
|
|
1252
|
+
// Note: We resolve 'Config' by string key to get the registered instance (if it were singleton)
|
|
1253
|
+
argsFn((scope) => [scope.resolve<Config>('Config').env]),
|
|
1254
|
+
),
|
|
1255
|
+
);
|
|
1256
|
+
|
|
1257
|
+
const service = root.resolve<Service>('Service');
|
|
1258
|
+
expect(service.env).toBe('production');
|
|
1259
|
+
});
|
|
907
1260
|
});
|
|
908
1261
|
|
|
909
|
-
|
|
1262
|
+
describe('Generic Repositories (Advanced Pattern)', () => {
|
|
1263
|
+
// This example demonstrates how to implement the Generic Repository pattern
|
|
1264
|
+
// where a generic EntityManager needs to know WHICH repository to use.
|
|
1265
|
+
|
|
910
1266
|
interface IRepository {
|
|
911
1267
|
name: string;
|
|
912
1268
|
}
|
|
913
1269
|
|
|
914
|
-
|
|
915
|
-
const
|
|
1270
|
+
// Tokens for specific repository types
|
|
1271
|
+
const UserRepositoryToken = new SingleToken<IRepository>('UserRepository');
|
|
1272
|
+
const TodoRepositoryToken = new SingleToken<IRepository>('TodoRepository');
|
|
916
1273
|
|
|
917
|
-
@register(bindTo(
|
|
1274
|
+
@register(bindTo(UserRepositoryToken))
|
|
918
1275
|
class UserRepository implements IRepository {
|
|
919
1276
|
name = 'UserRepository';
|
|
920
1277
|
}
|
|
921
1278
|
|
|
922
|
-
@register(bindTo(
|
|
1279
|
+
@register(bindTo(TodoRepositoryToken))
|
|
923
1280
|
class TodoRepository implements IRepository {
|
|
924
1281
|
name = 'TodoRepository';
|
|
925
1282
|
}
|
|
926
1283
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
const IEntityManagerKey = new SingleToken<IEntityManager>('IEntityManager');
|
|
1284
|
+
// EntityManager is generic - it works with ANY repository
|
|
1285
|
+
// We use argsFn(resolveByArgs) to tell it to look at the arguments passed to .args()
|
|
1286
|
+
const EntityManagerToken = new SingleToken<EntityManager>('EntityManager');
|
|
932
1287
|
|
|
933
|
-
@register(
|
|
1288
|
+
@register(
|
|
1289
|
+
bindTo(EntityManagerToken),
|
|
1290
|
+
argsFn(resolveByArgs), // <--- Key magic: resolves dependencies based on arguments passed to token
|
|
1291
|
+
singleton(MultiCache.fromFirstArg), // Cache unique instance per repository type
|
|
1292
|
+
)
|
|
934
1293
|
class EntityManager {
|
|
935
1294
|
constructor(public repository: IRepository) {}
|
|
936
1295
|
}
|
|
937
1296
|
|
|
938
|
-
class
|
|
1297
|
+
class App {
|
|
939
1298
|
constructor(
|
|
940
|
-
|
|
941
|
-
@inject(
|
|
942
|
-
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
const root = createContainer()
|
|
946
|
-
.addRegistration(R.fromClass(EntityManager))
|
|
947
|
-
.addRegistration(R.fromClass(UserRepository))
|
|
948
|
-
.addRegistration(R.fromClass(TodoRepository));
|
|
949
|
-
const main = root.resolve(Main);
|
|
950
|
-
|
|
951
|
-
expect(main.userEntities.repository).toBeInstanceOf(UserRepository);
|
|
952
|
-
expect(main.todoEntities.repository).toBeInstanceOf(TodoRepository);
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
it('should resolve memoized dependency by passing arguments resolve from container by another argument', function () {
|
|
956
|
-
interface IRepository {
|
|
957
|
-
name: string;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
const IUserRepositoryKey = new SingleToken<IRepository>('IUserRepository');
|
|
961
|
-
const ITodoRepositoryKey = new SingleToken<IRepository>('ITodoRepository');
|
|
1299
|
+
// Inject EntityManager configured for Users
|
|
1300
|
+
@inject(EntityManagerToken.args(UserRepositoryToken))
|
|
1301
|
+
public userManager: EntityManager,
|
|
962
1302
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1303
|
+
// Inject EntityManager configured for Todos
|
|
1304
|
+
@inject(EntityManagerToken.args(TodoRepositoryToken))
|
|
1305
|
+
public todoManager: EntityManager,
|
|
1306
|
+
) {}
|
|
966
1307
|
}
|
|
967
1308
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1309
|
+
it('should create specialized instances based on token arguments', function () {
|
|
1310
|
+
const root = createContainer()
|
|
1311
|
+
.addRegistration(R.fromClass(EntityManager))
|
|
1312
|
+
.addRegistration(R.fromClass(UserRepository))
|
|
1313
|
+
.addRegistration(R.fromClass(TodoRepository));
|
|
972
1314
|
|
|
973
|
-
|
|
974
|
-
repository: IRepository;
|
|
975
|
-
}
|
|
1315
|
+
const app = root.resolve(App);
|
|
976
1316
|
|
|
977
|
-
|
|
1317
|
+
expect(app.userManager.repository).toBeInstanceOf(UserRepository);
|
|
1318
|
+
expect(app.todoManager.repository).toBeInstanceOf(TodoRepository);
|
|
1319
|
+
});
|
|
978
1320
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1321
|
+
it('should cache specialized instances separately', function () {
|
|
1322
|
+
const root = createContainer()
|
|
1323
|
+
.addRegistration(R.fromClass(EntityManager))
|
|
1324
|
+
.addRegistration(R.fromClass(UserRepository))
|
|
1325
|
+
.addRegistration(R.fromClass(TodoRepository));
|
|
983
1326
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
@inject(IEntityManagerKey.args(ITodoRepositoryKey)) public todoEntities: EntityManager,
|
|
988
|
-
) {}
|
|
989
|
-
}
|
|
1327
|
+
// Resolve user manager twice
|
|
1328
|
+
const userManager1 = EntityManagerToken.args(UserRepositoryToken).resolve(root);
|
|
1329
|
+
const userManager2 = EntityManagerToken.args(UserRepositoryToken).resolve(root);
|
|
990
1330
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
.addRegistration(R.fromClass(UserRepository))
|
|
994
|
-
.addRegistration(R.fromClass(TodoRepository));
|
|
995
|
-
const main = root.resolve(Main);
|
|
1331
|
+
// Should be same instance (cached)
|
|
1332
|
+
expect(userManager1).toBe(userManager2);
|
|
996
1333
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
expect(main.userEntities.repository).toBe(userRepository);
|
|
1334
|
+
// Resolve todo manager
|
|
1335
|
+
const todoManager = EntityManagerToken.args(TodoRepositoryToken).resolve(root);
|
|
1000
1336
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1337
|
+
// Should be different from user manager
|
|
1338
|
+
expect(todoManager).not.toBe(userManager1);
|
|
1339
|
+
});
|
|
1004
1340
|
});
|
|
1005
1341
|
});
|
|
1006
1342
|
|
|
@@ -1012,6 +1348,7 @@ Sometimes you want to hide dependency if somebody wants to resolve it from certa
|
|
|
1012
1348
|
- `Provider.fromClass(Logger).pipe(scopeAccess(({ invocationScope, providerScope }) => invocationScope === providerScope))`
|
|
1013
1349
|
|
|
1014
1350
|
```typescript
|
|
1351
|
+
import 'reflect-metadata';
|
|
1015
1352
|
import {
|
|
1016
1353
|
bindTo,
|
|
1017
1354
|
Container,
|
|
@@ -1023,22 +1360,79 @@ import {
|
|
|
1023
1360
|
singleton,
|
|
1024
1361
|
} from 'ts-ioc-container';
|
|
1025
1362
|
|
|
1363
|
+
/**
|
|
1364
|
+
* User Management Domain - Visibility Control
|
|
1365
|
+
*
|
|
1366
|
+
* Some services should only be accessible in specific scopes:
|
|
1367
|
+
* - AdminService: Only accessible in admin routes
|
|
1368
|
+
* - AuditLogger: Only accessible at application level (not per-request)
|
|
1369
|
+
* - DebugService: Only accessible in development environment
|
|
1370
|
+
*
|
|
1371
|
+
* scopeAccess() controls VISIBILITY - whether a registered service
|
|
1372
|
+
* can be resolved from a particular scope.
|
|
1373
|
+
*
|
|
1374
|
+
* This provides security-by-design:
|
|
1375
|
+
* - Prevents accidental access to sensitive services
|
|
1376
|
+
* - Enforces architectural boundaries
|
|
1377
|
+
* - Catches misuse at resolution time (not runtime)
|
|
1378
|
+
*/
|
|
1026
1379
|
describe('Visibility', function () {
|
|
1027
|
-
it('should
|
|
1380
|
+
it('should restrict admin services to admin routes only', () => {
|
|
1381
|
+
// UserManagementService can delete users - admin only!
|
|
1028
1382
|
@register(
|
|
1029
|
-
bindTo('
|
|
1030
|
-
scope((s) => s.hasTag('
|
|
1383
|
+
bindTo('IUserManagement'),
|
|
1384
|
+
scope((s) => s.hasTag('application')), // Registered at app level
|
|
1031
1385
|
singleton(),
|
|
1386
|
+
// Only accessible from admin scope, not regular request scope
|
|
1387
|
+
scopeAccess(({ invocationScope }) => invocationScope.hasTag('admin')),
|
|
1388
|
+
)
|
|
1389
|
+
class UserManagementService {
|
|
1390
|
+
deleteUser(userId: string): string {
|
|
1391
|
+
return `Deleted user ${userId}`;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(UserManagementService));
|
|
1396
|
+
|
|
1397
|
+
// Admin route scope
|
|
1398
|
+
const adminScope = appContainer.createScope({ tags: ['request', 'admin'] });
|
|
1399
|
+
|
|
1400
|
+
// Regular user route scope
|
|
1401
|
+
const userScope = appContainer.createScope({ tags: ['request', 'user'] });
|
|
1402
|
+
|
|
1403
|
+
// Admin can access UserManagementService
|
|
1404
|
+
const adminService = adminScope.resolve<UserManagementService>('IUserManagement');
|
|
1405
|
+
expect(adminService.deleteUser('user-123')).toBe('Deleted user user-123');
|
|
1406
|
+
|
|
1407
|
+
// Regular users cannot access it - security enforced at DI level
|
|
1408
|
+
expect(() => userScope.resolve('IUserManagement')).toThrowError(DependencyNotFoundError);
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
it('should restrict application-level services from request scope', () => {
|
|
1412
|
+
// AuditLogger should only be used at application initialization
|
|
1413
|
+
// Not from request handlers (to prevent log corruption from concurrent access)
|
|
1414
|
+
@register(
|
|
1415
|
+
bindTo('IAuditLogger'),
|
|
1416
|
+
scope((s) => s.hasTag('application')),
|
|
1417
|
+
singleton(),
|
|
1418
|
+
// Only accessible from the scope where it was registered
|
|
1032
1419
|
scopeAccess(({ invocationScope, providerScope }) => invocationScope === providerScope),
|
|
1033
1420
|
)
|
|
1034
|
-
class
|
|
1421
|
+
class AuditLogger {
|
|
1422
|
+
log(message: string): string {
|
|
1423
|
+
return `AUDIT: ${message}`;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1035
1426
|
|
|
1036
|
-
const
|
|
1427
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(AuditLogger));
|
|
1037
1428
|
|
|
1038
|
-
const
|
|
1429
|
+
const requestScope = appContainer.createScope({ tags: ['request'] });
|
|
1039
1430
|
|
|
1040
|
-
|
|
1041
|
-
expect(
|
|
1431
|
+
// Application can use AuditLogger (for startup logging)
|
|
1432
|
+
expect(appContainer.resolve<AuditLogger>('IAuditLogger').log('App started')).toBe('AUDIT: App started');
|
|
1433
|
+
|
|
1434
|
+
// Request handlers cannot access it directly
|
|
1435
|
+
expect(() => requestScope.resolve('IAuditLogger')).toThrowError(DependencyNotFoundError);
|
|
1042
1436
|
});
|
|
1043
1437
|
});
|
|
1044
1438
|
|
|
@@ -1051,6 +1445,7 @@ Alias is needed to group keys
|
|
|
1051
1445
|
- `Provider.fromClass(Logger).pipe(alias('logger'))`
|
|
1052
1446
|
|
|
1053
1447
|
```typescript
|
|
1448
|
+
import 'reflect-metadata';
|
|
1054
1449
|
import {
|
|
1055
1450
|
bindTo,
|
|
1056
1451
|
Container,
|
|
@@ -1062,95 +1457,127 @@ import {
|
|
|
1062
1457
|
select as s,
|
|
1063
1458
|
} from 'ts-ioc-container';
|
|
1064
1459
|
|
|
1460
|
+
/**
|
|
1461
|
+
* User Management Domain - Alias Pattern (Multiple Implementations)
|
|
1462
|
+
*
|
|
1463
|
+
* Aliases allow multiple services to be registered under the same key.
|
|
1464
|
+
* This is useful for:
|
|
1465
|
+
* - Plugin systems (multiple notification channels)
|
|
1466
|
+
* - Strategy pattern (multiple authentication providers)
|
|
1467
|
+
* - Event handlers (multiple listeners for same event)
|
|
1468
|
+
*
|
|
1469
|
+
* Example: NotificationService with Email, SMS, and Push implementations
|
|
1470
|
+
*/
|
|
1065
1471
|
describe('alias', () => {
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1472
|
+
// All notification services share this alias
|
|
1473
|
+
const INotificationChannel = 'INotificationChannel';
|
|
1474
|
+
const notificationChannel = register(bindTo(s.alias(INotificationChannel)));
|
|
1068
1475
|
|
|
1069
|
-
interface
|
|
1070
|
-
|
|
1476
|
+
interface INotificationChannel {
|
|
1477
|
+
send(userId: string, message: string): void;
|
|
1478
|
+
getDeliveredMessages(): string[];
|
|
1071
1479
|
}
|
|
1072
1480
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1481
|
+
// Email notification - always available
|
|
1482
|
+
@notificationChannel
|
|
1483
|
+
class EmailNotifier implements INotificationChannel {
|
|
1484
|
+
private delivered: string[] = [];
|
|
1077
1485
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
applyTo(application: IApplication): void {
|
|
1081
|
-
application.markMiddlewareAsApplied('LoggerMiddleware');
|
|
1486
|
+
send(userId: string, message: string): void {
|
|
1487
|
+
this.delivered.push(`EMAIL to ${userId}: ${message}`);
|
|
1082
1488
|
}
|
|
1083
|
-
}
|
|
1084
1489
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
applyTo(application: IApplication): void {
|
|
1088
|
-
application.markMiddlewareAsApplied('ErrorHandlerMiddleware');
|
|
1490
|
+
getDeliveredMessages(): string[] {
|
|
1491
|
+
return this.delivered;
|
|
1089
1492
|
}
|
|
1090
1493
|
}
|
|
1091
1494
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1495
|
+
// SMS notification - for urgent messages
|
|
1496
|
+
@notificationChannel
|
|
1497
|
+
class SmsNotifier implements INotificationChannel {
|
|
1498
|
+
private delivered: string[] = [];
|
|
1096
1499
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1500
|
+
send(userId: string, message: string): void {
|
|
1501
|
+
this.delivered.push(`SMS to ${userId}: ${message}`);
|
|
1502
|
+
}
|
|
1100
1503
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1504
|
+
getDeliveredMessages(): string[] {
|
|
1505
|
+
return this.delivered;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1104
1508
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1509
|
+
it('should notify through all channels', () => {
|
|
1510
|
+
// NotificationManager broadcasts to ALL registered channels
|
|
1511
|
+
class NotificationManager {
|
|
1512
|
+
constructor(@inject(s.alias(INotificationChannel)) private channels: INotificationChannel[]) {}
|
|
1108
1513
|
|
|
1109
|
-
|
|
1110
|
-
for (const
|
|
1111
|
-
|
|
1514
|
+
notifyUser(userId: string, message: string): void {
|
|
1515
|
+
for (const channel of this.channels) {
|
|
1516
|
+
channel.send(userId, message);
|
|
1112
1517
|
}
|
|
1113
1518
|
}
|
|
1519
|
+
|
|
1520
|
+
getChannelCount(): number {
|
|
1521
|
+
return this.channels.length;
|
|
1522
|
+
}
|
|
1114
1523
|
}
|
|
1115
1524
|
|
|
1116
1525
|
const container = new Container()
|
|
1117
|
-
.addRegistration(R.fromClass(
|
|
1118
|
-
.addRegistration(R.fromClass(
|
|
1526
|
+
.addRegistration(R.fromClass(EmailNotifier))
|
|
1527
|
+
.addRegistration(R.fromClass(SmsNotifier));
|
|
1119
1528
|
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1529
|
+
const manager = container.resolve(NotificationManager);
|
|
1530
|
+
manager.notifyUser('user-123', 'Your password was reset');
|
|
1122
1531
|
|
|
1123
|
-
|
|
1124
|
-
expect(
|
|
1532
|
+
// Both channels received the message
|
|
1533
|
+
expect(manager.getChannelCount()).toBe(2);
|
|
1125
1534
|
});
|
|
1126
1535
|
|
|
1127
|
-
it('should resolve by
|
|
1128
|
-
|
|
1129
|
-
|
|
1536
|
+
it('should resolve single implementation by alias', () => {
|
|
1537
|
+
// Sometimes you only need one implementation (e.g., primary email service)
|
|
1538
|
+
@register(bindTo(s.alias('IPrimaryNotifier')))
|
|
1539
|
+
class PrimaryEmailNotifier {
|
|
1540
|
+
readonly type = 'email';
|
|
1541
|
+
}
|
|
1130
1542
|
|
|
1131
|
-
const container = new Container().addRegistration(R.fromClass(
|
|
1543
|
+
const container = new Container().addRegistration(R.fromClass(PrimaryEmailNotifier));
|
|
1132
1544
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1545
|
+
// resolveOneByAlias returns first matching implementation
|
|
1546
|
+
const notifier = container.resolveOneByAlias<PrimaryEmailNotifier>('IPrimaryNotifier');
|
|
1547
|
+
expect(notifier.type).toBe('email');
|
|
1548
|
+
|
|
1549
|
+
// Direct key resolution fails - only alias is registered
|
|
1550
|
+
expect(() => container.resolve('IPrimaryNotifier')).toThrowError(DependencyNotFoundError);
|
|
1135
1551
|
});
|
|
1136
1552
|
|
|
1137
|
-
it('should
|
|
1138
|
-
|
|
1139
|
-
|
|
1553
|
+
it('should use different implementations per scope', () => {
|
|
1554
|
+
// Development: Console logger for easy debugging
|
|
1555
|
+
@register(bindTo(s.alias('ILogger')), scope((s) => s.hasTag('development')))
|
|
1556
|
+
class ConsoleLogger {
|
|
1557
|
+
readonly type = 'console';
|
|
1558
|
+
}
|
|
1140
1559
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1560
|
+
// Production: Database logger for audit trail
|
|
1561
|
+
@register(bindTo(s.alias('ILogger')), scope((s) => s.hasTag('production')))
|
|
1562
|
+
class DatabaseLogger {
|
|
1563
|
+
readonly type = 'database';
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// Development environment
|
|
1567
|
+
const devContainer = new Container({ tags: ['development'] })
|
|
1568
|
+
.addRegistration(R.fromClass(ConsoleLogger))
|
|
1569
|
+
.addRegistration(R.fromClass(DatabaseLogger));
|
|
1143
1570
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
.addRegistration(R.fromClass(
|
|
1571
|
+
// Production environment
|
|
1572
|
+
const prodContainer = new Container({ tags: ['production'] })
|
|
1573
|
+
.addRegistration(R.fromClass(ConsoleLogger))
|
|
1574
|
+
.addRegistration(R.fromClass(DatabaseLogger));
|
|
1147
1575
|
|
|
1148
|
-
const
|
|
1149
|
-
const
|
|
1150
|
-
const result2 = child.resolveOneByAlias('ILogger');
|
|
1576
|
+
const devLogger = devContainer.resolveOneByAlias<ConsoleLogger | DatabaseLogger>('ILogger');
|
|
1577
|
+
const prodLogger = prodContainer.resolveOneByAlias<ConsoleLogger | DatabaseLogger>('ILogger');
|
|
1151
1578
|
|
|
1152
|
-
expect(
|
|
1153
|
-
expect(
|
|
1579
|
+
expect(devLogger.type).toBe('console');
|
|
1580
|
+
expect(prodLogger.type).toBe('database');
|
|
1154
1581
|
});
|
|
1155
1582
|
});
|
|
1156
1583
|
|
|
@@ -1173,7 +1600,23 @@ import {
|
|
|
1173
1600
|
singleton,
|
|
1174
1601
|
} from 'ts-ioc-container';
|
|
1175
1602
|
|
|
1176
|
-
|
|
1603
|
+
/**
|
|
1604
|
+
* User Management Domain - Decorator Pattern
|
|
1605
|
+
*
|
|
1606
|
+
* The decorator pattern wraps a service with additional behavior:
|
|
1607
|
+
* - Logging: Log all repository operations for audit
|
|
1608
|
+
* - Caching: Cache results of expensive operations
|
|
1609
|
+
* - Retry: Automatically retry failed operations
|
|
1610
|
+
* - Validation: Validate inputs before processing
|
|
1611
|
+
*
|
|
1612
|
+
* In DI, decorators are applied at registration time, so consumers
|
|
1613
|
+
* get the decorated version without knowing about the decoration.
|
|
1614
|
+
*
|
|
1615
|
+
* This example shows a TodoRepository decorated with logging -
|
|
1616
|
+
* every save operation is automatically logged.
|
|
1617
|
+
*/
|
|
1618
|
+
describe('Decorator Pattern', () => {
|
|
1619
|
+
// Singleton logger collects all log entries
|
|
1177
1620
|
@register(singleton())
|
|
1178
1621
|
class Logger {
|
|
1179
1622
|
private logs: string[] = [];
|
|
@@ -1196,50 +1639,58 @@ describe('lazy provider', () => {
|
|
|
1196
1639
|
text: string;
|
|
1197
1640
|
}
|
|
1198
1641
|
|
|
1199
|
-
|
|
1642
|
+
// Decorator: Wraps any IRepository with logging behavior
|
|
1643
|
+
class LoggingRepository implements IRepository {
|
|
1200
1644
|
constructor(
|
|
1201
1645
|
private repository: IRepository,
|
|
1202
1646
|
@inject(s.token('Logger').lazy()) private logger: Logger,
|
|
1203
1647
|
) {}
|
|
1204
1648
|
|
|
1205
1649
|
async save(item: Todo): Promise<void> {
|
|
1650
|
+
// Log the operation
|
|
1206
1651
|
this.logger.log(item.id);
|
|
1652
|
+
// Delegate to the wrapped repository
|
|
1207
1653
|
return this.repository.save(item);
|
|
1208
1654
|
}
|
|
1209
1655
|
}
|
|
1210
1656
|
|
|
1211
|
-
|
|
1657
|
+
// Decorator factory - creates LoggingRepository wrapping the original
|
|
1658
|
+
const withLogging = (repository: IRepository, scope: IContainer) =>
|
|
1659
|
+
scope.resolve(LoggingRepository, { args: [repository] });
|
|
1212
1660
|
|
|
1213
|
-
|
|
1661
|
+
// TodoRepository is automatically decorated with logging
|
|
1662
|
+
@register(bindTo('IRepository'), decorate(withLogging))
|
|
1214
1663
|
class TodoRepository implements IRepository {
|
|
1215
|
-
async save(item: Todo): Promise<void> {
|
|
1664
|
+
async save(item: Todo): Promise<void> {
|
|
1665
|
+
// Actual database save logic would go here
|
|
1666
|
+
}
|
|
1216
1667
|
}
|
|
1217
1668
|
|
|
1218
1669
|
class App {
|
|
1219
1670
|
constructor(@inject('IRepository') public repository: IRepository) {}
|
|
1220
1671
|
|
|
1221
1672
|
async run() {
|
|
1222
|
-
await this.repository.save({ id: '1', text: '
|
|
1223
|
-
await this.repository.save({ id: '2', text: '
|
|
1673
|
+
await this.repository.save({ id: '1', text: 'Buy groceries' });
|
|
1674
|
+
await this.repository.save({ id: '2', text: 'Walk the dog' });
|
|
1224
1675
|
}
|
|
1225
1676
|
}
|
|
1226
1677
|
|
|
1227
|
-
function
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1678
|
+
function createAppContainer() {
|
|
1679
|
+
return new Container({ tags: ['application'] })
|
|
1680
|
+
.addRegistration(R.fromClass(TodoRepository))
|
|
1681
|
+
.addRegistration(R.fromClass(Logger));
|
|
1231
1682
|
}
|
|
1232
1683
|
|
|
1233
|
-
it('should
|
|
1234
|
-
|
|
1235
|
-
const container = createContainer();
|
|
1684
|
+
it('should automatically log all repository operations via decorator', async () => {
|
|
1685
|
+
const container = createAppContainer();
|
|
1236
1686
|
|
|
1237
|
-
// Act
|
|
1238
1687
|
const app = container.resolve(App);
|
|
1239
1688
|
const logger = container.resolve<Logger>('Logger');
|
|
1689
|
+
|
|
1690
|
+
// App uses repository normally - unaware of logging decorator
|
|
1240
1691
|
await app.run();
|
|
1241
1692
|
|
|
1242
|
-
//
|
|
1693
|
+
// All operations were logged transparently
|
|
1243
1694
|
expect(logger.printLogs()).toBe('1,2');
|
|
1244
1695
|
});
|
|
1245
1696
|
});
|
|
@@ -1272,52 +1723,75 @@ import {
|
|
|
1272
1723
|
singleton,
|
|
1273
1724
|
} from 'ts-ioc-container';
|
|
1274
1725
|
|
|
1726
|
+
/**
|
|
1727
|
+
* User Management Domain - Registration Patterns
|
|
1728
|
+
*
|
|
1729
|
+
* Registrations define how dependencies are bound to the container.
|
|
1730
|
+
* Common patterns:
|
|
1731
|
+
* - Register by class (auto-generates key from class name)
|
|
1732
|
+
* - Register by value (constants, configuration)
|
|
1733
|
+
* - Register by factory function (dynamic creation)
|
|
1734
|
+
* - Register with aliases (multiple keys for same service)
|
|
1735
|
+
*
|
|
1736
|
+
* This is the foundation for dependency injection - telling the container
|
|
1737
|
+
* "when someone asks for X, give them Y".
|
|
1738
|
+
*/
|
|
1275
1739
|
describe('Registration module', function () {
|
|
1276
|
-
const
|
|
1740
|
+
const createAppContainer = () => new Container({ tags: ['application'] });
|
|
1277
1741
|
|
|
1278
|
-
it('should register class', function () {
|
|
1279
|
-
|
|
1742
|
+
it('should register class with scope and lifecycle', function () {
|
|
1743
|
+
// Logger is registered at application scope as a singleton
|
|
1744
|
+
@register(bindTo('ILogger'), scope((s) => s.hasTag('application')), singleton())
|
|
1280
1745
|
class Logger {}
|
|
1281
1746
|
|
|
1282
|
-
const
|
|
1747
|
+
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
|
|
1283
1748
|
|
|
1284
|
-
expect(
|
|
1749
|
+
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
|
|
1285
1750
|
});
|
|
1286
1751
|
|
|
1287
|
-
it('should register value', function () {
|
|
1288
|
-
|
|
1752
|
+
it('should register configuration value', function () {
|
|
1753
|
+
// Register application configuration as a value
|
|
1754
|
+
const appContainer = createAppContainer().addRegistration(R.fromValue('production').bindToKey('Environment'));
|
|
1289
1755
|
|
|
1290
|
-
expect(
|
|
1756
|
+
expect(appContainer.resolve('Environment')).toBe('production');
|
|
1291
1757
|
});
|
|
1292
1758
|
|
|
1293
|
-
it('should register
|
|
1294
|
-
|
|
1759
|
+
it('should register factory function', function () {
|
|
1760
|
+
// Factory functions are useful for dynamic creation
|
|
1761
|
+
const appContainer = createAppContainer().addRegistration(
|
|
1762
|
+
R.fromFn(() => `app-${Date.now()}`).bindToKey('RequestId'),
|
|
1763
|
+
);
|
|
1295
1764
|
|
|
1296
|
-
expect(
|
|
1765
|
+
expect(appContainer.resolve('RequestId')).toContain('app-');
|
|
1297
1766
|
});
|
|
1298
1767
|
|
|
1299
|
-
it('should raise an error if key is not
|
|
1768
|
+
it('should raise an error if binding key is not provided', () => {
|
|
1769
|
+
// Values and functions must have explicit keys (classes use class name by default)
|
|
1300
1770
|
expect(() => {
|
|
1301
|
-
|
|
1771
|
+
createAppContainer().addRegistration(R.fromValue('orphan-value'));
|
|
1302
1772
|
}).toThrowError(DependencyMissingKeyError);
|
|
1303
1773
|
});
|
|
1304
1774
|
|
|
1305
|
-
it('should register dependency by class name
|
|
1775
|
+
it('should register dependency by class name when no key decorator is used', function () {
|
|
1776
|
+
// Without @register(bindTo('key')), the class name becomes the key
|
|
1306
1777
|
class FileLogger {}
|
|
1307
1778
|
|
|
1308
|
-
const
|
|
1779
|
+
const appContainer = createAppContainer().addRegistration(R.fromClass(FileLogger));
|
|
1309
1780
|
|
|
1310
|
-
expect(
|
|
1781
|
+
expect(appContainer.resolve('FileLogger')).toBeInstanceOf(FileLogger);
|
|
1311
1782
|
});
|
|
1312
1783
|
|
|
1313
|
-
it('should
|
|
1784
|
+
it('should register with multiple keys using aliases', function () {
|
|
1785
|
+
// Same service accessible via direct key and alias
|
|
1314
1786
|
@register(bindTo('ILogger'), bindTo(s.alias('Logger')), singleton())
|
|
1315
1787
|
class Logger {}
|
|
1316
1788
|
|
|
1317
|
-
const
|
|
1789
|
+
const appContainer = createAppContainer().addRegistration(R.fromClass(Logger));
|
|
1318
1790
|
|
|
1319
|
-
|
|
1320
|
-
expect(
|
|
1791
|
+
// Accessible via alias (for group resolution)
|
|
1792
|
+
expect(appContainer.resolveByAlias('Logger')[0]).toBeInstanceOf(Logger);
|
|
1793
|
+
// Accessible via direct key
|
|
1794
|
+
expect(appContainer.resolve('ILogger')).toBeInstanceOf(Logger);
|
|
1321
1795
|
});
|
|
1322
1796
|
});
|
|
1323
1797
|
|
|
@@ -1331,13 +1805,35 @@ Sometimes you need to register provider only in scope which matches to certain c
|
|
|
1331
1805
|
```typescript
|
|
1332
1806
|
import { bindTo, Container, register, Registration as R, scope, singleton } from 'ts-ioc-container';
|
|
1333
1807
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1808
|
+
/**
|
|
1809
|
+
* Scoping - Scope Match Rule
|
|
1810
|
+
*
|
|
1811
|
+
* You can restrict WHERE a provider is registered.
|
|
1812
|
+
* This is useful for singleton services that should only exist in the root scope,
|
|
1813
|
+
* or per-request services that should only exist in request scopes.
|
|
1814
|
+
*/
|
|
1815
|
+
|
|
1336
1816
|
describe('ScopeProvider', function () {
|
|
1337
|
-
it('should
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1817
|
+
it('should register provider only in matching scope', function () {
|
|
1818
|
+
// SharedState should be a singleton in the root 'application' scope
|
|
1819
|
+
// It will be visible to all child scopes, but physically resides in 'application'
|
|
1820
|
+
@register(
|
|
1821
|
+
bindTo('SharedState'),
|
|
1822
|
+
scope((s) => s.hasTag('application')), // Only register in application scope
|
|
1823
|
+
singleton(), // One instance per application
|
|
1824
|
+
)
|
|
1825
|
+
class SharedState {
|
|
1826
|
+
data = 'shared';
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
const appContainer = new Container({ tags: ['application'] }).addRegistration(R.fromClass(SharedState));
|
|
1830
|
+
const requestScope = appContainer.createScope({ tags: ['request'] });
|
|
1831
|
+
|
|
1832
|
+
// Both resolve to the SAME instance because it's a singleton in the app scope
|
|
1833
|
+
const appState = appContainer.resolve('SharedState');
|
|
1834
|
+
const requestState = requestScope.resolve('SharedState');
|
|
1835
|
+
|
|
1836
|
+
expect(appState).toBe(requestState);
|
|
1341
1837
|
});
|
|
1342
1838
|
});
|
|
1343
1839
|
|
|
@@ -1347,41 +1843,120 @@ describe('ScopeProvider', function () {
|
|
|
1347
1843
|
Sometimes you want to encapsulate registration logic in separate module. This is what `IContainerModule` is for.
|
|
1348
1844
|
|
|
1349
1845
|
```typescript
|
|
1846
|
+
import 'reflect-metadata';
|
|
1350
1847
|
import { bindTo, Container, type IContainer, type IContainerModule, register, Registration as R } from 'ts-ioc-container';
|
|
1351
1848
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1849
|
+
/**
|
|
1850
|
+
* User Management Domain - Container Modules
|
|
1851
|
+
*
|
|
1852
|
+
* Modules organize related registrations and allow swapping implementations
|
|
1853
|
+
* based on environment (development, testing, production).
|
|
1854
|
+
*
|
|
1855
|
+
* Common module patterns:
|
|
1856
|
+
* - ProductionModule: Real database, external APIs, email service
|
|
1857
|
+
* - DevelopmentModule: In-memory database, mock APIs, console logging
|
|
1858
|
+
* - TestingModule: Mocks with assertion capabilities
|
|
1859
|
+
*
|
|
1860
|
+
* This enables:
|
|
1861
|
+
* - Easy environment switching
|
|
1862
|
+
* - Isolated testing without external dependencies
|
|
1863
|
+
* - Feature flags via module composition
|
|
1864
|
+
*/
|
|
1865
|
+
|
|
1866
|
+
// Auth service interface - same API for all environments
|
|
1867
|
+
interface IAuthService {
|
|
1868
|
+
authenticate(email: string, password: string): boolean;
|
|
1869
|
+
getServiceType(): string;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// Production: Real authentication with database lookup
|
|
1873
|
+
@register(bindTo('IAuthService'))
|
|
1874
|
+
class ProductionAuthService implements IAuthService {
|
|
1875
|
+
authenticate(email: string, password: string): boolean {
|
|
1876
|
+
// In production, this would query the database
|
|
1877
|
+
return email === 'admin@example.com' && password === 'secure_password';
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
getServiceType(): string {
|
|
1881
|
+
return 'production';
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// Development: Accepts any credentials for easy testing
|
|
1886
|
+
@register(bindTo('IAuthService'))
|
|
1887
|
+
class DevelopmentAuthService implements IAuthService {
|
|
1888
|
+
authenticate(_email: string, _password: string): boolean {
|
|
1889
|
+
// Always succeed in development for easier testing
|
|
1890
|
+
return true;
|
|
1891
|
+
}
|
|
1354
1892
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1893
|
+
getServiceType(): string {
|
|
1894
|
+
return 'development';
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1357
1897
|
|
|
1358
|
-
|
|
1898
|
+
// Production module - real services with security
|
|
1899
|
+
class ProductionModule implements IContainerModule {
|
|
1359
1900
|
applyTo(container: IContainer): void {
|
|
1360
|
-
container.addRegistration(R.fromClass(
|
|
1901
|
+
container.addRegistration(R.fromClass(ProductionAuthService));
|
|
1902
|
+
// In a real app, also register:
|
|
1903
|
+
// - Real database connection
|
|
1904
|
+
// - External email service
|
|
1905
|
+
// - Payment gateway
|
|
1361
1906
|
}
|
|
1362
1907
|
}
|
|
1363
1908
|
|
|
1364
|
-
|
|
1909
|
+
// Development module - mocks and conveniences
|
|
1910
|
+
class DevelopmentModule implements IContainerModule {
|
|
1365
1911
|
applyTo(container: IContainer): void {
|
|
1366
|
-
container.addRegistration(R.fromClass(
|
|
1912
|
+
container.addRegistration(R.fromClass(DevelopmentAuthService));
|
|
1913
|
+
// In a real app, also register:
|
|
1914
|
+
// - In-memory database
|
|
1915
|
+
// - Console email logger
|
|
1916
|
+
// - Mock payment gateway
|
|
1367
1917
|
}
|
|
1368
1918
|
}
|
|
1369
1919
|
|
|
1370
1920
|
describe('Container Modules', function () {
|
|
1371
1921
|
function createContainer(isProduction: boolean) {
|
|
1372
|
-
|
|
1922
|
+
const module = isProduction ? new ProductionModule() : new DevelopmentModule();
|
|
1923
|
+
return new Container().useModule(module);
|
|
1373
1924
|
}
|
|
1374
1925
|
|
|
1375
|
-
it('should
|
|
1926
|
+
it('should use production auth with strict validation', function () {
|
|
1376
1927
|
const container = createContainer(true);
|
|
1928
|
+
const auth = container.resolve<IAuthService>('IAuthService');
|
|
1377
1929
|
|
|
1378
|
-
expect(
|
|
1930
|
+
expect(auth.getServiceType()).toBe('production');
|
|
1931
|
+
expect(auth.authenticate('admin@example.com', 'secure_password')).toBe(true);
|
|
1932
|
+
expect(auth.authenticate('admin@example.com', 'wrong_password')).toBe(false);
|
|
1379
1933
|
});
|
|
1380
1934
|
|
|
1381
|
-
it('should
|
|
1935
|
+
it('should use development auth with permissive validation', function () {
|
|
1382
1936
|
const container = createContainer(false);
|
|
1937
|
+
const auth = container.resolve<IAuthService>('IAuthService');
|
|
1938
|
+
|
|
1939
|
+
expect(auth.getServiceType()).toBe('development');
|
|
1940
|
+
// Development mode accepts any credentials
|
|
1941
|
+
expect(auth.authenticate('any@email.com', 'any_password')).toBe(true);
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
it('should allow composing multiple modules', function () {
|
|
1945
|
+
// Modules can be composed for feature flags or A/B testing
|
|
1946
|
+
class FeatureFlagModule implements IContainerModule {
|
|
1947
|
+
constructor(private enableNewFeature: boolean) {}
|
|
1383
1948
|
|
|
1384
|
-
|
|
1949
|
+
applyTo(container: IContainer): void {
|
|
1950
|
+
if (this.enableNewFeature) {
|
|
1951
|
+
// Register new feature implementations
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
const container = new Container().useModule(new ProductionModule()).useModule(new FeatureFlagModule(true));
|
|
1957
|
+
|
|
1958
|
+
// Base services from ProductionModule
|
|
1959
|
+
expect(container.resolve<IAuthService>('IAuthService').getServiceType()).toBe('production');
|
|
1385
1960
|
});
|
|
1386
1961
|
});
|
|
1387
1962
|
|
|
@@ -1402,32 +1977,49 @@ import {
|
|
|
1402
1977
|
Registration as R,
|
|
1403
1978
|
} from 'ts-ioc-container';
|
|
1404
1979
|
|
|
1980
|
+
/**
|
|
1981
|
+
* Lifecycle - OnConstruct Hook
|
|
1982
|
+
*
|
|
1983
|
+
* The @onConstruct hook allows you to run logic immediately after an object is created.
|
|
1984
|
+
* This is useful for:
|
|
1985
|
+
* - Initialization logic that depends on injected services
|
|
1986
|
+
* - Setting up event listeners
|
|
1987
|
+
* - Establishing connections (though lazy is often better)
|
|
1988
|
+
* - Computing initial state
|
|
1989
|
+
*
|
|
1990
|
+
* Note: You must register the AddOnConstructHookModule or manually add the hook runner.
|
|
1991
|
+
*/
|
|
1992
|
+
|
|
1405
1993
|
const execute: HookFn = (ctx: HookContext) => {
|
|
1406
1994
|
ctx.invokeMethod({ args: ctx.resolveArgs() });
|
|
1407
1995
|
};
|
|
1408
1996
|
|
|
1409
|
-
class Car {
|
|
1410
|
-
private engine!: string;
|
|
1411
|
-
|
|
1412
|
-
@onConstruct(execute)
|
|
1413
|
-
setEngine(@inject('engine') engine: string) {
|
|
1414
|
-
this.engine = engine;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
getEngine() {
|
|
1418
|
-
return this.engine;
|
|
1419
|
-
}
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
1997
|
describe('onConstruct', function () {
|
|
1423
|
-
it('should run
|
|
1424
|
-
|
|
1998
|
+
it('should run initialization method after dependencies are resolved', function () {
|
|
1999
|
+
class DatabaseConnection {
|
|
2000
|
+
isConnected = false;
|
|
2001
|
+
connectionString = '';
|
|
2002
|
+
|
|
2003
|
+
// @onConstruct marks this method to be called after instantiation
|
|
2004
|
+
// Arguments are resolved from the container like constructor params
|
|
2005
|
+
@onConstruct(execute)
|
|
2006
|
+
connect(@inject('ConnectionString') connectionString: string) {
|
|
2007
|
+
this.connectionString = connectionString;
|
|
2008
|
+
this.isConnected = true;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
const container = new Container()
|
|
2013
|
+
// Enable @onConstruct support
|
|
1425
2014
|
.useModule(new AddOnConstructHookModule())
|
|
1426
|
-
|
|
2015
|
+
// Register config
|
|
2016
|
+
.addRegistration(R.fromValue('postgres://localhost:5432').bindTo('ConnectionString'));
|
|
1427
2017
|
|
|
1428
|
-
|
|
2018
|
+
// Resolve class - constructor is called, then @onConstruct method
|
|
2019
|
+
const db = container.resolve(DatabaseConnection);
|
|
1429
2020
|
|
|
1430
|
-
expect(
|
|
2021
|
+
expect(db.isConnected).toBe(true);
|
|
2022
|
+
expect(db.connectionString).toBe('postgres://localhost:5432');
|
|
1431
2023
|
});
|
|
1432
2024
|
});
|
|
1433
2025
|
|
|
@@ -1502,22 +2094,45 @@ describe('onDispose', function () {
|
|
|
1502
2094
|
### Inject property
|
|
1503
2095
|
|
|
1504
2096
|
```typescript
|
|
2097
|
+
import 'reflect-metadata';
|
|
1505
2098
|
import { Container, hook, HooksRunner, injectProp, Registration } from 'ts-ioc-container';
|
|
1506
2099
|
|
|
1507
|
-
|
|
2100
|
+
/**
|
|
2101
|
+
* UI Components - Property Injection
|
|
2102
|
+
*
|
|
2103
|
+
* Property injection is useful when you don't control the class instantiation
|
|
2104
|
+
* (like in some UI frameworks, Web Components, or legacy systems) or when
|
|
2105
|
+
* you want to avoid massive constructors in base classes.
|
|
2106
|
+
*
|
|
2107
|
+
* This example demonstrates a ViewModel that gets dependencies injected
|
|
2108
|
+
* AFTER construction via an initialization hook.
|
|
2109
|
+
*/
|
|
2110
|
+
|
|
1508
2111
|
describe('inject property', () => {
|
|
1509
2112
|
it('should inject property', () => {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
2113
|
+
// Runner for the 'onInit' lifecycle hook
|
|
2114
|
+
const onInitHookRunner = new HooksRunner('onInit');
|
|
2115
|
+
|
|
2116
|
+
class UserViewModel {
|
|
2117
|
+
// Inject 'GreetingService' into 'greeting' property during 'onInit'
|
|
2118
|
+
@hook('onInit', injectProp('GreetingService'))
|
|
2119
|
+
greetingService!: string;
|
|
2120
|
+
|
|
2121
|
+
display(): string {
|
|
2122
|
+
return `${this.greetingService} User`;
|
|
2123
|
+
}
|
|
1513
2124
|
}
|
|
1514
|
-
const expected = 'Hello world!';
|
|
1515
2125
|
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
|
|
2126
|
+
const container = new Container().addRegistration(Registration.fromValue('Hello').bindToKey('GreetingService'));
|
|
2127
|
+
|
|
2128
|
+
// 1. Create instance (dependencies not yet injected)
|
|
2129
|
+
const viewModel = container.resolve(UserViewModel);
|
|
2130
|
+
|
|
2131
|
+
// 2. Run lifecycle hooks to inject properties
|
|
2132
|
+
onInitHookRunner.execute(viewModel, { scope: container });
|
|
1519
2133
|
|
|
1520
|
-
expect(
|
|
2134
|
+
expect(viewModel.greetingService).toBe('Hello');
|
|
2135
|
+
expect(viewModel.display()).toBe('Hello User');
|
|
1521
2136
|
});
|
|
1522
2137
|
});
|
|
1523
2138
|
|