@martel/calyx 1.12.0 → 1.13.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/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/cli/index.ts +7 -1
- package/src/config/config.module.ts +16 -2
- package/src/config/config.service.ts +20 -6
- package/src/core/container.ts +340 -154
- package/src/core/testing-module.ts +4 -0
- package/src/cqrs/cqrs.ts +93 -4
- package/src/database/sequelize.module.ts +239 -0
- package/src/event-emitter/decorators.ts +2 -2
- package/src/event-emitter/event-emitter.ts +3 -0
- package/src/http/application.ts +135 -6
- package/src/http/decorators.ts +21 -1
- package/src/http/exceptions.ts +97 -0
- package/src/http/factory.ts +3 -0
- package/src/http/router.ts +27 -4
- package/src/index.ts +1 -0
- package/src/microservices/exceptions.ts +10 -0
- package/src/microservices/index.ts +1 -0
- package/src/queue/queue.module.ts +73 -5
- package/src/terminus/terminus.ts +75 -2
- package/src/validation/compiler.ts +133 -10
- package/src/validation/decorators.ts +164 -2
- package/src/websockets/exceptions.ts +10 -0
- package/src/websockets/index.ts +1 -0
- package/tests/circular-di.test.ts +151 -0
- package/tests/di.test.ts +10 -2
- package/tests/nestjs-parity.test.ts +255 -0
package/src/core/container.ts
CHANGED
|
@@ -65,6 +65,7 @@ export class CalyxContainer {
|
|
|
65
65
|
private providerScopes = new Map<InjectionToken, Scope>();
|
|
66
66
|
private controllerScopes = new Map<any, Scope>();
|
|
67
67
|
private compiledControllerFactories = new Map<any, (requestContext: Map<any, any>) => any>();
|
|
68
|
+
private deferredProxies: { moduleClass: any; token: any; proxyTarget: { instance: any } }[] = [];
|
|
68
69
|
|
|
69
70
|
getModuleRecord(moduleClass: any): ModuleRecord | undefined {
|
|
70
71
|
return this.modules.get(resolveForwardRef(moduleClass));
|
|
@@ -124,29 +125,9 @@ export class CalyxContainer {
|
|
|
124
125
|
this.addModule(actualModule);
|
|
125
126
|
record.imports.add(actualModule);
|
|
126
127
|
|
|
127
|
-
// If it is dynamic module, merge its extra
|
|
128
|
+
// If it is dynamic module, merge its extra metadata recursively
|
|
128
129
|
if (isDynamicModule(resolvedImp)) {
|
|
129
|
-
|
|
130
|
-
if (resolvedImp.providers) {
|
|
131
|
-
for (const prov of resolvedImp.providers) {
|
|
132
|
-
const token = this.getProviderToken(prov);
|
|
133
|
-
dynamicRecord.providers.set(token, prov);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (resolvedImp.controllers) {
|
|
137
|
-
for (const ctrl of resolvedImp.controllers) {
|
|
138
|
-
dynamicRecord.controllers.add(ctrl);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (resolvedImp.exports) {
|
|
142
|
-
for (const exp of resolvedImp.exports) {
|
|
143
|
-
const resolvedExp = resolveForwardRef(exp);
|
|
144
|
-
dynamicRecord.exports.add(resolvedExp);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (resolvedImp.global) {
|
|
148
|
-
this.globalModules.add(actualModule);
|
|
149
|
-
}
|
|
130
|
+
this.mergeDynamicModuleMetadata(resolvedImp);
|
|
150
131
|
}
|
|
151
132
|
}
|
|
152
133
|
|
|
@@ -171,6 +152,45 @@ export class CalyxContainer {
|
|
|
171
152
|
}
|
|
172
153
|
}
|
|
173
154
|
|
|
155
|
+
private mergeDynamicModuleMetadata(dynModule: DynamicModule) {
|
|
156
|
+
const actualModule = dynModule.module;
|
|
157
|
+
const record = this.modules.get(actualModule)!;
|
|
158
|
+
|
|
159
|
+
if (dynModule.imports) {
|
|
160
|
+
for (const imp of dynModule.imports) {
|
|
161
|
+
const resolvedImp = resolveForwardRef(imp);
|
|
162
|
+
const actualImp = isDynamicModule(resolvedImp) ? resolvedImp.module : resolvedImp;
|
|
163
|
+
|
|
164
|
+
this.addModule(actualImp);
|
|
165
|
+
record.imports.add(actualImp);
|
|
166
|
+
|
|
167
|
+
if (isDynamicModule(resolvedImp)) {
|
|
168
|
+
this.mergeDynamicModuleMetadata(resolvedImp);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (dynModule.providers) {
|
|
173
|
+
for (const prov of dynModule.providers) {
|
|
174
|
+
const token = this.getProviderToken(prov);
|
|
175
|
+
record.providers.set(token, prov);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (dynModule.controllers) {
|
|
179
|
+
for (const ctrl of dynModule.controllers) {
|
|
180
|
+
record.controllers.add(ctrl);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (dynModule.exports) {
|
|
184
|
+
for (const exp of dynModule.exports) {
|
|
185
|
+
const resolvedExp = resolveForwardRef(exp);
|
|
186
|
+
record.exports.add(resolvedExp);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (dynModule.global) {
|
|
190
|
+
this.globalModules.add(actualModule);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
174
194
|
private getProviderToken(provider: Provider): InjectionToken {
|
|
175
195
|
if (typeof provider === 'function') {
|
|
176
196
|
return provider;
|
|
@@ -178,19 +198,82 @@ export class CalyxContainer {
|
|
|
178
198
|
return provider.provide;
|
|
179
199
|
}
|
|
180
200
|
|
|
201
|
+
private resolveDeferredProxies() {
|
|
202
|
+
for (const dp of this.deferredProxies) {
|
|
203
|
+
if (!dp.proxyTarget.instance) {
|
|
204
|
+
const record = this.modules.get(dp.moduleClass);
|
|
205
|
+
if (record && record.instances.has(dp.token)) {
|
|
206
|
+
dp.proxyTarget.instance = record.instances.get(dp.token);
|
|
207
|
+
} else {
|
|
208
|
+
try {
|
|
209
|
+
dp.proxyTarget.instance = this.getGlobalOrAnyInstance(dp.token);
|
|
210
|
+
} catch {
|
|
211
|
+
// ignore
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
181
218
|
resolveTokenInModuleContext<T>(moduleClass: any, token: InjectionToken, requestContext?: Map<any, any>): T {
|
|
219
|
+
const resolvedToken = resolveForwardRef(token);
|
|
182
220
|
// 1. Check circular dependency
|
|
183
221
|
const isResolving = this.resolvingStack.some(
|
|
184
|
-
(item) => item.moduleClass === moduleClass && item.token ===
|
|
222
|
+
(item) => item.moduleClass === moduleClass && item.token === resolvedToken
|
|
185
223
|
);
|
|
186
224
|
if (isResolving) {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
225
|
+
const container = this;
|
|
226
|
+
const proxyTarget = { instance: null as any };
|
|
227
|
+
this.deferredProxies.push({
|
|
228
|
+
moduleClass,
|
|
229
|
+
token: resolvedToken,
|
|
230
|
+
proxyTarget,
|
|
231
|
+
});
|
|
232
|
+
return new Proxy(proxyTarget, {
|
|
233
|
+
get(target, prop) {
|
|
234
|
+
if (prop === '__isCalyxProxy') return true;
|
|
235
|
+
if (prop === '__proxyTarget') return target;
|
|
236
|
+
if (!target.instance) {
|
|
237
|
+
const record = container.modules.get(moduleClass);
|
|
238
|
+
if (record && record.instances.has(resolvedToken)) {
|
|
239
|
+
target.instance = record.instances.get(resolvedToken);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!target.instance) {
|
|
243
|
+
try {
|
|
244
|
+
target.instance = container.getGlobalOrAnyInstance(resolvedToken);
|
|
245
|
+
} catch {
|
|
246
|
+
// ignore
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!target.instance) {
|
|
250
|
+
if (typeof prop === 'symbol' || prop === 'then' || prop === 'toJSON') {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
throw new Error(`Calyx circular proxy DI error: Accessing property "${String(prop)}" on circular dependency ${resolvedToken.name || resolvedToken} before it is instantiated.`);
|
|
254
|
+
}
|
|
255
|
+
const value = Reflect.get(target.instance, prop);
|
|
256
|
+
if (typeof value === 'function') {
|
|
257
|
+
return value.bind(target.instance);
|
|
258
|
+
}
|
|
259
|
+
return value;
|
|
260
|
+
},
|
|
261
|
+
set(target, prop, value) {
|
|
262
|
+
if (!target.instance) {
|
|
263
|
+
const record = container.modules.get(moduleClass);
|
|
264
|
+
if (record && record.instances.has(resolvedToken)) {
|
|
265
|
+
target.instance = record.instances.get(resolvedToken);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!target.instance) {
|
|
269
|
+
throw new Error(`Calyx circular proxy DI error: Setting property "${String(prop)}" on circular dependency ${resolvedToken.name || resolvedToken} before it is instantiated.`);
|
|
270
|
+
}
|
|
271
|
+
return Reflect.set(target.instance, prop, value);
|
|
272
|
+
}
|
|
273
|
+
}) as any;
|
|
191
274
|
}
|
|
192
275
|
|
|
193
|
-
this.resolvingStack.push({ moduleClass, token });
|
|
276
|
+
this.resolvingStack.push({ moduleClass, token: resolvedToken });
|
|
194
277
|
|
|
195
278
|
try {
|
|
196
279
|
const record = this.modules.get(moduleClass);
|
|
@@ -199,14 +282,14 @@ export class CalyxContainer {
|
|
|
199
282
|
}
|
|
200
283
|
|
|
201
284
|
// Special resolution cases
|
|
202
|
-
if (
|
|
285
|
+
if (resolvedToken === REQUEST) {
|
|
203
286
|
if (!requestContext || !requestContext.has(REQUEST)) {
|
|
204
287
|
throw new Error(`calyx DI: REQUEST token resolved outside of a request context`);
|
|
205
288
|
}
|
|
206
289
|
return requestContext.get(REQUEST);
|
|
207
290
|
}
|
|
208
291
|
|
|
209
|
-
if (
|
|
292
|
+
if (resolvedToken === ModuleRef) {
|
|
210
293
|
const instance = record.instances.get(ModuleRef);
|
|
211
294
|
if (instance) return instance;
|
|
212
295
|
const moduleRefInstance = new ContainerModuleRef(this, moduleClass);
|
|
@@ -214,16 +297,17 @@ export class CalyxContainer {
|
|
|
214
297
|
return moduleRefInstance as any;
|
|
215
298
|
}
|
|
216
299
|
|
|
217
|
-
if (
|
|
300
|
+
if (resolvedToken === moduleClass) {
|
|
218
301
|
const instance = record.instances.get(moduleClass);
|
|
219
302
|
if (instance) return instance;
|
|
220
303
|
const instantiatedModule = this.instantiateClass(moduleClass, moduleClass, requestContext);
|
|
221
304
|
record.instances.set(moduleClass, instantiatedModule);
|
|
305
|
+
this.resolveDeferredProxies();
|
|
222
306
|
return instantiatedModule;
|
|
223
307
|
}
|
|
224
308
|
|
|
225
309
|
// Find where the token is defined
|
|
226
|
-
const resolution = this.findProviderDefinition(moduleClass,
|
|
310
|
+
const resolution = this.findProviderDefinition(moduleClass, resolvedToken);
|
|
227
311
|
if (!resolution) {
|
|
228
312
|
// Not found. Check if the token is optional
|
|
229
313
|
const currentResolving = this.resolvingStack[this.resolvingStack.length - 2];
|
|
@@ -238,7 +322,7 @@ export class CalyxContainer {
|
|
|
238
322
|
for (let i = 0; i < paramTypes.length; i++) {
|
|
239
323
|
const pToken = injectTokens.get(i) ?? paramTypes[i];
|
|
240
324
|
const resolvedPToken = resolveForwardRef(pToken);
|
|
241
|
-
if (resolvedPToken ===
|
|
325
|
+
if (resolvedPToken === resolvedToken && optionalParams.has(i)) {
|
|
242
326
|
isOptional = true;
|
|
243
327
|
break;
|
|
244
328
|
}
|
|
@@ -248,23 +332,24 @@ export class CalyxContainer {
|
|
|
248
332
|
}
|
|
249
333
|
}
|
|
250
334
|
}
|
|
251
|
-
throw new Error(`calyx DI: Cannot resolve dependency "${String(
|
|
335
|
+
throw new Error(`calyx DI: Cannot resolve dependency "${String(resolvedToken.name ?? resolvedToken)}" in module ${moduleClass.name || moduleClass}`);
|
|
252
336
|
}
|
|
253
337
|
|
|
254
338
|
const { targetModuleClass, provider } = resolution;
|
|
255
339
|
const targetRecord = this.modules.get(targetModuleClass)!;
|
|
256
340
|
|
|
257
|
-
const scope = this.providerScopes.get(
|
|
341
|
+
const scope = this.providerScopes.get(resolvedToken) ?? Scope.DEFAULT;
|
|
258
342
|
|
|
259
343
|
if (scope === Scope.REQUEST) {
|
|
260
344
|
if (!requestContext) {
|
|
261
|
-
throw new Error(`calyx DI: Cannot resolve request-scoped provider "${String(
|
|
345
|
+
throw new Error(`calyx DI: Cannot resolve request-scoped provider "${String(resolvedToken.name ?? resolvedToken)}" without request context.`);
|
|
262
346
|
}
|
|
263
|
-
if (requestContext.has(
|
|
264
|
-
return requestContext.get(
|
|
347
|
+
if (requestContext.has(resolvedToken)) {
|
|
348
|
+
return requestContext.get(resolvedToken);
|
|
265
349
|
}
|
|
266
350
|
const instance = this.createInstanceFromProvider(provider, targetModuleClass, requestContext);
|
|
267
|
-
requestContext.set(
|
|
351
|
+
requestContext.set(resolvedToken, instance);
|
|
352
|
+
this.resolveDeferredProxies();
|
|
268
353
|
return instance;
|
|
269
354
|
}
|
|
270
355
|
|
|
@@ -273,12 +358,13 @@ export class CalyxContainer {
|
|
|
273
358
|
}
|
|
274
359
|
|
|
275
360
|
// Check if instance already exists in target module (Singleton)
|
|
276
|
-
if (targetRecord.instances.has(
|
|
277
|
-
return targetRecord.instances.get(
|
|
361
|
+
if (targetRecord.instances.has(resolvedToken)) {
|
|
362
|
+
return targetRecord.instances.get(resolvedToken);
|
|
278
363
|
}
|
|
279
364
|
|
|
280
365
|
const instance = this.createInstanceFromProvider(provider, targetModuleClass, requestContext);
|
|
281
|
-
targetRecord.instances.set(
|
|
366
|
+
targetRecord.instances.set(resolvedToken, instance);
|
|
367
|
+
this.resolveDeferredProxies();
|
|
282
368
|
return instance;
|
|
283
369
|
} finally {
|
|
284
370
|
this.resolvingStack.pop();
|
|
@@ -402,7 +488,7 @@ export class CalyxContainer {
|
|
|
402
488
|
const propertyInjects: Map<string | symbol, InjectionToken> =
|
|
403
489
|
Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, Class) || new Map();
|
|
404
490
|
for (const [propertyKey, token] of propertyInjects.entries()) {
|
|
405
|
-
instance[propertyKey] = this.resolveTokenInModuleContext(moduleClass, token, requestContext);
|
|
491
|
+
instance[propertyKey] = this.resolveTokenInModuleContext(moduleClass, resolveForwardRef(token), requestContext);
|
|
406
492
|
}
|
|
407
493
|
|
|
408
494
|
return instance;
|
|
@@ -464,9 +550,11 @@ export class CalyxContainer {
|
|
|
464
550
|
if (record.instances.get(moduleClass) === null) {
|
|
465
551
|
const instance = this.instantiateClass(moduleClass, moduleClass);
|
|
466
552
|
record.instances.set(moduleClass, instance);
|
|
553
|
+
this.resolveDeferredProxies();
|
|
467
554
|
}
|
|
468
555
|
}
|
|
469
556
|
|
|
557
|
+
this.resolveDeferredProxies();
|
|
470
558
|
this.compileControllerFactories();
|
|
471
559
|
}
|
|
472
560
|
|
|
@@ -493,9 +581,11 @@ export class CalyxContainer {
|
|
|
493
581
|
if (record.instances.get(moduleClass) === null) {
|
|
494
582
|
const instance = await this.instantiateClassAsync(moduleClass, moduleClass);
|
|
495
583
|
record.instances.set(moduleClass, instance);
|
|
584
|
+
this.resolveDeferredProxies();
|
|
496
585
|
}
|
|
497
586
|
}
|
|
498
587
|
|
|
588
|
+
this.resolveDeferredProxies();
|
|
499
589
|
this.compileControllerFactories();
|
|
500
590
|
}
|
|
501
591
|
|
|
@@ -506,6 +596,7 @@ export class CalyxContainer {
|
|
|
506
596
|
}
|
|
507
597
|
const instance = this.instantiateClass(controllerClass, moduleClass);
|
|
508
598
|
record.instances.set(controllerClass, instance);
|
|
599
|
+
this.resolveDeferredProxies();
|
|
509
600
|
return instance;
|
|
510
601
|
}
|
|
511
602
|
|
|
@@ -516,6 +607,7 @@ export class CalyxContainer {
|
|
|
516
607
|
}
|
|
517
608
|
const instance = await this.instantiateClassAsync(controllerClass, moduleClass);
|
|
518
609
|
record.instances.set(controllerClass, instance);
|
|
610
|
+
this.resolveDeferredProxies();
|
|
519
611
|
return instance;
|
|
520
612
|
}
|
|
521
613
|
|
|
@@ -546,17 +638,63 @@ export class CalyxContainer {
|
|
|
546
638
|
}
|
|
547
639
|
|
|
548
640
|
async resolveTokenInModuleContextAsync<T>(moduleClass: any, token: InjectionToken, requestContext?: Map<any, any>): Promise<T> {
|
|
641
|
+
const resolvedToken = resolveForwardRef(token);
|
|
549
642
|
const isResolving = this.resolvingStack.some(
|
|
550
|
-
(item) => item.moduleClass === moduleClass && item.token ===
|
|
643
|
+
(item) => item.moduleClass === moduleClass && item.token === resolvedToken
|
|
551
644
|
);
|
|
552
645
|
if (isResolving) {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
646
|
+
const container = this;
|
|
647
|
+
const proxyTarget = { instance: null as any };
|
|
648
|
+
this.deferredProxies.push({
|
|
649
|
+
moduleClass,
|
|
650
|
+
token: resolvedToken,
|
|
651
|
+
proxyTarget,
|
|
652
|
+
});
|
|
653
|
+
return new Proxy(proxyTarget, {
|
|
654
|
+
get(target, prop) {
|
|
655
|
+
if (prop === '__isCalyxProxy') return true;
|
|
656
|
+
if (prop === '__proxyTarget') return target;
|
|
657
|
+
if (!target.instance) {
|
|
658
|
+
const record = container.modules.get(moduleClass);
|
|
659
|
+
if (record && record.instances.has(resolvedToken)) {
|
|
660
|
+
target.instance = record.instances.get(resolvedToken);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (!target.instance) {
|
|
664
|
+
try {
|
|
665
|
+
target.instance = container.getGlobalOrAnyInstance(resolvedToken);
|
|
666
|
+
} catch {
|
|
667
|
+
// ignore
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (!target.instance) {
|
|
671
|
+
if (typeof prop === 'symbol' || prop === 'then' || prop === 'toJSON') {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
throw new Error(`Calyx circular proxy DI error: Accessing property "${String(prop)}" on circular dependency ${resolvedToken.name || resolvedToken} before it is instantiated.`);
|
|
675
|
+
}
|
|
676
|
+
const value = Reflect.get(target.instance, prop);
|
|
677
|
+
if (typeof value === 'function') {
|
|
678
|
+
return value.bind(target.instance);
|
|
679
|
+
}
|
|
680
|
+
return value;
|
|
681
|
+
},
|
|
682
|
+
set(target, prop, value) {
|
|
683
|
+
if (!target.instance) {
|
|
684
|
+
const record = container.modules.get(moduleClass);
|
|
685
|
+
if (record && record.instances.has(resolvedToken)) {
|
|
686
|
+
target.instance = record.instances.get(resolvedToken);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (!target.instance) {
|
|
690
|
+
throw new Error(`Calyx circular proxy DI error: Setting property "${String(prop)}" on circular dependency ${resolvedToken.name || resolvedToken} before it is instantiated.`);
|
|
691
|
+
}
|
|
692
|
+
return Reflect.set(target.instance, prop, value);
|
|
693
|
+
}
|
|
694
|
+
}) as any;
|
|
557
695
|
}
|
|
558
696
|
|
|
559
|
-
this.resolvingStack.push({ moduleClass, token });
|
|
697
|
+
this.resolvingStack.push({ moduleClass, token: resolvedToken });
|
|
560
698
|
|
|
561
699
|
try {
|
|
562
700
|
const record = this.modules.get(moduleClass);
|
|
@@ -564,14 +702,14 @@ export class CalyxContainer {
|
|
|
564
702
|
throw new Error(`Module ${moduleClass.name || moduleClass} is not registered in the container`);
|
|
565
703
|
}
|
|
566
704
|
|
|
567
|
-
if (
|
|
705
|
+
if (resolvedToken === REQUEST) {
|
|
568
706
|
if (!requestContext || !requestContext.has(REQUEST)) {
|
|
569
707
|
throw new Error(`calyx DI: REQUEST token resolved outside of a request context`);
|
|
570
708
|
}
|
|
571
709
|
return requestContext.get(REQUEST);
|
|
572
710
|
}
|
|
573
711
|
|
|
574
|
-
if (
|
|
712
|
+
if (resolvedToken === ModuleRef) {
|
|
575
713
|
const instance = record.instances.get(ModuleRef);
|
|
576
714
|
if (instance) return instance;
|
|
577
715
|
const moduleRefInstance = new ContainerModuleRef(this, moduleClass);
|
|
@@ -579,15 +717,16 @@ export class CalyxContainer {
|
|
|
579
717
|
return moduleRefInstance as any;
|
|
580
718
|
}
|
|
581
719
|
|
|
582
|
-
if (
|
|
720
|
+
if (resolvedToken === moduleClass) {
|
|
583
721
|
const instance = record.instances.get(moduleClass);
|
|
584
722
|
if (instance) return instance;
|
|
585
723
|
const instantiatedModule = await this.instantiateClassAsync(moduleClass, moduleClass, requestContext);
|
|
586
724
|
record.instances.set(moduleClass, instantiatedModule);
|
|
725
|
+
this.resolveDeferredProxies();
|
|
587
726
|
return instantiatedModule;
|
|
588
727
|
}
|
|
589
728
|
|
|
590
|
-
const resolution = this.findProviderDefinition(moduleClass,
|
|
729
|
+
const resolution = this.findProviderDefinition(moduleClass, resolvedToken);
|
|
591
730
|
if (!resolution) {
|
|
592
731
|
const currentResolving = this.resolvingStack[this.resolvingStack.length - 2];
|
|
593
732
|
if (currentResolving) {
|
|
@@ -601,7 +740,7 @@ export class CalyxContainer {
|
|
|
601
740
|
for (let i = 0; i < paramTypes.length; i++) {
|
|
602
741
|
const pToken = injectTokens.get(i) ?? paramTypes[i];
|
|
603
742
|
const resolvedPToken = resolveForwardRef(pToken);
|
|
604
|
-
if (resolvedPToken ===
|
|
743
|
+
if (resolvedPToken === resolvedToken && optionalParams.has(i)) {
|
|
605
744
|
isOptional = true;
|
|
606
745
|
break;
|
|
607
746
|
}
|
|
@@ -611,23 +750,24 @@ export class CalyxContainer {
|
|
|
611
750
|
}
|
|
612
751
|
}
|
|
613
752
|
}
|
|
614
|
-
throw new Error(`calyx DI: Cannot resolve dependency "${String(
|
|
753
|
+
throw new Error(`calyx DI: Cannot resolve dependency "${String(resolvedToken.name ?? resolvedToken)}" in module ${moduleClass.name || moduleClass}`);
|
|
615
754
|
}
|
|
616
755
|
|
|
617
756
|
const { targetModuleClass, provider } = resolution;
|
|
618
757
|
const targetRecord = this.modules.get(targetModuleClass)!;
|
|
619
758
|
|
|
620
|
-
const scope = this.providerScopes.get(
|
|
759
|
+
const scope = this.providerScopes.get(resolvedToken) ?? Scope.DEFAULT;
|
|
621
760
|
|
|
622
761
|
if (scope === Scope.REQUEST) {
|
|
623
762
|
if (!requestContext) {
|
|
624
|
-
throw new Error(`calyx DI: Cannot resolve request-scoped provider "${String(
|
|
763
|
+
throw new Error(`calyx DI: Cannot resolve request-scoped provider "${String(resolvedToken.name ?? resolvedToken)}" without request context.`);
|
|
625
764
|
}
|
|
626
|
-
if (requestContext.has(
|
|
627
|
-
return requestContext.get(
|
|
765
|
+
if (requestContext.has(resolvedToken)) {
|
|
766
|
+
return requestContext.get(resolvedToken);
|
|
628
767
|
}
|
|
629
768
|
const instance = await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
|
|
630
|
-
requestContext.set(
|
|
769
|
+
requestContext.set(resolvedToken, instance);
|
|
770
|
+
this.resolveDeferredProxies();
|
|
631
771
|
return instance;
|
|
632
772
|
}
|
|
633
773
|
|
|
@@ -635,12 +775,13 @@ export class CalyxContainer {
|
|
|
635
775
|
return await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
|
|
636
776
|
}
|
|
637
777
|
|
|
638
|
-
if (targetRecord.instances.has(
|
|
639
|
-
return targetRecord.instances.get(
|
|
778
|
+
if (targetRecord.instances.has(resolvedToken)) {
|
|
779
|
+
return targetRecord.instances.get(resolvedToken);
|
|
640
780
|
}
|
|
641
781
|
|
|
642
782
|
const instance = await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
|
|
643
|
-
targetRecord.instances.set(
|
|
783
|
+
targetRecord.instances.set(resolvedToken, instance);
|
|
784
|
+
this.resolveDeferredProxies();
|
|
644
785
|
return instance;
|
|
645
786
|
} finally {
|
|
646
787
|
this.resolvingStack.pop();
|
|
@@ -700,7 +841,7 @@ export class CalyxContainer {
|
|
|
700
841
|
const propertyInjects: Map<string | symbol, InjectionToken> =
|
|
701
842
|
Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, Class) || new Map();
|
|
702
843
|
for (const [propertyKey, token] of propertyInjects.entries()) {
|
|
703
|
-
instance[propertyKey] = await this.resolveTokenInModuleContextAsync(moduleClass, token, requestContext);
|
|
844
|
+
instance[propertyKey] = await this.resolveTokenInModuleContextAsync(moduleClass, resolveForwardRef(token), requestContext);
|
|
704
845
|
}
|
|
705
846
|
|
|
706
847
|
return instance;
|
|
@@ -865,125 +1006,170 @@ export class CalyxContainer {
|
|
|
865
1006
|
return idx;
|
|
866
1007
|
};
|
|
867
1008
|
|
|
1009
|
+
const getLazyRequestProxy = (token: any, requestContext: Map<any, any>) => {
|
|
1010
|
+
const proxyTarget = { instance: null as any };
|
|
1011
|
+
return new Proxy(proxyTarget, {
|
|
1012
|
+
get(target, prop) {
|
|
1013
|
+
if (prop === '__isCalyxProxy') return true;
|
|
1014
|
+
if (prop === '__proxyTarget') return target;
|
|
1015
|
+
if (!target.instance) {
|
|
1016
|
+
target.instance = requestContext.get(token);
|
|
1017
|
+
}
|
|
1018
|
+
if (!target.instance) {
|
|
1019
|
+
if (typeof prop === 'symbol' || prop === 'then' || prop === 'toJSON') {
|
|
1020
|
+
return undefined;
|
|
1021
|
+
}
|
|
1022
|
+
throw new Error(`Calyx circular request proxy error: Accessing property "${String(prop)}" before it is instantiated.`);
|
|
1023
|
+
}
|
|
1024
|
+
const val = Reflect.get(target.instance, prop);
|
|
1025
|
+
if (typeof val === 'function') {
|
|
1026
|
+
return val.bind(target.instance);
|
|
1027
|
+
}
|
|
1028
|
+
return val;
|
|
1029
|
+
},
|
|
1030
|
+
set(target, prop, value) {
|
|
1031
|
+
if (!target.instance) {
|
|
1032
|
+
target.instance = requestContext.get(token);
|
|
1033
|
+
}
|
|
1034
|
+
if (!target.instance) {
|
|
1035
|
+
throw new Error(`Calyx circular request proxy error: Setting property "${String(prop)}" before it is instantiated.`);
|
|
1036
|
+
}
|
|
1037
|
+
return Reflect.set(target.instance, prop, value);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
const lazyProxyHelperIdx = getClosureIdx(getLazyRequestProxy);
|
|
1043
|
+
|
|
868
1044
|
const instantiations: string[] = [];
|
|
869
1045
|
const compiledTokens = new Set<string>();
|
|
1046
|
+
const compilingTokens = new Set<string>();
|
|
870
1047
|
|
|
871
1048
|
const compileToken = (token: any, currentModuleClass: any): string => {
|
|
872
|
-
|
|
1049
|
+
const resolvedToken = resolveForwardRef(token);
|
|
1050
|
+
if (resolvedToken === REQUEST) {
|
|
873
1051
|
return `requestContext.get(c[${getClosureIdx(REQUEST)}])`;
|
|
874
1052
|
}
|
|
875
|
-
if (
|
|
1053
|
+
if (resolvedToken === ModuleRef) {
|
|
876
1054
|
const record = this.modules.get(currentModuleClass)!;
|
|
877
1055
|
return `c[${getClosureIdx(record.instances.get(ModuleRef))}]`;
|
|
878
1056
|
}
|
|
879
1057
|
|
|
880
|
-
const scope = this.providerScopes.get(
|
|
1058
|
+
const scope = this.providerScopes.get(resolvedToken) ?? Scope.DEFAULT;
|
|
881
1059
|
if (scope === Scope.DEFAULT) {
|
|
882
|
-
const instance = this.resolveTokenInModuleContext(currentModuleClass,
|
|
1060
|
+
const instance = this.resolveTokenInModuleContext(currentModuleClass, resolvedToken);
|
|
883
1061
|
return `c[${getClosureIdx(instance)}]`;
|
|
884
1062
|
}
|
|
885
1063
|
|
|
886
|
-
const tokenKey = `${currentModuleClass.name || currentModuleClass}::${String(
|
|
1064
|
+
const tokenKey = `${currentModuleClass.name || currentModuleClass}::${String(resolvedToken.name || resolvedToken)}`;
|
|
887
1065
|
if (compiledTokens.has(tokenKey)) {
|
|
888
|
-
return `requestContext.get(c[${getClosureIdx(
|
|
1066
|
+
return `requestContext.get(c[${getClosureIdx(resolvedToken)}])`;
|
|
889
1067
|
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if (!resolution) {
|
|
893
|
-
const optional = this.isTokenOptional(token, currentModuleClass);
|
|
894
|
-
return optional ? `undefined` : `(() => { throw new Error('calyx JIT DI: Cannot resolve dependency ' + String(c[${getClosureIdx(token)}])); })()`;
|
|
1068
|
+
if (compilingTokens.has(tokenKey)) {
|
|
1069
|
+
return `c[${lazyProxyHelperIdx}](c[${getClosureIdx(resolvedToken)}], requestContext)`;
|
|
895
1070
|
}
|
|
896
1071
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
const
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1072
|
+
compilingTokens.add(tokenKey);
|
|
1073
|
+
try {
|
|
1074
|
+
const resolution = this.findProviderDefinition(currentModuleClass, resolvedToken);
|
|
1075
|
+
if (!resolution) {
|
|
1076
|
+
const optional = this.isTokenOptional(resolvedToken, currentModuleClass);
|
|
1077
|
+
return optional ? `undefined` : `(() => { throw new Error('calyx JIT DI: Cannot resolve dependency ' + String(c[${getClosureIdx(resolvedToken)}])); })()`;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const { targetModuleClass, provider } = resolution;
|
|
1081
|
+
|
|
1082
|
+
let valExpr = '';
|
|
1083
|
+
if (typeof provider !== 'function' && 'useValue' in provider) {
|
|
1084
|
+
valExpr = `c[${getClosureIdx(provider.useValue)}]`;
|
|
1085
|
+
} else if (typeof provider !== 'function' && 'useExisting' in provider) {
|
|
1086
|
+
return compileToken(resolveForwardRef(provider.useExisting), targetModuleClass);
|
|
1087
|
+
} else {
|
|
1088
|
+
const ClassToInstantiate = typeof provider === 'function'
|
|
1089
|
+
? provider
|
|
1090
|
+
: ('useClass' in provider ? provider.useClass : null);
|
|
1091
|
+
|
|
1092
|
+
if (ClassToInstantiate) {
|
|
1093
|
+
const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', ClassToInstantiate) || [];
|
|
1094
|
+
const injectTokens: Map<number, any> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, ClassToInstantiate) || new Map();
|
|
1095
|
+
const optionalParams: Set<number> = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, ClassToInstantiate) || new Set();
|
|
1096
|
+
|
|
1097
|
+
const argsExprs = paramTypes.map((paramType, i) => {
|
|
1098
|
+
const depToken = injectTokens.get(i) ?? paramType;
|
|
1099
|
+
const resolvedDepToken = resolveForwardRef(depToken);
|
|
1100
|
+
try {
|
|
1101
|
+
return compileToken(resolvedDepToken, targetModuleClass);
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
if (optionalParams.has(i)) {
|
|
1104
|
+
return `undefined`;
|
|
1105
|
+
}
|
|
1106
|
+
throw err;
|
|
922
1107
|
}
|
|
923
|
-
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
const propertyInjects: Map<string | symbol, any> =
|
|
1111
|
+
Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, ClassToInstantiate) || new Map();
|
|
1112
|
+
|
|
1113
|
+
const propAssignments: string[] = [];
|
|
1114
|
+
for (const [propKey, propToken] of propertyInjects.entries()) {
|
|
1115
|
+
const resolvedPropToken = resolveForwardRef(propToken);
|
|
1116
|
+
const propValExpr = compileToken(resolvedPropToken, targetModuleClass);
|
|
1117
|
+
propAssignments.push(`inst.${String(propKey)} = ${propValExpr};`);
|
|
924
1118
|
}
|
|
925
|
-
});
|
|
926
|
-
|
|
927
|
-
const propertyInjects: Map<string | symbol, any> =
|
|
928
|
-
Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, ClassToInstantiate) || new Map();
|
|
929
|
-
|
|
930
|
-
const propAssignments: string[] = [];
|
|
931
|
-
for (const [propKey, propToken] of propertyInjects.entries()) {
|
|
932
|
-
const resolvedPropToken = resolveForwardRef(propToken);
|
|
933
|
-
const propValExpr = compileToken(resolvedPropToken, targetModuleClass);
|
|
934
|
-
propAssignments.push(`inst.${String(propKey)} = ${propValExpr};`);
|
|
935
|
-
}
|
|
936
1119
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1120
|
+
const classIdx = getClosureIdx(ClassToInstantiate);
|
|
1121
|
+
const instantiateExpr = `new c[${classIdx}](${argsExprs.join(', ')})`;
|
|
1122
|
+
|
|
1123
|
+
if (scope === Scope.REQUEST) {
|
|
1124
|
+
compiledTokens.add(tokenKey);
|
|
1125
|
+
const tokenIdx = getClosureIdx(resolvedToken);
|
|
1126
|
+
instantiations.push(`
|
|
1127
|
+
if (!requestContext.has(c[${tokenIdx}])) {
|
|
1128
|
+
const inst = ${instantiateExpr};
|
|
1129
|
+
${propAssignments.join('\n')}
|
|
1130
|
+
requestContext.set(c[${tokenIdx}], inst);
|
|
1131
|
+
}
|
|
1132
|
+
`);
|
|
1133
|
+
valExpr = `requestContext.get(c[${tokenIdx}])`;
|
|
1134
|
+
} else {
|
|
1135
|
+
if (propAssignments.length > 0) {
|
|
1136
|
+
const tempVar = `transient_${compiledTokens.size}`;
|
|
1137
|
+
compiledTokens.add(tempVar);
|
|
1138
|
+
instantiations.push(`
|
|
1139
|
+
const ${tempVar} = ${instantiateExpr};
|
|
1140
|
+
${propAssignments.map(line => line.replace('inst.', `${tempVar}.`)).join('\n')}
|
|
1141
|
+
`);
|
|
1142
|
+
valExpr = tempVar;
|
|
1143
|
+
} else {
|
|
1144
|
+
valExpr = instantiateExpr;
|
|
948
1145
|
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1146
|
+
}
|
|
1147
|
+
} else if (provider && typeof provider === 'object' && 'useFactory' in provider) {
|
|
1148
|
+
const injectTokens = provider.inject || [];
|
|
1149
|
+
const argsExprs = injectTokens.map((t) => compileToken(resolveForwardRef(t), targetModuleClass));
|
|
1150
|
+
|
|
1151
|
+
const factoryIdx = getClosureIdx(provider.useFactory);
|
|
1152
|
+
const factoryCallExpr = `c[${factoryIdx}](${argsExprs.join(', ')})`;
|
|
1153
|
+
|
|
1154
|
+
if (scope === Scope.REQUEST) {
|
|
1155
|
+
compiledTokens.add(tokenKey);
|
|
1156
|
+
const tokenIdx = getClosureIdx(resolvedToken);
|
|
955
1157
|
instantiations.push(`
|
|
956
|
-
|
|
957
|
-
|
|
1158
|
+
if (!requestContext.has(c[${tokenIdx}])) {
|
|
1159
|
+
requestContext.set(c[${tokenIdx}], ${factoryCallExpr});
|
|
1160
|
+
}
|
|
958
1161
|
`);
|
|
959
|
-
valExpr =
|
|
1162
|
+
valExpr = `requestContext.get(c[${tokenIdx}])`;
|
|
960
1163
|
} else {
|
|
961
|
-
valExpr =
|
|
1164
|
+
valExpr = factoryCallExpr;
|
|
962
1165
|
}
|
|
963
1166
|
}
|
|
964
|
-
} else if (provider && typeof provider === 'object' && 'useFactory' in provider) {
|
|
965
|
-
const injectTokens = provider.inject || [];
|
|
966
|
-
const argsExprs = injectTokens.map((t) => compileToken(resolveForwardRef(t), targetModuleClass));
|
|
967
|
-
|
|
968
|
-
const factoryIdx = getClosureIdx(provider.useFactory);
|
|
969
|
-
const factoryCallExpr = `c[${factoryIdx}](${argsExprs.join(', ')})`;
|
|
970
|
-
|
|
971
|
-
if (scope === Scope.REQUEST) {
|
|
972
|
-
compiledTokens.add(tokenKey);
|
|
973
|
-
const tokenIdx = getClosureIdx(token);
|
|
974
|
-
instantiations.push(`
|
|
975
|
-
if (!requestContext.has(c[${tokenIdx}])) {
|
|
976
|
-
requestContext.set(c[${tokenIdx}], ${factoryCallExpr});
|
|
977
|
-
}
|
|
978
|
-
`);
|
|
979
|
-
valExpr = `requestContext.get(c[${tokenIdx}])`;
|
|
980
|
-
} else {
|
|
981
|
-
valExpr = factoryCallExpr;
|
|
982
|
-
}
|
|
983
1167
|
}
|
|
984
|
-
}
|
|
985
1168
|
|
|
986
|
-
|
|
1169
|
+
return valExpr;
|
|
1170
|
+
} finally {
|
|
1171
|
+
compilingTokens.delete(tokenKey);
|
|
1172
|
+
}
|
|
987
1173
|
};
|
|
988
1174
|
|
|
989
1175
|
const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', controllerClass) || [];
|