Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(testing): support override modules in test module builder #8777

52 changes: 49 additions & 3 deletions packages/core/injector/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import { CircularDependencyException } from '../errors/exceptions/circular-depen
import { UndefinedForwardRefException } from '../errors/exceptions/undefined-forwardref.exception';
import { UnknownModuleException } from '../errors/exceptions/unknown-module.exception';
import { REQUEST } from '../router/request/request-constants';
import { ModuleCompiler } from './compiler';
import { ModuleCompiler, ModuleFactory } from './compiler';
import { ContextId } from './instance-wrapper';
import { InternalCoreModule } from './internal-core-module';
import { InternalProvidersStorage } from './internal-providers-storage';
import { Module } from './module';
import { ModuleTokenFactory } from './module-token-factory';
import { ModulesContainer } from './modules-container';

type ModuleMetatype = Type<any> | DynamicModule | Promise<DynamicModule>;
type ModuleScope = Type<any>[];

export class NestContainer {
private readonly globalModules = new Set<Module>();
private readonly moduleTokenFactory = new ModuleTokenFactory();
Expand Down Expand Up @@ -54,8 +57,8 @@ export class NestContainer {
}

public async addModule(
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
scope: Type<any>[],
metatype: ModuleMetatype,
scope: ModuleScope,
): Promise<Module | undefined> {
// In DependenciesScanner#scanForModules we already check for undefined or invalid modules
// We still need to catch the edge-case of `forwardRef(() => undefined)`
Expand All @@ -68,8 +71,50 @@ export class NestContainer {
if (this.modules.has(token)) {
return this.modules.get(token);
}

return this.setModule(
{
token,
type,
dynamicMetadata,
},
scope,
);
}

public async replaceModule(
metatypeToReplace: ModuleMetatype,
newMetatype: ModuleMetatype,
scope: ModuleScope,
): Promise<Module | undefined> {
// In DependenciesScanner#scanForModules we already check for undefined or invalid modules
// We still need to catch the edge-case of `forwardRef(() => undefined)`
if (!metatypeToReplace || !newMetatype) {
throw new UndefinedForwardRefException(scope);
}

const { token } = await this.moduleCompiler.compile(metatypeToReplace);
const { type, dynamicMetadata } = await this.moduleCompiler.compile(
newMetatype,
);

return this.setModule(
{
token,
type,
dynamicMetadata,
},
scope,
);
}

private async setModule(
{ token, dynamicMetadata, type }: ModuleFactory,
scope: ModuleScope,
): Promise<Module | undefined> {
const moduleRef = new Module(type, this);
moduleRef.token = token;

this.modules.set(token, moduleRef);

await this.addDynamicMetadata(
Expand All @@ -81,6 +126,7 @@ export class NestContainer {
if (this.isGlobalModule(type, dynamicMetadata)) {
this.addGlobalModule(moduleRef);
}

return moduleRef;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/core/injector/lazy-module-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export class LazyModuleLoader {
| DynamicModule,
): Promise<ModuleRef> {
const moduleClassOrDynamicDefinition = await loaderFn();
const moduleInstances = await this.dependenciesScanner.scanForModules(
moduleClassOrDynamicDefinition,
);
const moduleInstances = await this.dependenciesScanner.scanForModules({
moduleDefinition: moduleClassOrDynamicDefinition,
});
if (moduleInstances.length === 0) {
// The module has been loaded already. In this case, we must
// retrieve a module reference from the exising container.
Expand Down
102 changes: 85 additions & 17 deletions packages/core/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ interface ApplicationProviderWrapper {
scope?: Scope;
}

export type ModuleDefinition =
| ForwardReference
| Type<unknown>
| DynamicModule
| Promise<DynamicModule>;

export interface ModuleToOverride {
moduleToReplace: ModuleDefinition;
newModule: ModuleDefinition;
}

interface ModulesScanParamaters {
moduleDefinition: ModuleDefinition;
scope?: Type<unknown>[];
ctxRegistry?: (ForwardReference | DynamicModule | Type<unknown>)[];
modulesToOverride?: ModuleToOverride[];
}

export class DependenciesScanner {
private readonly applicationProvidersApplyMap: ApplicationProviderWrapper[] =
[];
Expand All @@ -63,26 +81,30 @@ export class DependenciesScanner {
private readonly applicationConfig = new ApplicationConfig(),
) {}

public async scan(module: Type<any>) {
public async scan(module: Type<any>, modulesToOverride?: ModuleToOverride[]) {
await this.registerCoreModule();
await this.scanForModules(module);
await this.scanForModules({ moduleDefinition: module, modulesToOverride });
await this.scanModulesForDependencies();
this.calculateModulesDistance();

this.addScopedEnhancersMetadata();
this.container.bindGlobalScope();
}

public async scanForModules(
moduleDefinition:
| ForwardReference
| Type<unknown>
| DynamicModule
| Promise<DynamicModule>,
scope: Type<unknown>[] = [],
ctxRegistry: (ForwardReference | DynamicModule | Type<unknown>)[] = [],
): Promise<Module[]> {
const moduleInstance = await this.insertModule(moduleDefinition, scope);
public async scanForModules({
moduleDefinition,
scope = [],
ctxRegistry = [],
modulesToOverride = [],
}: ModulesScanParamaters): Promise<Module[]> {
const moduleInstance = await this.putModule(
moduleDefinition,
modulesToOverride,
scope,
);
moduleDefinition =
this.getOverrideModuleByModule(moduleDefinition, modulesToOverride)
?.newModule ?? moduleDefinition;
moduleDefinition =
moduleDefinition instanceof Promise
? await moduleDefinition
Expand Down Expand Up @@ -119,11 +141,12 @@ export class DependenciesScanner {
if (ctxRegistry.includes(innerModule)) {
continue;
}
const moduleRefs = await this.scanForModules(
innerModule,
[].concat(scope, moduleDefinition),
const moduleRefs = await this.scanForModules({
moduleDefinition: innerModule,
scope: [].concat(scope, moduleDefinition),
ctxRegistry,
);
modulesToOverride,
});
registeredModuleRefs = registeredModuleRefs.concat(moduleRefs);
}
if (!moduleInstance) {
Expand Down Expand Up @@ -409,7 +432,7 @@ export class DependenciesScanner {
this.container.getModuleCompiler(),
this.container.getHttpAdapterHostRef(),
);
const [instance] = await this.scanForModules(moduleDefinition);
const [instance] = await this.scanForModules({ moduleDefinition });
this.container.registerCoreModuleRef(instance);
}

Expand Down Expand Up @@ -515,4 +538,49 @@ export class DependenciesScanner {
private isRequestOrTransient(scope: Scope): boolean {
return scope === Scope.REQUEST || scope === Scope.TRANSIENT;
}

private putModule(
moduleDefinition: ModuleDefinition,
modulesToOverride: ModuleToOverride[],
scope: Type<unknown>[],
): Promise<Module | undefined> {
const overrideModule = this.getOverrideModuleByModule(
moduleDefinition,
modulesToOverride,
);
if (overrideModule !== undefined) {
return this.overrideModule(
moduleDefinition,
overrideModule.newModule,
scope,
);
}

return this.insertModule(moduleDefinition, scope);
}

private getOverrideModuleByModule(
module: ModuleDefinition,
modulesToOverride: ModuleToOverride[],
): ModuleToOverride | undefined {
return modulesToOverride.find(
moduleToOverride => moduleToOverride.moduleToReplace === module,
);
}

private async overrideModule(
moduleToOverride: any,
newModule: any,
scope: Type<unknown>[],
): Promise<Module | undefined> {
if (newModule && newModule.forwardRef && moduleToOverride.forwardRef) {
return this.container.replaceModule(
moduleToOverride.forwardRef(),
newModule.forwardRef(),
scope,
);
}

return this.container.replaceModule(moduleToOverride, newModule, scope);
}
}
31 changes: 31 additions & 0 deletions packages/core/test/injector/container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,37 @@ describe('NestContainer', () => {
expect(addGlobalModuleSpy.calledOnce).to.be.true;
});
});

describe('replaceModule', () => {
it('should replace module if already exists in collection', async () => {
@Module({})
class ReplaceTestModule {}

const modules = new Map();
const setSpy = sinon.spy(modules, 'set');
(container as any).modules = modules;

await container.addModule(TestModule as any, []);
await container.replaceModule(
TestModule as any,
ReplaceTestModule as any,
[],
);

expect(setSpy.calledTwice).to.be.true;
});

it('should throws an exception when metatype is not defined', () => {
expect(container.addModule(undefined, [])).to.eventually.throws();
});

it('should add global module when module is global', async () => {
const addGlobalModuleSpy = sinon.spy(container, 'addGlobalModule');
await container.addModule(GlobalTestModule as any, []);
expect(addGlobalModuleSpy.calledOnce).to.be.true;
});
});

describe('isGlobalModule', () => {
describe('when module is not globally scoped', () => {
it('should return false', () => {
Expand Down
Loading