mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 15:08:37 +00:00
483 lines
15 KiB
TypeScript
483 lines
15 KiB
TypeScript
import {
|
|
Abstract,
|
|
DynamicModule,
|
|
flatten,
|
|
ForwardReference,
|
|
Provider,
|
|
} from '@nestjs/common';
|
|
import {
|
|
EXCEPTION_FILTERS_METADATA,
|
|
GUARDS_METADATA,
|
|
INTERCEPTORS_METADATA,
|
|
MODULE_METADATA,
|
|
PIPES_METADATA,
|
|
ROUTE_ARGS_METADATA,
|
|
} from '@nestjs/common/constants';
|
|
import {
|
|
CanActivate,
|
|
ClassProvider,
|
|
ExceptionFilter,
|
|
ExistingProvider,
|
|
FactoryProvider,
|
|
NestInterceptor,
|
|
PipeTransform,
|
|
Scope,
|
|
ValueProvider,
|
|
} from '@nestjs/common/interfaces';
|
|
import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface';
|
|
import { Injectable } from '@nestjs/common/interfaces/injectable.interface';
|
|
import { Type } from '@nestjs/common/interfaces/type.interface';
|
|
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
|
|
import {
|
|
isFunction,
|
|
isNil,
|
|
isUndefined,
|
|
} from '@nestjs/common/utils/shared.utils';
|
|
import { ApplicationConfig } from './application-config';
|
|
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from './constants';
|
|
import { CircularDependencyException } from './errors/exceptions/circular-dependency.exception';
|
|
import { getClassScope } from './helpers/get-class-scope';
|
|
import { ModulesContainer } from './injector';
|
|
import { NestContainer } from './injector/container';
|
|
import { InstanceWrapper } from './injector/instance-wrapper';
|
|
import { Module } from './injector/module';
|
|
import { MetadataScanner } from './metadata-scanner';
|
|
|
|
interface ApplicationProviderWrapper {
|
|
moduleKey: string;
|
|
providerKey: string;
|
|
type: string | symbol | Type<any> | Abstract<any> | Function;
|
|
scope?: Scope;
|
|
}
|
|
|
|
export class DependenciesScanner {
|
|
private readonly applicationProvidersApplyMap: ApplicationProviderWrapper[] = [];
|
|
|
|
constructor(
|
|
private readonly container: NestContainer,
|
|
private readonly metadataScanner: MetadataScanner,
|
|
private readonly applicationConfig = new ApplicationConfig(),
|
|
) {}
|
|
|
|
public async scan(module: Type<any>) {
|
|
await this.registerCoreModule();
|
|
await this.scanForModules(module);
|
|
await this.scanModulesForDependencies();
|
|
|
|
this.addScopedEnhancersMetadata();
|
|
this.container.bindGlobalScope();
|
|
}
|
|
|
|
public async scanForModules(
|
|
module: ForwardReference | Type<unknown> | DynamicModule,
|
|
scope: Type<unknown>[] = [],
|
|
ctxRegistry: (ForwardReference | DynamicModule | Type<unknown>)[] = [],
|
|
): Promise<Module> {
|
|
const moduleInstance = await this.insertModule(module, scope);
|
|
ctxRegistry.push(module);
|
|
|
|
if (this.isForwardReference(module)) {
|
|
module = (module as ForwardReference).forwardRef();
|
|
}
|
|
const modules = !this.isDynamicModule(module as Type<any> | DynamicModule)
|
|
? this.reflectMetadata(module as Type<any>, MODULE_METADATA.IMPORTS)
|
|
: [
|
|
...this.reflectMetadata(
|
|
(module as DynamicModule).module,
|
|
MODULE_METADATA.IMPORTS,
|
|
),
|
|
...((module as DynamicModule).imports || []),
|
|
];
|
|
|
|
for (const innerModule of modules) {
|
|
if (ctxRegistry.includes(innerModule)) {
|
|
continue;
|
|
}
|
|
await this.scanForModules(
|
|
innerModule,
|
|
[].concat(scope, module),
|
|
ctxRegistry,
|
|
);
|
|
}
|
|
return moduleInstance;
|
|
}
|
|
|
|
public async insertModule(
|
|
module: any,
|
|
scope: Type<unknown>[],
|
|
): Promise<Module> {
|
|
if (module && module.forwardRef) {
|
|
return this.container.addModule(module.forwardRef(), scope);
|
|
}
|
|
return this.container.addModule(module, scope);
|
|
}
|
|
|
|
public async scanModulesForDependencies() {
|
|
const modules = 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);
|
|
}
|
|
this.calculateModulesDistance(modules);
|
|
}
|
|
|
|
public async reflectImports(
|
|
module: Type<unknown>,
|
|
token: string,
|
|
context: string,
|
|
) {
|
|
const modules = [
|
|
...this.reflectMetadata(module, MODULE_METADATA.IMPORTS),
|
|
...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, MODULE_METADATA.PROVIDERS),
|
|
...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, MODULE_METADATA.CONTROLLERS),
|
|
...this.container.getDynamicMetadataByToken(
|
|
token,
|
|
MODULE_METADATA.CONTROLLERS as 'controllers',
|
|
),
|
|
];
|
|
controllers.forEach(item => {
|
|
this.insertController(item, token);
|
|
this.reflectDynamicMetadata(item, token);
|
|
});
|
|
}
|
|
|
|
public reflectDynamicMetadata(obj: Type<Injectable>, token: string) {
|
|
if (!obj || !obj.prototype) {
|
|
return;
|
|
}
|
|
this.reflectInjectables(obj, token, GUARDS_METADATA);
|
|
this.reflectInjectables(obj, token, INTERCEPTORS_METADATA);
|
|
this.reflectInjectables(obj, token, EXCEPTION_FILTERS_METADATA);
|
|
this.reflectInjectables(obj, token, PIPES_METADATA);
|
|
this.reflectParamInjectables(obj, token, ROUTE_ARGS_METADATA);
|
|
}
|
|
|
|
public reflectExports(module: Type<unknown>, token: string) {
|
|
const exports = [
|
|
...this.reflectMetadata(module, MODULE_METADATA.EXPORTS),
|
|
...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(component, metadataKey);
|
|
const methodsInjectables = this.metadataScanner.scanFromPrototype(
|
|
null,
|
|
component.prototype,
|
|
this.reflectKeyMetadata.bind(this, component, metadataKey),
|
|
);
|
|
|
|
const flattenMethodsInjectables = this.flatten(methodsInjectables);
|
|
const combinedInjectables = [
|
|
...controllerInjectables,
|
|
...flattenMethodsInjectables,
|
|
].filter(isFunction);
|
|
const injectables = Array.from(new Set(combinedInjectables));
|
|
|
|
injectables.forEach(injectable =>
|
|
this.insertInjectable(injectable, token, component),
|
|
);
|
|
}
|
|
|
|
public reflectParamInjectables(
|
|
component: Type<Injectable>,
|
|
token: string,
|
|
metadataKey: string,
|
|
) {
|
|
const paramsMetadata = this.metadataScanner.scanFromPrototype(
|
|
null,
|
|
component.prototype,
|
|
method => Reflect.getMetadata(metadataKey, component, method),
|
|
);
|
|
const paramsInjectables = this.flatten(
|
|
paramsMetadata,
|
|
).map((param: Record<string, any>) =>
|
|
flatten(Object.keys(param).map(k => param[k].pipes)).filter(isFunction),
|
|
);
|
|
flatten(paramsInjectables).forEach((injectable: Type<Injectable>) =>
|
|
this.insertInjectable(injectable, token, component),
|
|
);
|
|
}
|
|
|
|
public reflectKeyMetadata(
|
|
component: Type<Injectable>,
|
|
key: string,
|
|
method: string,
|
|
) {
|
|
let prototype = component.prototype;
|
|
do {
|
|
const descriptor = Reflect.getOwnPropertyDescriptor(prototype, method);
|
|
if (!descriptor) {
|
|
continue;
|
|
}
|
|
return Reflect.getMetadata(key, descriptor.value);
|
|
} while (
|
|
(prototype = Reflect.getPrototypeOf(prototype)) &&
|
|
prototype !== Object.prototype &&
|
|
prototype
|
|
);
|
|
return undefined;
|
|
}
|
|
|
|
public async calculateModulesDistance(modules: ModulesContainer) {
|
|
const modulesGenerator = modules.values();
|
|
const rootModule = modulesGenerator.next().value;
|
|
const modulesStack = [rootModule];
|
|
|
|
const calculateDistance = (moduleRef: Module, distance = 1) => {
|
|
if (modulesStack.includes(moduleRef)) {
|
|
return;
|
|
}
|
|
modulesStack.push(moduleRef);
|
|
|
|
const moduleImports = rootModule.relatedModules;
|
|
moduleImports.forEach(module => {
|
|
module.distance = distance;
|
|
calculateDistance(module, distance + 1);
|
|
});
|
|
};
|
|
calculateDistance(rootModule);
|
|
}
|
|
|
|
public async insertImport(related: any, token: string, context: string) {
|
|
if (isUndefined(related)) {
|
|
throw new CircularDependencyException(context);
|
|
}
|
|
if (related && related.forwardRef) {
|
|
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 as Type<any>, token);
|
|
}
|
|
const applyProvidersMap = this.getApplyProvidersMap();
|
|
const providersKeys = Object.keys(applyProvidersMap);
|
|
const type = (provider as
|
|
| ClassProvider
|
|
| ValueProvider
|
|
| FactoryProvider
|
|
| ExistingProvider).provide;
|
|
|
|
if (!providersKeys.includes(type as string)) {
|
|
return this.container.addProvider(provider as any, token);
|
|
}
|
|
const providerToken = `${
|
|
type as string
|
|
} (UUID: ${randomStringGenerator()})`;
|
|
|
|
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;
|
|
|
|
if (
|
|
this.isRequestOrTransient(
|
|
(newProvider as FactoryProvider | ClassProvider).scope,
|
|
)
|
|
) {
|
|
return this.container.addInjectable(newProvider, token);
|
|
}
|
|
this.container.addProvider(newProvider, token);
|
|
}
|
|
|
|
public insertInjectable(
|
|
injectable: Type<Injectable>,
|
|
token: string,
|
|
host: Type<Injectable>,
|
|
) {
|
|
this.container.addInjectable(injectable, token, host);
|
|
}
|
|
|
|
public insertExportedProvider(
|
|
exportedProvider: Type<Injectable>,
|
|
token: string,
|
|
) {
|
|
this.container.addExportedProvider(exportedProvider, token);
|
|
}
|
|
|
|
public insertController(controller: Type<Controller>, token: string) {
|
|
this.container.addController(controller, token);
|
|
}
|
|
|
|
public reflectMetadata(metatype: Type<any>, metadataKey: string) {
|
|
return Reflect.getMetadata(metadataKey, metatype) || [];
|
|
}
|
|
|
|
public async registerCoreModule() {
|
|
const module = this.container.createCoreModule();
|
|
const instance = await this.scanForModules(module);
|
|
this.container.registerCoreModuleRef(instance);
|
|
}
|
|
|
|
/**
|
|
* Add either request or transient globally scoped enhancers
|
|
* to all controllers metadata storage
|
|
*/
|
|
public addScopedEnhancersMetadata() {
|
|
const scopedGlobalProviders = this.applicationProvidersApplyMap.filter(
|
|
wrapper => this.isRequestOrTransient(wrapper.scope),
|
|
);
|
|
|
|
scopedGlobalProviders.forEach(({ moduleKey, providerKey }) => {
|
|
const modulesContainer = this.container.getModules();
|
|
const { injectables } = modulesContainer.get(moduleKey);
|
|
const instanceWrapper = injectables.get(providerKey);
|
|
|
|
const modules = [...modulesContainer.values()];
|
|
const controllersArray = modules.map(module => [
|
|
...module.controllers.values(),
|
|
]);
|
|
const controllers = this.flatten(controllersArray);
|
|
controllers.forEach(controller =>
|
|
controller.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',
|
|
);
|
|
return applyRequestProvidersMap[type as string](instanceWrapper);
|
|
}
|
|
instanceWrapper = getInstanceWrapper(
|
|
moduleKey,
|
|
providerKey,
|
|
'providers',
|
|
);
|
|
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;
|
|
}
|
|
|
|
public isForwardReference(
|
|
module: Type<any> | DynamicModule | ForwardReference,
|
|
): module is ForwardReference {
|
|
return module && !!(module as ForwardReference).forwardRef;
|
|
}
|
|
|
|
private flatten<T = any>(arr: T[][]): T[] {
|
|
return arr.reduce((a: T[], b: T[]) => a.concat(b), []);
|
|
}
|
|
|
|
private isRequestOrTransient(scope: Scope): boolean {
|
|
return scope === Scope.REQUEST || scope === Scope.TRANSIENT;
|
|
}
|
|
}
|