Files
nest/packages/core/scanner.ts
2024-11-26 12:46:59 +01:00

763 lines
22 KiB
TypeScript

import { DynamicModule, ForwardReference, Provider } from '@nestjs/common';
import {
CATCH_WATERMARK,
CONTROLLER_WATERMARK,
ENHANCER_KEY_TO_SUBTYPE_MAP,
EXCEPTION_FILTERS_METADATA,
EnhancerSubtype,
GUARDS_METADATA,
INJECTABLE_WATERMARK,
INTERCEPTORS_METADATA,
MODULE_METADATA,
PIPES_METADATA,
ROUTE_ARGS_METADATA,
} from '@nestjs/common/constants';
import {
CanActivate,
ClassProvider,
Controller,
ExceptionFilter,
ExistingProvider,
FactoryProvider,
Injectable,
InjectionToken,
NestInterceptor,
PipeTransform,
Scope,
Type,
ValueProvider,
} from '@nestjs/common/interfaces';
import {
isFunction,
isNil,
isUndefined,
} from '@nestjs/common/utils/shared.utils';
import { iterate } from 'iterare';
import { ApplicationConfig } from './application-config';
import {
APP_FILTER,
APP_GUARD,
APP_INTERCEPTOR,
APP_PIPE,
ENHANCER_TOKEN_TO_SUBTYPE_MAP,
} from './constants';
import { CircularDependencyException } from './errors/exceptions/circular-dependency.exception';
import { InvalidClassModuleException } from './errors/exceptions/invalid-class-module.exception';
import { InvalidModuleException } from './errors/exceptions/invalid-module.exception';
import { UndefinedModuleException } from './errors/exceptions/undefined-module.exception';
import { getClassScope } from './helpers/get-class-scope';
import { NestContainer } from './injector/container';
import { InstanceWrapper } from './injector/instance-wrapper';
import { InternalCoreModuleFactory } from './injector/internal-core-module/internal-core-module-factory';
import { Module } from './injector/module';
import { GraphInspector } from './inspector/graph-inspector';
import { UuidFactory } from './inspector/uuid-factory';
import { ModuleDefinition } from './interfaces/module-definition.interface';
import { ModuleOverride } from './interfaces/module-override.interface';
import { MetadataScanner } from './metadata-scanner';
interface ApplicationProviderWrapper {
moduleKey: string;
providerKey: string;
type: InjectionToken;
scope?: Scope;
}
interface ModulesScanParameters {
moduleDefinition: ModuleDefinition;
scope?: Type<unknown>[];
ctxRegistry?: (ForwardReference | DynamicModule | Type<unknown>)[];
overrides?: ModuleOverride[];
lazy?: boolean;
}
export class DependenciesScanner {
private readonly applicationProvidersApplyMap: ApplicationProviderWrapper[] =
[];
constructor(
private readonly container: NestContainer,
private readonly metadataScanner: MetadataScanner,
private readonly graphInspector: GraphInspector,
private readonly applicationConfig = new ApplicationConfig(),
) {}
public async scan(
module: Type<any>,
options?: { overrides?: ModuleOverride[] },
) {
await this.registerCoreModule(options?.overrides);
await this.scanForModules({
moduleDefinition: module,
overrides: options?.overrides,
});
await this.scanModulesForDependencies();
this.addScopedEnhancersMetadata();
this.container.bindGlobalScope();
this.calculateModulesDistance();
}
public async scanForModules({
moduleDefinition,
lazy,
scope = [],
ctxRegistry = [],
overrides = [],
}: ModulesScanParameters): Promise<Module[]> {
const { moduleRef: moduleInstance, inserted: moduleInserted } =
(await this.insertOrOverrideModule(moduleDefinition, overrides, scope)) ??
{};
moduleDefinition =
this.getOverrideModuleByModule(moduleDefinition, overrides)?.newModule ??
moduleDefinition;
moduleDefinition =
moduleDefinition instanceof Promise
? await moduleDefinition
: moduleDefinition;
ctxRegistry.push(moduleDefinition);
if (this.isForwardReference(moduleDefinition)) {
moduleDefinition = (moduleDefinition as ForwardReference).forwardRef();
}
const modules = !this.isDynamicModule(
moduleDefinition as Type<any> | DynamicModule,
)
? this.reflectMetadata(
MODULE_METADATA.IMPORTS,
moduleDefinition as Type<any>,
)
: [
...this.reflectMetadata(
MODULE_METADATA.IMPORTS,
(moduleDefinition as DynamicModule).module,
),
...((moduleDefinition as DynamicModule).imports || []),
];
let registeredModuleRefs: Module[] = [];
for (const [index, innerModule] of modules.entries()) {
// In case of a circular dependency (ES module system), JavaScript will resolve the type to `undefined`.
if (innerModule === undefined) {
throw new UndefinedModuleException(moduleDefinition, index, scope);
}
if (!innerModule) {
throw new InvalidModuleException(moduleDefinition, index, scope);
}
if (ctxRegistry.includes(innerModule)) {
continue;
}
const moduleRefs = await this.scanForModules({
moduleDefinition: innerModule,
scope: ([] as Array<Type>).concat(scope, moduleDefinition as Type),
ctxRegistry,
overrides,
lazy,
});
registeredModuleRefs = registeredModuleRefs.concat(moduleRefs);
}
if (!moduleInstance) {
return registeredModuleRefs;
}
if (lazy && moduleInserted) {
this.container.bindGlobalsToImports(moduleInstance);
}
return [moduleInstance].concat(registeredModuleRefs);
}
public async insertModule(
moduleDefinition: any,
scope: Type<unknown>[],
): Promise<
| {
moduleRef: Module;
inserted: boolean;
}
| undefined
> {
const moduleToAdd = this.isForwardReference(moduleDefinition)
? moduleDefinition.forwardRef()
: moduleDefinition;
if (
this.isInjectable(moduleToAdd) ||
this.isController(moduleToAdd) ||
this.isExceptionFilter(moduleToAdd)
) {
throw new InvalidClassModuleException(moduleDefinition, scope);
}
return this.container.addModule(moduleToAdd, scope);
}
public async scanModulesForDependencies(
modules: Map<string, Module> = this.container.getModules(),
) {
for (const [token, { metatype }] of modules) {
await this.reflectImports(metatype, token, metatype.name);
this.reflectProviders(metatype, token);
this.reflectControllers(metatype, token);
this.reflectExports(metatype, token);
}
}
public async reflectImports(
module: Type<unknown>,
token: string,
context: string,
) {
const modules = [
...this.reflectMetadata(MODULE_METADATA.IMPORTS, module),
...this.container.getDynamicMetadataByToken(
token,
MODULE_METADATA.IMPORTS as 'imports',
)!,
];
for (const related of modules) {
await this.insertImport(related, token, context);
}
}
public reflectProviders(module: Type<any>, token: string) {
const providers = [
...this.reflectMetadata(MODULE_METADATA.PROVIDERS, module),
...this.container.getDynamicMetadataByToken(
token,
MODULE_METADATA.PROVIDERS as 'providers',
)!,
];
providers.forEach(provider => {
this.insertProvider(provider, token);
this.reflectDynamicMetadata(provider, token);
});
}
public reflectControllers(module: Type<any>, token: string) {
const controllers = [
...this.reflectMetadata(MODULE_METADATA.CONTROLLERS, module),
...this.container.getDynamicMetadataByToken(
token,
MODULE_METADATA.CONTROLLERS as 'controllers',
)!,
];
controllers.forEach(item => {
this.insertController(item, token);
this.reflectDynamicMetadata(item, token);
});
}
public reflectDynamicMetadata(cls: Type<Injectable>, token: string) {
if (!cls || !cls.prototype) {
return;
}
this.reflectInjectables(cls, token, GUARDS_METADATA);
this.reflectInjectables(cls, token, INTERCEPTORS_METADATA);
this.reflectInjectables(cls, token, EXCEPTION_FILTERS_METADATA);
this.reflectInjectables(cls, token, PIPES_METADATA);
this.reflectParamInjectables(cls, token, ROUTE_ARGS_METADATA);
}
public reflectExports(module: Type<unknown>, token: string) {
const exports = [
...this.reflectMetadata(MODULE_METADATA.EXPORTS, module),
...this.container.getDynamicMetadataByToken(
token,
MODULE_METADATA.EXPORTS as 'exports',
)!,
];
exports.forEach(exportedProvider =>
this.insertExportedProvider(exportedProvider, token),
);
}
public reflectInjectables(
component: Type<Injectable>,
token: string,
metadataKey: string,
) {
const controllerInjectables = this.reflectMetadata<Type<Injectable>>(
metadataKey,
component,
);
const methodInjectables = this.metadataScanner
.getAllMethodNames(component.prototype)
.reduce(
(acc, method) => {
const methodInjectable = this.reflectKeyMetadata(
component,
metadataKey,
method,
);
if (methodInjectable) {
acc.push(methodInjectable);
}
return acc;
},
[] as Array<{
methodKey: string;
metadata: Type<Injectable>[];
}>,
);
controllerInjectables.forEach(injectable =>
this.insertInjectable(
injectable,
token,
component,
ENHANCER_KEY_TO_SUBTYPE_MAP[metadataKey],
),
);
methodInjectables.forEach(methodInjectable => {
methodInjectable.metadata!.forEach(injectable =>
this.insertInjectable(
injectable,
token,
component,
ENHANCER_KEY_TO_SUBTYPE_MAP[metadataKey],
methodInjectable.methodKey!,
),
);
});
}
public reflectParamInjectables(
component: Type<Injectable>,
token: string,
metadataKey: string,
) {
const paramsMethods = this.metadataScanner.getAllMethodNames(
component.prototype,
);
paramsMethods.forEach(methodKey => {
const metadata: Record<
string,
{
index: number;
data: unknown;
pipes: Array<Type<PipeTransform> | PipeTransform>;
}
> = Reflect.getMetadata(metadataKey, component, methodKey);
if (!metadata) {
return;
}
const params = Object.values(metadata);
params
.map(item => item.pipes)
.flat(1)
.forEach(injectable =>
this.insertInjectable(
injectable,
token,
component,
'pipe',
methodKey,
),
);
});
}
public reflectKeyMetadata(
component: Type<Injectable>,
key: string,
methodKey: string,
): { methodKey: string; metadata: any } | undefined {
let prototype = component.prototype;
do {
const descriptor = Reflect.getOwnPropertyDescriptor(prototype, methodKey);
if (!descriptor) {
continue;
}
const metadata = Reflect.getMetadata(key, descriptor.value);
if (!metadata) {
return;
}
return { methodKey, metadata };
} while (
(prototype = Reflect.getPrototypeOf(prototype)) &&
prototype !== Object.prototype &&
prototype
);
return undefined;
}
public calculateModulesDistance() {
const modulesGenerator = this.container.getModules().values();
// Skip "InternalCoreModule" from calculating distance
modulesGenerator.next();
const calculateDistance = (
moduleRef: Module,
distance = 1,
modulesStack: Module[] = [],
) => {
const localModulesStack = [...modulesStack];
if (!moduleRef || localModulesStack.includes(moduleRef)) {
return;
}
localModulesStack.push(moduleRef);
const moduleImports = moduleRef.imports;
moduleImports.forEach(importedModuleRef => {
if (importedModuleRef) {
if (distance > importedModuleRef.distance) {
importedModuleRef.distance = distance;
}
calculateDistance(importedModuleRef, distance + 1, localModulesStack);
}
});
};
const rootModule = modulesGenerator.next().value;
calculateDistance(rootModule!);
}
public async insertImport(related: any, token: string, context: string) {
if (isUndefined(related)) {
throw new CircularDependencyException(context);
}
if (this.isForwardReference(related)) {
return this.container.addImport(related.forwardRef(), token);
}
await this.container.addImport(related, token);
}
public isCustomProvider(
provider: Provider,
): provider is
| ClassProvider
| ValueProvider
| FactoryProvider
| ExistingProvider {
return provider && !isNil((provider as any).provide);
}
public insertProvider(provider: Provider, token: string) {
const isCustomProvider = this.isCustomProvider(provider);
if (!isCustomProvider) {
return this.container.addProvider(provider, token);
}
const applyProvidersMap = this.getApplyProvidersMap();
const providersKeys = Object.keys(applyProvidersMap);
const type = provider.provide;
if (!providersKeys.includes(type as string)) {
return this.container.addProvider(provider as any, token);
}
const uuid = UuidFactory.get(type.toString());
const providerToken = `${type as string} (UUID: ${uuid})`;
let scope = (provider as ClassProvider | FactoryProvider).scope;
if (isNil(scope) && (provider as ClassProvider).useClass) {
scope = getClassScope((provider as ClassProvider).useClass);
}
this.applicationProvidersApplyMap.push({
type,
moduleKey: token,
providerKey: providerToken,
scope,
});
const newProvider = {
...provider,
provide: providerToken,
scope,
} as Provider;
const enhancerSubtype =
ENHANCER_TOKEN_TO_SUBTYPE_MAP[
type as
| typeof APP_GUARD
| typeof APP_PIPE
| typeof APP_FILTER
| typeof APP_INTERCEPTOR
];
const factoryOrClassProvider = newProvider as
| FactoryProvider
| ClassProvider;
if (this.isRequestOrTransient(factoryOrClassProvider.scope!)) {
return this.container.addInjectable(newProvider, token, enhancerSubtype);
}
this.container.addProvider(newProvider, token, enhancerSubtype);
}
public insertInjectable(
injectable: Type<Injectable> | object,
token: string,
host: Type<Injectable>,
subtype: EnhancerSubtype,
methodKey?: string,
) {
if (isFunction(injectable)) {
const instanceWrapper = this.container.addInjectable(
injectable as Type,
token,
subtype,
host,
) as InstanceWrapper;
this.graphInspector.insertEnhancerMetadataCache({
moduleToken: token,
classRef: host,
enhancerInstanceWrapper: instanceWrapper,
targetNodeId: instanceWrapper.id,
subtype,
methodKey,
});
return instanceWrapper;
} else {
this.graphInspector.insertEnhancerMetadataCache({
moduleToken: token,
classRef: host,
enhancerRef: injectable,
methodKey,
subtype,
});
}
}
public insertExportedProvider(
// TODO: improve the type definition below because it doesn't reflects the real usage of this method
exportedProvider: Type<Injectable> | ForwardReference,
token: string,
) {
const fulfilledProvider = this.isForwardReference(exportedProvider)
? exportedProvider.forwardRef()
: exportedProvider;
this.container.addExportedProvider(fulfilledProvider, token);
}
public insertController(controller: Type<Controller>, token: string) {
this.container.addController(controller, token);
}
private insertOrOverrideModule(
moduleDefinition: ModuleDefinition,
overrides: ModuleOverride[],
scope: Type<unknown>[],
): Promise<
| {
moduleRef: Module;
inserted: boolean;
}
| undefined
> {
const overrideModule = this.getOverrideModuleByModule(
moduleDefinition,
overrides,
);
if (overrideModule !== undefined) {
return this.overrideModule(
moduleDefinition,
overrideModule.newModule,
scope,
);
}
return this.insertModule(moduleDefinition, scope);
}
private getOverrideModuleByModule(
module: ModuleDefinition,
overrides: ModuleOverride[],
): ModuleOverride | undefined {
if (this.isForwardReference(module)) {
return overrides.find(moduleToOverride => {
return (
moduleToOverride.moduleToReplace === module.forwardRef() ||
(
moduleToOverride.moduleToReplace as ForwardReference
).forwardRef?.() === module.forwardRef()
);
});
}
return overrides.find(
moduleToOverride => moduleToOverride.moduleToReplace === module,
);
}
private async overrideModule(
moduleToOverride: ModuleDefinition,
newModule: ModuleDefinition,
scope: Type<unknown>[],
): Promise<
| {
moduleRef: Module;
inserted: boolean;
}
| undefined
> {
return this.container.replaceModule(
this.isForwardReference(moduleToOverride)
? moduleToOverride.forwardRef()
: moduleToOverride,
this.isForwardReference(newModule) ? newModule.forwardRef() : newModule,
scope,
);
}
public reflectMetadata<T = any>(
metadataKey: string,
metatype: Type<any>,
): T[] {
return Reflect.getMetadata(metadataKey, metatype) || [];
}
public async registerCoreModule(overrides?: ModuleOverride[]) {
const moduleDefinition = InternalCoreModuleFactory.create(
this.container,
this,
this.container.getModuleCompiler(),
this.container.getHttpAdapterHostRef(),
this.graphInspector,
overrides,
);
const [instance] = await this.scanForModules({
moduleDefinition,
overrides,
});
this.container.registerCoreModuleRef(instance);
}
/**
* Add either request or transient globally scoped enhancers
* to all controllers metadata storage
*/
public addScopedEnhancersMetadata() {
iterate(this.applicationProvidersApplyMap)
.filter(wrapper => this.isRequestOrTransient(wrapper.scope!))
.forEach(({ moduleKey, providerKey }) => {
const modulesContainer = this.container.getModules();
const { injectables } = modulesContainer.get(moduleKey)!;
const instanceWrapper = injectables.get(providerKey);
const iterableIterator = modulesContainer.values();
iterate(iterableIterator)
.map(moduleRef =>
Array.from<InstanceWrapper>(moduleRef.controllers.values()).concat(
moduleRef.entryProviders,
),
)
.flatten()
.forEach(controllerOrEntryProvider =>
controllerOrEntryProvider.addEnhancerMetadata(instanceWrapper!),
);
});
}
public applyApplicationProviders() {
const applyProvidersMap = this.getApplyProvidersMap();
const applyRequestProvidersMap = this.getApplyRequestProvidersMap();
const getInstanceWrapper = (
moduleKey: string,
providerKey: string,
collectionKey: 'providers' | 'injectables',
) => {
const modules = this.container.getModules();
const collection = modules.get(moduleKey)![collectionKey];
return collection.get(providerKey);
};
// Add global enhancers to the application config
this.applicationProvidersApplyMap.forEach(
({ moduleKey, providerKey, type, scope }) => {
let instanceWrapper: InstanceWrapper;
if (this.isRequestOrTransient(scope!)) {
instanceWrapper = getInstanceWrapper(
moduleKey,
providerKey,
'injectables',
)!;
this.graphInspector.insertAttachedEnhancer(instanceWrapper);
return applyRequestProvidersMap[type as string](instanceWrapper);
}
instanceWrapper = getInstanceWrapper(
moduleKey,
providerKey,
'providers',
)!;
this.graphInspector.insertAttachedEnhancer(instanceWrapper);
applyProvidersMap[type as string](instanceWrapper.instance);
},
);
}
public getApplyProvidersMap(): { [type: string]: Function } {
return {
[APP_INTERCEPTOR]: (interceptor: NestInterceptor) =>
this.applicationConfig.addGlobalInterceptor(interceptor),
[APP_PIPE]: (pipe: PipeTransform) =>
this.applicationConfig.addGlobalPipe(pipe),
[APP_GUARD]: (guard: CanActivate) =>
this.applicationConfig.addGlobalGuard(guard),
[APP_FILTER]: (filter: ExceptionFilter) =>
this.applicationConfig.addGlobalFilter(filter),
};
}
public getApplyRequestProvidersMap(): { [type: string]: Function } {
return {
[APP_INTERCEPTOR]: (interceptor: InstanceWrapper<NestInterceptor>) =>
this.applicationConfig.addGlobalRequestInterceptor(interceptor),
[APP_PIPE]: (pipe: InstanceWrapper<PipeTransform>) =>
this.applicationConfig.addGlobalRequestPipe(pipe),
[APP_GUARD]: (guard: InstanceWrapper<CanActivate>) =>
this.applicationConfig.addGlobalRequestGuard(guard),
[APP_FILTER]: (filter: InstanceWrapper<ExceptionFilter>) =>
this.applicationConfig.addGlobalRequestFilter(filter),
};
}
public isDynamicModule(
module: Type<any> | DynamicModule,
): module is DynamicModule {
return module && !!(module as DynamicModule).module;
}
/**
* @param metatype
* @returns `true` if `metatype` is annotated with the `@Injectable()` decorator.
*/
private isInjectable(metatype: Type<any>): boolean {
return !!Reflect.getMetadata(INJECTABLE_WATERMARK, metatype);
}
/**
* @param metatype
* @returns `true` if `metatype` is annotated with the `@Controller()` decorator.
*/
private isController(metatype: Type<any>): boolean {
return !!Reflect.getMetadata(CONTROLLER_WATERMARK, metatype);
}
/**
* @param metatype
* @returns `true` if `metatype` is annotated with the `@Catch()` decorator.
*/
private isExceptionFilter(metatype: Type<any>): boolean {
return !!Reflect.getMetadata(CATCH_WATERMARK, metatype);
}
private isForwardReference(
module: ModuleDefinition,
): module is ForwardReference {
return module && !!(module as ForwardReference).forwardRef;
}
private isRequestOrTransient(scope: Scope): boolean {
return scope === Scope.REQUEST || scope === Scope.TRANSIENT;
}
}