decorator-dependency-injection 1.0.7 → 1.1.0
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 +87 -19
- package/docs/FRAMEWORK_INTEGRATION.md +808 -0
- package/eslint.config.js +4 -1
- package/index.d.ts +6 -355
- package/index.js +46 -378
- package/package.json +6 -1
- package/src/Container.js +48 -201
- package/src/integrations/middleware.d.ts +40 -0
- package/src/integrations/middleware.js +171 -0
package/src/Container.js
CHANGED
|
@@ -17,48 +17,27 @@ export class Container {
|
|
|
17
17
|
/** @type {Map<string|Function, InstanceContext>} */
|
|
18
18
|
#instances = new Map()
|
|
19
19
|
|
|
20
|
-
/** @type {boolean} Enable debug logging */
|
|
21
20
|
#debug = false
|
|
22
21
|
|
|
23
|
-
/**
|
|
24
|
-
* Custom string tag for better debugging.
|
|
25
|
-
* Shows as [object Container] in console.
|
|
26
|
-
*/
|
|
27
22
|
get [Symbol.toStringTag]() {
|
|
28
23
|
return 'Container'
|
|
29
24
|
}
|
|
30
25
|
|
|
31
|
-
/**
|
|
32
|
-
* Make the container iterable.
|
|
33
|
-
* Yields registration info for each registered class.
|
|
34
|
-
* @yields {RegistrationInfo}
|
|
35
|
-
*/
|
|
26
|
+
/** @yields {RegistrationInfo} */
|
|
36
27
|
*[Symbol.iterator]() {
|
|
37
28
|
yield* this.list()
|
|
38
29
|
}
|
|
39
30
|
|
|
40
|
-
/**
|
|
41
|
-
* Get the number of registrations in the container.
|
|
42
|
-
* @returns {number}
|
|
43
|
-
*/
|
|
31
|
+
/** @returns {number} */
|
|
44
32
|
get size() {
|
|
45
33
|
return this.#instances.size
|
|
46
34
|
}
|
|
47
35
|
|
|
48
|
-
/**
|
|
49
|
-
* Enable or disable debug logging.
|
|
50
|
-
* When enabled, logs when instances are created.
|
|
51
|
-
* @param {boolean} enabled Whether to enable debug mode
|
|
52
|
-
*/
|
|
36
|
+
/** @param {boolean} enabled */
|
|
53
37
|
setDebug(enabled) {
|
|
54
38
|
this.#debug = enabled
|
|
55
39
|
}
|
|
56
40
|
|
|
57
|
-
/**
|
|
58
|
-
* Log a debug message if debug mode is enabled.
|
|
59
|
-
* @param {string} message The message to log
|
|
60
|
-
* @private
|
|
61
|
-
*/
|
|
62
41
|
#log(message) {
|
|
63
42
|
if (this.#debug) {
|
|
64
43
|
console.log(`[DI] ${message}`)
|
|
@@ -66,30 +45,21 @@ export class Container {
|
|
|
66
45
|
}
|
|
67
46
|
|
|
68
47
|
/**
|
|
69
|
-
*
|
|
70
|
-
* @param {
|
|
71
|
-
* @param {string} [name] Optional name key
|
|
48
|
+
* @param {Function} clazz
|
|
49
|
+
* @param {string} [name]
|
|
72
50
|
*/
|
|
73
51
|
registerSingleton(clazz, name) {
|
|
74
52
|
this.#register(clazz, 'singleton', name)
|
|
75
53
|
}
|
|
76
54
|
|
|
77
55
|
/**
|
|
78
|
-
*
|
|
79
|
-
* @param {
|
|
80
|
-
* @param {string} [name] Optional name key
|
|
56
|
+
* @param {Function} clazz
|
|
57
|
+
* @param {string} [name]
|
|
81
58
|
*/
|
|
82
59
|
registerFactory(clazz, name) {
|
|
83
60
|
this.#register(clazz, 'factory', name)
|
|
84
61
|
}
|
|
85
62
|
|
|
86
|
-
/**
|
|
87
|
-
* Internal registration logic.
|
|
88
|
-
* @param {Function} clazz The class constructor
|
|
89
|
-
* @param {'singleton'|'factory'} type The registration type
|
|
90
|
-
* @param {string} [name] Optional name key
|
|
91
|
-
* @private
|
|
92
|
-
*/
|
|
93
63
|
#register(clazz, type, name) {
|
|
94
64
|
const key = name ?? clazz
|
|
95
65
|
if (this.#instances.has(key)) {
|
|
@@ -103,68 +73,52 @@ export class Container {
|
|
|
103
73
|
}
|
|
104
74
|
|
|
105
75
|
/**
|
|
106
|
-
*
|
|
107
|
-
* @param {string|Function} clazzOrName The class or name to look up
|
|
76
|
+
* @param {string|Function} clazzOrName
|
|
108
77
|
* @returns {InstanceContext}
|
|
109
|
-
* @throws {Error} If
|
|
78
|
+
* @throws {Error} If not found
|
|
110
79
|
*/
|
|
111
80
|
getContext(clazzOrName) {
|
|
112
81
|
const context = this.#instances.get(clazzOrName)
|
|
113
82
|
if (context) {
|
|
114
83
|
return context
|
|
115
84
|
}
|
|
85
|
+
|
|
86
|
+
const name = clazzOrName?.name ?? clazzOrName
|
|
87
|
+
const nameStr = String(name)
|
|
116
88
|
const available = [...this.#instances.keys()]
|
|
117
89
|
.map(k => typeof k === 'string' ? k : k.name)
|
|
118
90
|
.join(', ')
|
|
119
91
|
|
|
120
|
-
const name = clazzOrName?.name || clazzOrName
|
|
121
|
-
const nameStr = String(name)
|
|
122
|
-
|
|
123
92
|
// Detect if this looks like a mock class from a module mocking system
|
|
124
93
|
const looksLikeMock = /^Mock[A-Z]|mock/i.test(nameStr) ||
|
|
125
94
|
nameStr.includes('Mock') ||
|
|
126
95
|
nameStr.startsWith('vi_') ||
|
|
127
96
|
nameStr.startsWith('jest_')
|
|
128
97
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (looksLikeMock) {
|
|
133
|
-
errorMessage += `\n\nHint: The class name "${name}" suggests this may be a mock created by a module mocking system. ` +
|
|
98
|
+
const hint = looksLikeMock
|
|
99
|
+
? `\n\nHint: The class name "${name}" suggests this may be a mock created by a module mocking system. ` +
|
|
134
100
|
`If you're using module mocking (e.g., vi.mock() or jest.mock()), consider using @Mock(OriginalService) instead, ` +
|
|
135
101
|
`which properly registers with the DI container.`
|
|
136
|
-
|
|
102
|
+
: ''
|
|
137
103
|
|
|
138
|
-
throw new Error(
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Cannot find injection source for "${name}". Available: [${available}]${hint}`
|
|
106
|
+
)
|
|
139
107
|
}
|
|
140
108
|
|
|
141
|
-
/**
|
|
142
|
-
* Check if a class or name is registered.
|
|
143
|
-
* @param {string|Function} clazzOrName The class or name to check
|
|
144
|
-
* @returns {boolean}
|
|
145
|
-
*/
|
|
109
|
+
/** @param {string|Function} clazzOrName */
|
|
146
110
|
has(clazzOrName) {
|
|
147
111
|
return this.#instances.has(clazzOrName)
|
|
148
112
|
}
|
|
149
113
|
|
|
150
|
-
/**
|
|
151
|
-
* Check if a class or name has a mock registered.
|
|
152
|
-
* @param {string|Function} clazzOrName The class or name to check
|
|
153
|
-
* @returns {boolean} true if a mock is registered, false otherwise
|
|
154
|
-
*/
|
|
114
|
+
/** @param {string|Function} clazzOrName */
|
|
155
115
|
isMocked(clazzOrName) {
|
|
156
116
|
return !!this.#instances.get(clazzOrName)?.originalClazz
|
|
157
117
|
}
|
|
158
118
|
|
|
159
119
|
/**
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* @param {string|Function} clazzOrName The class or name to unregister
|
|
164
|
-
* @returns {boolean} true if the registration was removed, false if it wasn't registered
|
|
165
|
-
*
|
|
166
|
-
* @example
|
|
167
|
-
* container.unregister(MyService) // Returns true if was registered
|
|
120
|
+
* @param {string|Function} clazzOrName
|
|
121
|
+
* @returns {boolean} true if removed
|
|
168
122
|
*/
|
|
169
123
|
unregister(clazzOrName) {
|
|
170
124
|
const removed = this.#instances.delete(clazzOrName)
|
|
@@ -174,17 +128,7 @@ export class Container {
|
|
|
174
128
|
return removed
|
|
175
129
|
}
|
|
176
130
|
|
|
177
|
-
/**
|
|
178
|
-
* List all registrations in the container.
|
|
179
|
-
* Useful for debugging and introspection.
|
|
180
|
-
*
|
|
181
|
-
* @returns {Array<{key: string|Function, type: 'singleton'|'factory', isMocked: boolean, hasInstance: boolean}>}
|
|
182
|
-
*
|
|
183
|
-
* @example
|
|
184
|
-
* container.list().forEach(reg => {
|
|
185
|
-
* console.log(`${reg.key}: ${reg.type}, mocked: ${reg.isMocked}`)
|
|
186
|
-
* })
|
|
187
|
-
*/
|
|
131
|
+
/** @returns {Array<{key: string|Function, name: string, type: 'singleton'|'factory', isMocked: boolean, hasInstance: boolean}>} */
|
|
188
132
|
list() {
|
|
189
133
|
return [...this.#instances.entries()].map(([key, context]) => ({
|
|
190
134
|
key,
|
|
@@ -196,13 +140,10 @@ export class Container {
|
|
|
196
140
|
}
|
|
197
141
|
|
|
198
142
|
/**
|
|
199
|
-
* Resolve and return an instance by class or name.
|
|
200
|
-
* This allows non-decorator code to retrieve instances from the container.
|
|
201
143
|
* @template T
|
|
202
|
-
* @param {string|Function} clazzOrName
|
|
203
|
-
* @param {...*} params
|
|
204
|
-
* @returns {T}
|
|
205
|
-
* @throws {Error} If the class or name is not registered
|
|
144
|
+
* @param {string|Function} clazzOrName
|
|
145
|
+
* @param {...*} params
|
|
146
|
+
* @returns {T}
|
|
206
147
|
*/
|
|
207
148
|
resolve(clazzOrName, ...params) {
|
|
208
149
|
const instanceContext = this.getContext(clazzOrName)
|
|
@@ -210,10 +151,9 @@ export class Container {
|
|
|
210
151
|
}
|
|
211
152
|
|
|
212
153
|
/**
|
|
213
|
-
*
|
|
214
|
-
* @param {
|
|
215
|
-
* @
|
|
216
|
-
* @returns {Object} The instance
|
|
154
|
+
* @param {InstanceContext} instanceContext
|
|
155
|
+
* @param {Array} params
|
|
156
|
+
* @returns {Object}
|
|
217
157
|
*/
|
|
218
158
|
getInstance(instanceContext, params) {
|
|
219
159
|
if (instanceContext.type === 'singleton' && instanceContext.instance) {
|
|
@@ -221,14 +161,14 @@ export class Container {
|
|
|
221
161
|
return instanceContext.instance
|
|
222
162
|
}
|
|
223
163
|
|
|
164
|
+
this.#log(`Creating ${instanceContext.type}: ${instanceContext.clazz.name}`)
|
|
224
165
|
let instance
|
|
225
166
|
try {
|
|
226
|
-
this.#log(`Creating ${instanceContext.type}: ${instanceContext.clazz.name}`)
|
|
227
167
|
instance = new instanceContext.clazz(...params)
|
|
228
168
|
} catch (err) {
|
|
229
169
|
if (err instanceof RangeError) {
|
|
230
170
|
throw new Error(
|
|
231
|
-
`Circular dependency detected for ${instanceContext.clazz.name
|
|
171
|
+
`Circular dependency detected for ${instanceContext.clazz.name ?? instanceContext.clazz}. ` +
|
|
232
172
|
`Use @InjectLazy to break the cycle.`
|
|
233
173
|
)
|
|
234
174
|
}
|
|
@@ -248,10 +188,9 @@ export class Container {
|
|
|
248
188
|
}
|
|
249
189
|
|
|
250
190
|
/**
|
|
251
|
-
*
|
|
252
|
-
* @param {
|
|
253
|
-
* @param {
|
|
254
|
-
* @param {boolean} [useProxy=false] Whether to proxy unmocked methods to original
|
|
191
|
+
* @param {string|Function} targetClazzOrName
|
|
192
|
+
* @param {Function} mockClazz
|
|
193
|
+
* @param {boolean} [useProxy=false]
|
|
255
194
|
*/
|
|
256
195
|
registerMock(targetClazzOrName, mockClazz, useProxy = false) {
|
|
257
196
|
const instanceContext = this.getContext(targetClazzOrName)
|
|
@@ -262,48 +201,20 @@ export class Container {
|
|
|
262
201
|
originalClazz: instanceContext.clazz,
|
|
263
202
|
clazz: mockClazz,
|
|
264
203
|
proxy: useProxy,
|
|
265
|
-
instance: undefined
|
|
204
|
+
instance: undefined
|
|
266
205
|
})
|
|
267
206
|
this.#log(`Mocked ${targetClazzOrName?.name ?? targetClazzOrName} with ${mockClazz.name}${useProxy ? ' (proxy)' : ''}`)
|
|
268
207
|
}
|
|
269
208
|
|
|
270
209
|
/**
|
|
271
|
-
* Remove
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* @param {string|Function} clazzOrName The class or name to restore
|
|
275
|
-
* @throws {Error} If the class or name is not registered
|
|
276
|
-
*
|
|
277
|
-
* @example
|
|
278
|
-
* // After this call, resolve(MyService) returns the original class, not the mock
|
|
279
|
-
* container.removeMock(MyService)
|
|
210
|
+
* Remove mock and restore original. Does NOT clear mock call history.
|
|
211
|
+
* @param {string|Function} clazzOrName
|
|
280
212
|
*/
|
|
281
213
|
removeMock(clazzOrName) {
|
|
282
214
|
this.#restoreOriginal(this.#instances.get(clazzOrName), clazzOrName)
|
|
283
215
|
}
|
|
284
216
|
|
|
285
|
-
/**
|
|
286
|
-
* @deprecated Use removeMock() instead. This method will be removed in a future version.
|
|
287
|
-
*
|
|
288
|
-
* Note: This does NOT reset mock call history like vi.fn().mockClear().
|
|
289
|
-
* It completely removes the mock and restores the original class.
|
|
290
|
-
*
|
|
291
|
-
* @param {string|Function} clazzOrName The class or name to restore
|
|
292
|
-
* @throws {Error} If the class or name is not registered
|
|
293
|
-
*/
|
|
294
|
-
resetMock(clazzOrName) {
|
|
295
|
-
console.warn(
|
|
296
|
-
'[DI] resetMock() is deprecated and will be removed in a future version. ' +
|
|
297
|
-
'Use removeMock() instead. Note: This removes the mock entirely, ' +
|
|
298
|
-
'it does NOT clear mock call history.'
|
|
299
|
-
)
|
|
300
|
-
this.removeMock(clazzOrName)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Remove all mocks and restore original classes.
|
|
305
|
-
* This completely removes all mocks - it does NOT just clear mock call history.
|
|
306
|
-
*/
|
|
217
|
+
/** Remove all mocks. Does NOT clear mock call history. */
|
|
307
218
|
removeAllMocks() {
|
|
308
219
|
for (const instanceContext of this.#instances.values()) {
|
|
309
220
|
this.#restoreOriginal(instanceContext)
|
|
@@ -311,44 +222,15 @@ export class Container {
|
|
|
311
222
|
}
|
|
312
223
|
|
|
313
224
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
* It completely removes all mocks and restores original classes.
|
|
318
|
-
*/
|
|
319
|
-
resetAllMocks() {
|
|
320
|
-
console.warn(
|
|
321
|
-
'[DI] resetAllMocks() is deprecated and will be removed in a future version. ' +
|
|
322
|
-
'Use removeAllMocks() instead. Note: This removes all mocks entirely, ' +
|
|
323
|
-
'it does NOT clear mock call history.'
|
|
324
|
-
)
|
|
325
|
-
this.removeAllMocks()
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Reset singleton instances without removing registrations.
|
|
330
|
-
* This clears cached singleton instances so they will be recreated on next resolve.
|
|
331
|
-
* Mock registrations are preserved by default.
|
|
332
|
-
*
|
|
333
|
-
* @param {Object} [options] Options for resetting
|
|
334
|
-
* @param {boolean} [options.preserveMocks=true] If true, keeps mock registrations intact.
|
|
335
|
-
* If false, also removes mocks (same as clear()).
|
|
336
|
-
*
|
|
337
|
-
* @example
|
|
338
|
-
* // Clear singleton instances but keep mocks registered
|
|
339
|
-
* container.resetSingletons()
|
|
340
|
-
*
|
|
341
|
-
* // Clear singleton instances AND remove mocks
|
|
342
|
-
* container.resetSingletons({ preserveMocks: false })
|
|
225
|
+
* Clear cached singleton instances. They'll be recreated on next resolve.
|
|
226
|
+
* @param {Object} [options]
|
|
227
|
+
* @param {boolean} [options.preserveMocks=true]
|
|
343
228
|
*/
|
|
344
229
|
resetSingletons(options = {}) {
|
|
345
230
|
const { preserveMocks = true } = options
|
|
346
231
|
|
|
347
232
|
for (const instanceContext of this.#instances.values()) {
|
|
348
|
-
// Clear the cached singleton instance
|
|
349
233
|
delete instanceContext.instance
|
|
350
|
-
|
|
351
|
-
// Optionally remove mock registrations
|
|
352
234
|
if (!preserveMocks && instanceContext.originalClazz) {
|
|
353
235
|
this.#restoreOriginal(instanceContext)
|
|
354
236
|
}
|
|
@@ -357,27 +239,14 @@ export class Container {
|
|
|
357
239
|
}
|
|
358
240
|
|
|
359
241
|
/**
|
|
360
|
-
* Clear all
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* Note: By default this removes ALL registrations including @Singleton and @Factory classes.
|
|
364
|
-
* For clearing just singleton instances while keeping registrations, use resetSingletons().
|
|
365
|
-
*
|
|
366
|
-
* @param {Object} [options] Options for clearing
|
|
367
|
-
* @param {boolean} [options.preserveRegistrations=false] If true, keeps all registrations but clears cached instances.
|
|
368
|
-
*
|
|
369
|
-
* @example
|
|
370
|
-
* // Remove everything (full reset)
|
|
371
|
-
* container.clear()
|
|
372
|
-
*
|
|
373
|
-
* // Clear cached instances but keep all registrations (including mocks)
|
|
374
|
-
* container.clear({ preserveRegistrations: true })
|
|
242
|
+
* Clear all registrations. Use resetSingletons() to keep registrations.
|
|
243
|
+
* @param {Object} [options]
|
|
244
|
+
* @param {boolean} [options.preserveRegistrations=false]
|
|
375
245
|
*/
|
|
376
246
|
clear(options = {}) {
|
|
377
247
|
const { preserveRegistrations = false } = options
|
|
378
248
|
|
|
379
249
|
if (preserveRegistrations) {
|
|
380
|
-
// Just clear cached instances, keep all registrations
|
|
381
250
|
for (const instanceContext of this.#instances.values()) {
|
|
382
251
|
delete instanceContext.instance
|
|
383
252
|
}
|
|
@@ -389,25 +258,11 @@ export class Container {
|
|
|
389
258
|
}
|
|
390
259
|
|
|
391
260
|
/**
|
|
392
|
-
* Get the mock instance for a mocked class.
|
|
393
|
-
* This is useful when you need to configure mock behavior dynamically in tests.
|
|
394
|
-
*
|
|
395
261
|
* @template T
|
|
396
|
-
* @param {string|Function} clazzOrName
|
|
397
|
-
* @param {...*} params
|
|
398
|
-
* @returns {T}
|
|
399
|
-
* @throws {Error} If
|
|
400
|
-
* @throws {Error} If the class is not mocked
|
|
401
|
-
*
|
|
402
|
-
* @example
|
|
403
|
-
* @Mock(UserService)
|
|
404
|
-
* class MockUserService {
|
|
405
|
-
* getUser = vi.fn()
|
|
406
|
-
* }
|
|
407
|
-
*
|
|
408
|
-
* // In test:
|
|
409
|
-
* const mockInstance = container.getMockInstance(UserService)
|
|
410
|
-
* mockInstance.getUser.mockReturnValue({ id: 1, name: 'Test' })
|
|
262
|
+
* @param {string|Function} clazzOrName
|
|
263
|
+
* @param {...*} params
|
|
264
|
+
* @returns {T}
|
|
265
|
+
* @throws {Error} If not mocked
|
|
411
266
|
*/
|
|
412
267
|
getMockInstance(clazzOrName, ...params) {
|
|
413
268
|
const instanceContext = this.getContext(clazzOrName)
|
|
@@ -422,13 +277,6 @@ export class Container {
|
|
|
422
277
|
return this.getInstance(instanceContext, params)
|
|
423
278
|
}
|
|
424
279
|
|
|
425
|
-
/**
|
|
426
|
-
* Internal function to restore an instance context to its original.
|
|
427
|
-
* @param {InstanceContext} instanceContext The instance context to reset
|
|
428
|
-
* @param {string|Function} [clazzOrName] Optional identifier for error messages
|
|
429
|
-
* @throws {Error} If instanceContext is null or undefined
|
|
430
|
-
* @private
|
|
431
|
-
*/
|
|
432
280
|
#restoreOriginal(instanceContext, clazzOrName) {
|
|
433
281
|
if (!instanceContext) {
|
|
434
282
|
const name = clazzOrName?.name || clazzOrName || 'unknown'
|
|
@@ -439,7 +287,6 @@ export class Container {
|
|
|
439
287
|
clazz: instanceContext.originalClazz,
|
|
440
288
|
instance: undefined,
|
|
441
289
|
originalClazz: undefined,
|
|
442
|
-
originalInstance: undefined,
|
|
443
290
|
proxy: undefined
|
|
444
291
|
})
|
|
445
292
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Container, InjectionToken } from '../../index'
|
|
2
|
+
|
|
3
|
+
export type ContainerScope = 'request' | 'global'
|
|
4
|
+
|
|
5
|
+
export function getGlobalContainer(): Container
|
|
6
|
+
export function getContainer(): Container
|
|
7
|
+
|
|
8
|
+
export interface ResolveOptions {
|
|
9
|
+
scope?: ContainerScope
|
|
10
|
+
params?: any[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolve<T>(clazzOrName: InjectionToken<T>, options?: ResolveOptions): T
|
|
14
|
+
|
|
15
|
+
export function runWithContainer<T>(
|
|
16
|
+
container: Container,
|
|
17
|
+
fn: () => T,
|
|
18
|
+
options?: { scope?: ContainerScope }
|
|
19
|
+
): T
|
|
20
|
+
|
|
21
|
+
export interface MiddlewareOptions {
|
|
22
|
+
scope?: ContainerScope
|
|
23
|
+
debug?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ContainerRequest {
|
|
27
|
+
di: Container
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function containerMiddleware(
|
|
31
|
+
options?: MiddlewareOptions
|
|
32
|
+
): (req: any, res: any, next: () => void) => void
|
|
33
|
+
|
|
34
|
+
export function koaContainerMiddleware(
|
|
35
|
+
options?: MiddlewareOptions
|
|
36
|
+
): (ctx: any, next: () => Promise<void>) => Promise<void>
|
|
37
|
+
|
|
38
|
+
export function withContainer<T extends (...args: any[]) => any>(
|
|
39
|
+
options?: MiddlewareOptions
|
|
40
|
+
): (handler: T) => T
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server middleware integration for decorator-dependency-injection.
|
|
3
|
+
*
|
|
4
|
+
* Provides request-scoped containers using AsyncLocalStorage, enabling
|
|
5
|
+
* automatic per-request isolation without manual container management.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: When using this module, `resolve()` behaves differently than
|
|
8
|
+
* the main module's resolve:
|
|
9
|
+
* - Inside a request: Returns instances from the request-scoped container
|
|
10
|
+
* (singletons are isolated per-request, preventing data leaks between users)
|
|
11
|
+
* - Outside a request: Falls back to the global container
|
|
12
|
+
*
|
|
13
|
+
* @module decorator-dependency-injection/middleware
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
17
|
+
import { Container } from '../Container.js'
|
|
18
|
+
import { defaultContainer as mainDefaultContainer } from '../../index.js'
|
|
19
|
+
|
|
20
|
+
/** @type {AsyncLocalStorage<{container: Container, scope: string}>} */
|
|
21
|
+
const requestContext = new AsyncLocalStorage()
|
|
22
|
+
|
|
23
|
+
/** @type {Container} */
|
|
24
|
+
const globalContainer = mainDefaultContainer
|
|
25
|
+
|
|
26
|
+
/** @returns {Container|null} */
|
|
27
|
+
export function getGlobalContainer() {
|
|
28
|
+
return globalContainer
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getRequestContext() {
|
|
32
|
+
return requestContext.getStore()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @returns {Container} */
|
|
36
|
+
export function getContainer() {
|
|
37
|
+
return getRequestContext()?.container ?? globalContainer
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Request-aware resolve. Uses request-scoped container inside requests,
|
|
42
|
+
* falls back to global container outside. Auto-registers from global container.
|
|
43
|
+
*
|
|
44
|
+
* @template T
|
|
45
|
+
* @param {string|Function} clazzOrName
|
|
46
|
+
* @param {Object} [options]
|
|
47
|
+
* @param {'request'|'global'} [options.scope]
|
|
48
|
+
* @param {Array} [options.params]
|
|
49
|
+
* @returns {T}
|
|
50
|
+
*/
|
|
51
|
+
export function resolve(clazzOrName, options = {}) {
|
|
52
|
+
const { scope, params = [] } = options
|
|
53
|
+
const ctx = getRequestContext()
|
|
54
|
+
|
|
55
|
+
if (scope === 'global' || ctx?.scope === 'global') {
|
|
56
|
+
return globalContainer.resolve(clazzOrName, ...params)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!ctx) {
|
|
60
|
+
if (scope === 'request') {
|
|
61
|
+
console.warn(
|
|
62
|
+
`[DI] resolve() called with scope='request' but no request context exists. ` +
|
|
63
|
+
`Did you forget to use containerMiddleware()? Falling back to global container.`
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
return globalContainer.resolve(clazzOrName, ...params)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const requestContainer = ctx.container
|
|
70
|
+
|
|
71
|
+
if (!requestContainer.has(clazzOrName) && globalContainer?.has(clazzOrName)) {
|
|
72
|
+
const globalContext = globalContainer.getContext(clazzOrName)
|
|
73
|
+
if (globalContext.type === 'singleton') {
|
|
74
|
+
requestContainer.registerSingleton(globalContext.clazz,
|
|
75
|
+
typeof clazzOrName === 'string' ? clazzOrName : undefined)
|
|
76
|
+
} else {
|
|
77
|
+
requestContainer.registerFactory(globalContext.clazz,
|
|
78
|
+
typeof clazzOrName === 'string' ? clazzOrName : undefined)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return requestContainer.resolve(clazzOrName, ...params)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @template T
|
|
87
|
+
* @param {Container} container
|
|
88
|
+
* @param {function(): T} fn
|
|
89
|
+
* @param {Object} [options]
|
|
90
|
+
* @param {'request'|'global'} [options.scope='request']
|
|
91
|
+
* @returns {T}
|
|
92
|
+
*/
|
|
93
|
+
export function runWithContainer(container, fn, options = {}) {
|
|
94
|
+
const { scope = 'request' } = options
|
|
95
|
+
return requestContext.run({ container, scope }, fn)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @typedef {Object} MiddlewareOptions
|
|
100
|
+
* @property {'request'|'global'} [scope='request'] - Container scope mode:
|
|
101
|
+
* - 'request': Each request gets its own container with isolated singletons (default, SSR-safe)
|
|
102
|
+
* - 'global': All requests share the global container (use only for stateless services)
|
|
103
|
+
* @property {boolean} [debug=false] - Enable debug logging
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Express/Connect middleware. scope='request' gives each request isolated singletons (SSR-safe).
|
|
108
|
+
* @param {MiddlewareOptions} [options={}]
|
|
109
|
+
* @returns {function(req, res, next): void}
|
|
110
|
+
*/
|
|
111
|
+
export function containerMiddleware(options = {}) {
|
|
112
|
+
const { scope = 'request', debug = false } = options
|
|
113
|
+
|
|
114
|
+
return (req, res, next) => {
|
|
115
|
+
if (scope === 'global') {
|
|
116
|
+
requestContext.run({ container: globalContainer, scope: 'global' }, () => {
|
|
117
|
+
req.di = globalContainer
|
|
118
|
+
next()
|
|
119
|
+
})
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const container = new Container()
|
|
124
|
+
if (debug) container.setDebug(true)
|
|
125
|
+
req.di = container
|
|
126
|
+
requestContext.run({ container, scope: 'request' }, () => next())
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Koa middleware. See containerMiddleware() for scope behavior.
|
|
132
|
+
* @param {MiddlewareOptions} [options={}]
|
|
133
|
+
* @returns {function(ctx, next): Promise<void>}
|
|
134
|
+
*/
|
|
135
|
+
export function koaContainerMiddleware(options = {}) {
|
|
136
|
+
const { scope = 'request', debug = false } = options
|
|
137
|
+
|
|
138
|
+
return async (ctx, next) => {
|
|
139
|
+
if (scope === 'global') {
|
|
140
|
+
await requestContext.run({ container: globalContainer, scope: 'global' }, async () => {
|
|
141
|
+
ctx.di = globalContainer
|
|
142
|
+
await next()
|
|
143
|
+
})
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const container = new Container()
|
|
148
|
+
if (debug) container.setDebug(true)
|
|
149
|
+
ctx.di = container
|
|
150
|
+
await requestContext.run({ container, scope: 'request' }, () => next())
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Hono/Fastify-style handler wrapper. See containerMiddleware() for scope behavior.
|
|
156
|
+
* @param {MiddlewareOptions} [options={}]
|
|
157
|
+
* @returns {function(handler): function}
|
|
158
|
+
*/
|
|
159
|
+
export function withContainer(options = {}) {
|
|
160
|
+
const { scope = 'request', debug = false } = options
|
|
161
|
+
|
|
162
|
+
return (handler) => (...args) => {
|
|
163
|
+
if (scope === 'global') {
|
|
164
|
+
return requestContext.run({ container: globalContainer, scope: 'global' }, () => handler(...args))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const container = new Container()
|
|
168
|
+
if (debug) container.setDebug(true)
|
|
169
|
+
return requestContext.run({ container, scope: 'request' }, () => handler(...args))
|
|
170
|
+
}
|
|
171
|
+
}
|