@martel/calyx 1.10.1 → 1.12.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 +14 -0
- package/package.json +1 -1
- package/src/cache/cache.interceptor.ts +4 -2
- package/src/cache/decorators.ts +4 -0
- package/src/cache/index.ts +1 -0
- package/src/core/container.ts +242 -9
- package/src/core/index.ts +2 -0
- package/src/core/lazy-module-loader.ts +29 -0
- package/src/core/metadata.ts +6 -1
- package/src/core/testing-module.ts +119 -0
- package/src/cqrs/cqrs.ts +175 -0
- package/src/graphql/decorators.ts +16 -0
- package/src/graphql/graphql.module.ts +103 -3
- package/src/http/application.ts +160 -19
- package/src/http/decorators.ts +4 -0
- package/src/index.ts +2 -0
- package/src/microservices/clients.module.ts +47 -0
- package/src/microservices/index.ts +1 -0
- package/src/microservices/microservice.ts +1 -1
- package/src/openapi/swagger.module.ts +29 -8
- package/src/schedule/decorators.ts +10 -6
- package/src/schedule/index.ts +1 -0
- package/src/schedule/schedule.module.ts +3 -2
- package/src/schedule/scheduler-registry.ts +50 -0
- package/src/security/index.ts +1 -0
- package/src/security/throttler.module.ts +108 -0
- package/src/terminus/terminus.ts +61 -0
- package/src/validation/http-pipes.ts +128 -0
- package/src/validation/index.ts +1 -0
- package/src/websockets/decorators.ts +12 -2
- package/tests/graphql.test.ts +101 -0
- package/tests/nestjs-parity.test.ts +272 -0
- package/tests/openapi.test.ts +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.12.0](https://github.com/bmartel/calyx/compare/v1.11.0...v1.12.0) (2026-07-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **core:** implement core NestJS feature parity extensions ([ba69f26](https://github.com/bmartel/calyx/commit/ba69f266d1b6ead3ab2ce4f6c5b112258131ed75))
|
|
7
|
+
|
|
8
|
+
# [1.11.0](https://github.com/bmartel/calyx/compare/v1.10.1...v1.11.0) (2026-07-01)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **graphql,openapi:** support schema-first compiling, custom context and validation schema auto-enrichment ([e8dcce7](https://github.com/bmartel/calyx/commit/e8dcce78d75097ac88a8a3a984b5834d4d7f1818))
|
|
14
|
+
|
|
1
15
|
## [1.10.1](https://github.com/bmartel/calyx/compare/v1.10.0...v1.10.1) (2026-07-01)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -18,7 +18,9 @@ export class CacheInterceptor implements NestInterceptor {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const url = new URL(req.url);
|
|
21
|
-
const
|
|
21
|
+
const handler = context.getHandler();
|
|
22
|
+
const key = Reflect.getMetadata('cache_metadata_key', handler) ?? `http_cache::${url.pathname}${url.search}`;
|
|
23
|
+
const ttl = Reflect.getMetadata('cache_metadata_ttl', handler);
|
|
22
24
|
|
|
23
25
|
const cached = await this.cacheService.get(key);
|
|
24
26
|
if (cached !== undefined) {
|
|
@@ -26,7 +28,7 @@ export class CacheInterceptor implements NestInterceptor {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const result = await next.handle();
|
|
29
|
-
await this.cacheService.set(key, result);
|
|
31
|
+
await this.cacheService.set(key, result, ttl);
|
|
30
32
|
return result;
|
|
31
33
|
}
|
|
32
34
|
}
|
package/src/cache/index.ts
CHANGED
package/src/core/container.ts
CHANGED
|
@@ -47,14 +47,14 @@ class ContainerModuleRef extends ModuleRef {
|
|
|
47
47
|
const requestContext = contextId instanceof Map ? contextId : new Map<any, any>();
|
|
48
48
|
const strict = options?.strict ?? true;
|
|
49
49
|
if (strict) {
|
|
50
|
-
return this.container.
|
|
50
|
+
return await this.container.resolveTokenInModuleContextAsync(this.moduleClass, token, requestContext);
|
|
51
51
|
} else {
|
|
52
|
-
return this.container.
|
|
52
|
+
return await this.container.resolveTokenGloballyAsync(token, requestContext);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async create<T>(type: Type<T>): Promise<T> {
|
|
57
|
-
return this.container.
|
|
57
|
+
return await this.container.instantiateClassAsync(type, this.moduleClass);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -288,6 +288,8 @@ export class CalyxContainer {
|
|
|
288
288
|
private createInstanceFromProvider(provider: Provider, targetModuleClass: any, requestContext?: Map<any, any>): any {
|
|
289
289
|
if (typeof provider !== 'function' && 'useValue' in provider) {
|
|
290
290
|
return provider.useValue;
|
|
291
|
+
} else if (typeof provider !== 'function' && 'useExisting' in provider) {
|
|
292
|
+
return this.resolveTokenInModuleContext(targetModuleClass, resolveForwardRef(provider.useExisting), requestContext);
|
|
291
293
|
} else if (typeof provider !== 'function' && 'useClass' in provider) {
|
|
292
294
|
return this.instantiateClass(provider.useClass, targetModuleClass, requestContext);
|
|
293
295
|
} else if (typeof provider !== 'function' && 'useFactory' in provider) {
|
|
@@ -415,13 +417,31 @@ export class CalyxContainer {
|
|
|
415
417
|
throw new Error(`calyx DI: Instance of "${String(token.name ?? token)}" not found in any module context`);
|
|
416
418
|
}
|
|
417
419
|
|
|
418
|
-
bootstrap(rootModule: any) {
|
|
420
|
+
bootstrap(rootModule: any): void | Promise<void> {
|
|
419
421
|
this.addModule(rootModule);
|
|
420
|
-
|
|
421
|
-
// Compute scopes of all providers and controllers (including bubble-up)
|
|
422
422
|
this.resolveProviderAndControllerScopes();
|
|
423
423
|
|
|
424
|
-
|
|
424
|
+
let hasAsync = false;
|
|
425
|
+
for (const record of this.modules.values()) {
|
|
426
|
+
for (const provider of record.providers.values()) {
|
|
427
|
+
if (typeof provider !== 'function' && 'useFactory' in provider) {
|
|
428
|
+
if (provider.useFactory.constructor.name === 'AsyncFunction') {
|
|
429
|
+
hasAsync = true;
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (hasAsync) break;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (hasAsync) {
|
|
438
|
+
return this.bootstrapAsync(rootModule);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this.bootstrapSync(rootModule);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private bootstrapSync(rootModule: any) {
|
|
425
445
|
for (const [moduleClass, record] of this.modules.entries()) {
|
|
426
446
|
for (const token of record.providers.keys()) {
|
|
427
447
|
const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
|
|
@@ -431,7 +451,6 @@ export class CalyxContainer {
|
|
|
431
451
|
}
|
|
432
452
|
}
|
|
433
453
|
|
|
434
|
-
// Instantiate all singleton controllers
|
|
435
454
|
for (const [moduleClass, record] of this.modules.entries()) {
|
|
436
455
|
for (const controllerClass of record.controllers) {
|
|
437
456
|
const scope = this.controllerScopes.get(controllerClass) ?? Scope.DEFAULT;
|
|
@@ -441,7 +460,6 @@ export class CalyxContainer {
|
|
|
441
460
|
}
|
|
442
461
|
}
|
|
443
462
|
|
|
444
|
-
// Instantiate module classes themselves
|
|
445
463
|
for (const [moduleClass, record] of this.modules.entries()) {
|
|
446
464
|
if (record.instances.get(moduleClass) === null) {
|
|
447
465
|
const instance = this.instantiateClass(moduleClass, moduleClass);
|
|
@@ -452,6 +470,35 @@ export class CalyxContainer {
|
|
|
452
470
|
this.compileControllerFactories();
|
|
453
471
|
}
|
|
454
472
|
|
|
473
|
+
private async bootstrapAsync(rootModule: any) {
|
|
474
|
+
for (const [moduleClass, record] of this.modules.entries()) {
|
|
475
|
+
for (const token of record.providers.keys()) {
|
|
476
|
+
const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
|
|
477
|
+
if (scope === Scope.DEFAULT) {
|
|
478
|
+
await this.resolveTokenInModuleContextAsync(moduleClass, token);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
for (const [moduleClass, record] of this.modules.entries()) {
|
|
484
|
+
for (const controllerClass of record.controllers) {
|
|
485
|
+
const scope = this.controllerScopes.get(controllerClass) ?? Scope.DEFAULT;
|
|
486
|
+
if (scope === Scope.DEFAULT) {
|
|
487
|
+
await this.resolveControllerAsync(moduleClass, controllerClass);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
for (const [moduleClass, record] of this.modules.entries()) {
|
|
493
|
+
if (record.instances.get(moduleClass) === null) {
|
|
494
|
+
const instance = await this.instantiateClassAsync(moduleClass, moduleClass);
|
|
495
|
+
record.instances.set(moduleClass, instance);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
this.compileControllerFactories();
|
|
500
|
+
}
|
|
501
|
+
|
|
455
502
|
private resolveController(moduleClass: any, controllerClass: any): any {
|
|
456
503
|
const record = this.modules.get(moduleClass)!;
|
|
457
504
|
if (record.instances.has(controllerClass)) {
|
|
@@ -462,6 +509,16 @@ export class CalyxContainer {
|
|
|
462
509
|
return instance;
|
|
463
510
|
}
|
|
464
511
|
|
|
512
|
+
private async resolveControllerAsync(moduleClass: any, controllerClass: any): Promise<any> {
|
|
513
|
+
const record = this.modules.get(moduleClass)!;
|
|
514
|
+
if (record.instances.has(controllerClass)) {
|
|
515
|
+
return record.instances.get(controllerClass);
|
|
516
|
+
}
|
|
517
|
+
const instance = await this.instantiateClassAsync(controllerClass, moduleClass);
|
|
518
|
+
record.instances.set(controllerClass, instance);
|
|
519
|
+
return instance;
|
|
520
|
+
}
|
|
521
|
+
|
|
465
522
|
resolveTokenGlobally<T>(token: InjectionToken, requestContext?: Map<any, any>): T {
|
|
466
523
|
const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
|
|
467
524
|
if (scope === Scope.REQUEST && requestContext?.has(token)) {
|
|
@@ -475,6 +532,180 @@ export class CalyxContainer {
|
|
|
475
532
|
return this.getGlobalOrAnyInstance(token);
|
|
476
533
|
}
|
|
477
534
|
|
|
535
|
+
async resolveTokenGloballyAsync<T>(token: InjectionToken, requestContext?: Map<any, any>): Promise<T> {
|
|
536
|
+
const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
|
|
537
|
+
if (scope === Scope.REQUEST && requestContext?.has(token)) {
|
|
538
|
+
return requestContext.get(token);
|
|
539
|
+
}
|
|
540
|
+
for (const [moduleClass, record] of this.modules.entries()) {
|
|
541
|
+
if (record.providers.has(token)) {
|
|
542
|
+
return await this.resolveTokenInModuleContextAsync(moduleClass, token, requestContext);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return this.getGlobalOrAnyInstance(token);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async resolveTokenInModuleContextAsync<T>(moduleClass: any, token: InjectionToken, requestContext?: Map<any, any>): Promise<T> {
|
|
549
|
+
const isResolving = this.resolvingStack.some(
|
|
550
|
+
(item) => item.moduleClass === moduleClass && item.token === token
|
|
551
|
+
);
|
|
552
|
+
if (isResolving) {
|
|
553
|
+
const path = this.resolvingStack
|
|
554
|
+
.map((item) => `${item.moduleClass.name ?? item.moduleClass}::${String(item.token.name ?? item.token)}`)
|
|
555
|
+
.join(' -> ');
|
|
556
|
+
throw new Error(`Circular dependency detected: ${path} -> ${moduleClass.name ?? moduleClass}::${String(token.name ?? token)}`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
this.resolvingStack.push({ moduleClass, token });
|
|
560
|
+
|
|
561
|
+
try {
|
|
562
|
+
const record = this.modules.get(moduleClass);
|
|
563
|
+
if (!record) {
|
|
564
|
+
throw new Error(`Module ${moduleClass.name || moduleClass} is not registered in the container`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (token === REQUEST) {
|
|
568
|
+
if (!requestContext || !requestContext.has(REQUEST)) {
|
|
569
|
+
throw new Error(`calyx DI: REQUEST token resolved outside of a request context`);
|
|
570
|
+
}
|
|
571
|
+
return requestContext.get(REQUEST);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (token === ModuleRef) {
|
|
575
|
+
const instance = record.instances.get(ModuleRef);
|
|
576
|
+
if (instance) return instance;
|
|
577
|
+
const moduleRefInstance = new ContainerModuleRef(this, moduleClass);
|
|
578
|
+
record.instances.set(ModuleRef, moduleRefInstance);
|
|
579
|
+
return moduleRefInstance as any;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (token === moduleClass) {
|
|
583
|
+
const instance = record.instances.get(moduleClass);
|
|
584
|
+
if (instance) return instance;
|
|
585
|
+
const instantiatedModule = await this.instantiateClassAsync(moduleClass, moduleClass, requestContext);
|
|
586
|
+
record.instances.set(moduleClass, instantiatedModule);
|
|
587
|
+
return instantiatedModule;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const resolution = this.findProviderDefinition(moduleClass, token);
|
|
591
|
+
if (!resolution) {
|
|
592
|
+
const currentResolving = this.resolvingStack[this.resolvingStack.length - 2];
|
|
593
|
+
if (currentResolving) {
|
|
594
|
+
const parentClass = currentResolving.token;
|
|
595
|
+
if (typeof parentClass === 'function') {
|
|
596
|
+
const optionalParams: Set<number> = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, parentClass) || new Set();
|
|
597
|
+
const injectTokens: Map<number, InjectionToken> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, parentClass) || new Map();
|
|
598
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', parentClass) || [];
|
|
599
|
+
|
|
600
|
+
let isOptional = false;
|
|
601
|
+
for (let i = 0; i < paramTypes.length; i++) {
|
|
602
|
+
const pToken = injectTokens.get(i) ?? paramTypes[i];
|
|
603
|
+
const resolvedPToken = resolveForwardRef(pToken);
|
|
604
|
+
if (resolvedPToken === token && optionalParams.has(i)) {
|
|
605
|
+
isOptional = true;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (isOptional) {
|
|
610
|
+
return undefined as any;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
throw new Error(`calyx DI: Cannot resolve dependency "${String(token.name ?? token)}" in module ${moduleClass.name || moduleClass}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const { targetModuleClass, provider } = resolution;
|
|
618
|
+
const targetRecord = this.modules.get(targetModuleClass)!;
|
|
619
|
+
|
|
620
|
+
const scope = this.providerScopes.get(token) ?? Scope.DEFAULT;
|
|
621
|
+
|
|
622
|
+
if (scope === Scope.REQUEST) {
|
|
623
|
+
if (!requestContext) {
|
|
624
|
+
throw new Error(`calyx DI: Cannot resolve request-scoped provider "${String(token.name ?? token)}" without request context.`);
|
|
625
|
+
}
|
|
626
|
+
if (requestContext.has(token)) {
|
|
627
|
+
return requestContext.get(token);
|
|
628
|
+
}
|
|
629
|
+
const instance = await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
|
|
630
|
+
requestContext.set(token, instance);
|
|
631
|
+
return instance;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (scope === Scope.TRANSIENT) {
|
|
635
|
+
return await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (targetRecord.instances.has(token)) {
|
|
639
|
+
return targetRecord.instances.get(token);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const instance = await this.createInstanceFromProviderAsync(provider, targetModuleClass, requestContext);
|
|
643
|
+
targetRecord.instances.set(token, instance);
|
|
644
|
+
return instance;
|
|
645
|
+
} finally {
|
|
646
|
+
this.resolvingStack.pop();
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private async createInstanceFromProviderAsync(provider: Provider, targetModuleClass: any, requestContext?: Map<any, any>): Promise<any> {
|
|
651
|
+
if (typeof provider !== 'function' && 'useValue' in provider) {
|
|
652
|
+
return provider.useValue;
|
|
653
|
+
} else if (typeof provider !== 'function' && 'useExisting' in provider) {
|
|
654
|
+
return await this.resolveTokenInModuleContextAsync(targetModuleClass, resolveForwardRef(provider.useExisting), requestContext);
|
|
655
|
+
} else if (typeof provider !== 'function' && 'useClass' in provider) {
|
|
656
|
+
return await this.instantiateClassAsync(provider.useClass, targetModuleClass, requestContext);
|
|
657
|
+
} else if (typeof provider !== 'function' && 'useFactory' in provider) {
|
|
658
|
+
const injectTokens = provider.inject || [];
|
|
659
|
+
const args = await Promise.all(
|
|
660
|
+
injectTokens.map((t) => this.resolveTokenInModuleContextAsync(targetModuleClass, resolveForwardRef(t), requestContext))
|
|
661
|
+
);
|
|
662
|
+
const res = provider.useFactory(...args);
|
|
663
|
+
return res instanceof Promise ? await res : res;
|
|
664
|
+
} else {
|
|
665
|
+
return await this.instantiateClassAsync(provider as Type<any>, targetModuleClass, requestContext);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
public async instantiateClassAsync(Class: Type<any>, moduleClass: any, requestContext?: Map<any, any>): Promise<any> {
|
|
670
|
+
if (
|
|
671
|
+
!Reflect.hasMetadata(METADATA_KEYS.INJECTABLE, Class) &&
|
|
672
|
+
!Reflect.hasMetadata(METADATA_KEYS.CONTROLLER, Class) &&
|
|
673
|
+
!Reflect.hasMetadata(METADATA_KEYS.MODULE, Class)
|
|
674
|
+
) {
|
|
675
|
+
throw new Error(`Class ${Class.name} is missing @Injectable(), @Controller() or @Module() decorator`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', Class) || [];
|
|
679
|
+
const injectTokens: Map<number, InjectionToken> = Reflect.getMetadata(METADATA_KEYS.INJECT_TOKENS, Class) || new Map();
|
|
680
|
+
const optionalParams: Set<number> = Reflect.getMetadata(METADATA_KEYS.OPTIONAL_PARAMS, Class) || new Set();
|
|
681
|
+
|
|
682
|
+
const args = await Promise.all(
|
|
683
|
+
paramTypes.map(async (paramType, i) => {
|
|
684
|
+
const token = injectTokens.get(i) ?? paramType;
|
|
685
|
+
const resolvedToken = resolveForwardRef(token);
|
|
686
|
+
|
|
687
|
+
try {
|
|
688
|
+
return await this.resolveTokenInModuleContextAsync(moduleClass, resolvedToken, requestContext);
|
|
689
|
+
} catch (err) {
|
|
690
|
+
if (optionalParams.has(i)) {
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
throw err;
|
|
694
|
+
}
|
|
695
|
+
})
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
const instance = new Class(...args);
|
|
699
|
+
|
|
700
|
+
const propertyInjects: Map<string | symbol, InjectionToken> =
|
|
701
|
+
Reflect.getOwnMetadata(METADATA_KEYS.PROPERTY_INJECTS, Class) || new Map();
|
|
702
|
+
for (const [propertyKey, token] of propertyInjects.entries()) {
|
|
703
|
+
instance[propertyKey] = await this.resolveTokenInModuleContextAsync(moduleClass, token, requestContext);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return instance;
|
|
707
|
+
}
|
|
708
|
+
|
|
478
709
|
resolveControllerInRequestContext(moduleClass: any, controllerClass: any, requestContext: Map<any, any>): any {
|
|
479
710
|
const scope = this.controllerScopes.get(controllerClass) ?? Scope.DEFAULT;
|
|
480
711
|
if (scope === Scope.REQUEST) {
|
|
@@ -668,6 +899,8 @@ export class CalyxContainer {
|
|
|
668
899
|
let valExpr = '';
|
|
669
900
|
if (typeof provider !== 'function' && 'useValue' in provider) {
|
|
670
901
|
valExpr = `c[${getClosureIdx(provider.useValue)}]`;
|
|
902
|
+
} else if (typeof provider !== 'function' && 'useExisting' in provider) {
|
|
903
|
+
return compileToken(resolveForwardRef(provider.useExisting), targetModuleClass);
|
|
671
904
|
} else {
|
|
672
905
|
const ClassToInstantiate = typeof provider === 'function'
|
|
673
906
|
? provider
|
package/src/core/index.ts
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Injectable } from './decorators.ts';
|
|
2
|
+
import { CalyxContainer } from './container.ts';
|
|
3
|
+
import { ModuleRef } from './module-ref.ts';
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
export class LazyModuleLoader {
|
|
7
|
+
constructor(private readonly moduleRef: ModuleRef) {}
|
|
8
|
+
|
|
9
|
+
async load(loader: () => Promise<any> | any): Promise<ModuleRef> {
|
|
10
|
+
let moduleClass = await loader();
|
|
11
|
+
if (moduleClass && typeof moduleClass === 'object' && 'default' in moduleClass) {
|
|
12
|
+
moduleClass = moduleClass.default;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const container: CalyxContainer = (this.moduleRef as any).container;
|
|
16
|
+
if (!container) {
|
|
17
|
+
throw new Error('LazyModuleLoader: Container reference not found on moduleRef');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
await container.bootstrap(moduleClass);
|
|
21
|
+
|
|
22
|
+
const record = container.getModuleRecord(moduleClass);
|
|
23
|
+
if (!record) {
|
|
24
|
+
throw new Error(`LazyModuleLoader: Module record not found for ${moduleClass.name}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return record.instances.get(ModuleRef);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/core/metadata.ts
CHANGED
|
@@ -44,7 +44,12 @@ export interface FactoryProvider {
|
|
|
44
44
|
scope?: Scope;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export
|
|
47
|
+
export interface ExistingProvider {
|
|
48
|
+
provide: InjectionToken;
|
|
49
|
+
useExisting: InjectionToken;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type Provider = Type<any> | ValueProvider | ClassProvider | FactoryProvider | ExistingProvider;
|
|
48
53
|
|
|
49
54
|
export interface ModuleMetadata {
|
|
50
55
|
imports?: any[];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { CalyxContainer } from './container.ts';
|
|
2
|
+
import { ModuleMetadata, Type, InjectionToken } from './metadata.ts';
|
|
3
|
+
import { Module } from './decorators.ts';
|
|
4
|
+
import { ModuleRef } from './module-ref.ts';
|
|
5
|
+
import { CalyxApplication } from '../http/application.ts';
|
|
6
|
+
|
|
7
|
+
export class TestingModule extends ModuleRef {
|
|
8
|
+
constructor(public readonly container: CalyxContainer, private readonly rootModuleClass: any) {
|
|
9
|
+
super();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
createCalyxApplication(): CalyxApplication {
|
|
13
|
+
const app = new CalyxApplication(this.rootModuleClass);
|
|
14
|
+
(app as any).container = this.container;
|
|
15
|
+
return app;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get<T>(token: InjectionToken, options?: { strict: boolean }): T {
|
|
19
|
+
const strict = options?.strict ?? false;
|
|
20
|
+
if (strict) {
|
|
21
|
+
return this.container.resolveTokenInModuleContext(this.rootModuleClass, token);
|
|
22
|
+
} else {
|
|
23
|
+
return this.container.getGlobalOrAnyInstance(token);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async resolve<T>(token: InjectionToken, contextId?: any, options?: { strict: boolean }): Promise<T> {
|
|
28
|
+
const requestContext = contextId instanceof Map ? contextId : new Map<any, any>();
|
|
29
|
+
const strict = options?.strict ?? false;
|
|
30
|
+
if (strict) {
|
|
31
|
+
return await this.container.resolveTokenInModuleContextAsync(this.rootModuleClass, token, requestContext);
|
|
32
|
+
} else {
|
|
33
|
+
return await this.container.resolveTokenGloballyAsync(token, requestContext);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async create<T>(type: Type<T>): Promise<T> {
|
|
38
|
+
return await this.container.instantiateClassAsync(type, this.rootModuleClass);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async close() {
|
|
42
|
+
const instances = this.container.getProviderAndControllerInstances();
|
|
43
|
+
for (const inst of instances) {
|
|
44
|
+
if (inst && typeof inst.onModuleDestroy === 'function') {
|
|
45
|
+
await inst.onModuleDestroy();
|
|
46
|
+
}
|
|
47
|
+
if (inst && typeof inst.onApplicationShutdown === 'function') {
|
|
48
|
+
await inst.onApplicationShutdown();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class TestingModuleBuilder {
|
|
55
|
+
private overrides = new Map<InjectionToken, { useValue?: any; useClass?: any; useFactory?: any; inject?: any[] }>();
|
|
56
|
+
|
|
57
|
+
constructor(private readonly metadata: ModuleMetadata) {}
|
|
58
|
+
|
|
59
|
+
overrideProvider(token: InjectionToken) {
|
|
60
|
+
const builder = {
|
|
61
|
+
useValue: (value: any) => {
|
|
62
|
+
this.overrides.set(token, { useValue: value });
|
|
63
|
+
return this;
|
|
64
|
+
},
|
|
65
|
+
useClass: (clazz: any) => {
|
|
66
|
+
this.overrides.set(token, { useClass: clazz });
|
|
67
|
+
return this;
|
|
68
|
+
},
|
|
69
|
+
useFactory: (options: { factory: (...args: any[]) => any; inject?: any[] }) => {
|
|
70
|
+
this.overrides.set(token, { useFactory: options.factory, inject: options.inject });
|
|
71
|
+
return this;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
return builder;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
overrideGuard(token: any) { return this.overrideProvider(token); }
|
|
78
|
+
overrideInterceptor(token: any) { return this.overrideProvider(token); }
|
|
79
|
+
overridePipe(token: any) { return this.overrideProvider(token); }
|
|
80
|
+
overrideFilter(token: any) { return this.overrideProvider(token); }
|
|
81
|
+
|
|
82
|
+
async compile(): Promise<TestingModule> {
|
|
83
|
+
@Module({
|
|
84
|
+
imports: this.metadata.imports || [],
|
|
85
|
+
controllers: this.metadata.controllers || [],
|
|
86
|
+
providers: this.metadata.providers || [],
|
|
87
|
+
exports: this.metadata.exports || [],
|
|
88
|
+
})
|
|
89
|
+
class RootTestModule {}
|
|
90
|
+
|
|
91
|
+
const container = new CalyxContainer();
|
|
92
|
+
container.addModule(RootTestModule);
|
|
93
|
+
|
|
94
|
+
// Apply overrides to module records
|
|
95
|
+
for (const [token, override] of this.overrides.entries()) {
|
|
96
|
+
for (const record of (container as any).modules.values()) {
|
|
97
|
+
if (record.providers.has(token)) {
|
|
98
|
+
if ('useValue' in override) {
|
|
99
|
+
record.providers.set(token, { provide: token, useValue: override.useValue });
|
|
100
|
+
} else if ('useClass' in override) {
|
|
101
|
+
record.providers.set(token, { provide: token, useClass: override.useClass });
|
|
102
|
+
} else if ('useFactory' in override) {
|
|
103
|
+
record.providers.set(token, { provide: token, useFactory: override.useFactory, inject: override.inject });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await container.bootstrap(RootTestModule);
|
|
110
|
+
|
|
111
|
+
return new TestingModule(container, RootTestModule);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export class Test {
|
|
116
|
+
static createTestingModule(metadata: ModuleMetadata): TestingModuleBuilder {
|
|
117
|
+
return new TestingModuleBuilder(metadata);
|
|
118
|
+
}
|
|
119
|
+
}
|