mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
613 lines
17 KiB
TypeScript
613 lines
17 KiB
TypeScript
import { Scope } from '@nestjs/common';
|
|
import {
|
|
OPTIONAL_DEPS_METADATA,
|
|
OPTIONAL_PROPERTY_DEPS_METADATA,
|
|
PARAMTYPES_METADATA,
|
|
PROPERTY_DEPS_METADATA,
|
|
SELF_DECLARED_DEPS_METADATA,
|
|
} from '@nestjs/common/constants';
|
|
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 {
|
|
isFunction,
|
|
isNil,
|
|
isObject,
|
|
isUndefined,
|
|
} from '@nestjs/common/utils/shared.utils';
|
|
import { RuntimeException } from '../errors/exceptions/runtime.exception';
|
|
import { UndefinedDependencyException } from '../errors/exceptions/undefined-dependency.exception';
|
|
import { UnknownDependenciesException } from '../errors/exceptions/unknown-dependencies.exception';
|
|
import { AsyncContext } from './../hooks';
|
|
import { STATIC_CONTEXT } from './constants';
|
|
import {
|
|
ContextId,
|
|
InstancePerContext,
|
|
InstanceWrapper,
|
|
} from './instance-wrapper';
|
|
import { Module } from './module';
|
|
import { ModulesContainer } from './modules-container';
|
|
|
|
/**
|
|
* The type of an injectable dependency
|
|
*/
|
|
export type InjectorDependency = Type<any> | Function | string | symbol;
|
|
|
|
/**
|
|
* The property-based dependency
|
|
*/
|
|
export interface PropertyDependency {
|
|
key: string;
|
|
name: InjectorDependency;
|
|
isOptional?: boolean;
|
|
instance?: any;
|
|
}
|
|
|
|
/**
|
|
* Context of a dependency which gets injected by
|
|
* the injector
|
|
*/
|
|
export interface InjectorDependencyContext {
|
|
/**
|
|
* The name of the property key (property-based injection)
|
|
*/
|
|
key?: string;
|
|
/**
|
|
* The name of the function or injection token
|
|
*/
|
|
name?: string;
|
|
/**
|
|
* The index of the dependency which gets injected
|
|
* from the dependencies array
|
|
*/
|
|
index?: number;
|
|
/**
|
|
* The dependency array which gets injected
|
|
*/
|
|
dependencies?: InjectorDependency[];
|
|
}
|
|
|
|
export class Injector {
|
|
public async loadMiddleware(
|
|
wrapper: InstanceWrapper,
|
|
collection: Map<string, InstanceWrapper>,
|
|
module: Module,
|
|
contextId = STATIC_CONTEXT,
|
|
) {
|
|
const { metatype } = wrapper;
|
|
const targetMetatype = collection.get(metatype.name);
|
|
if (targetMetatype.instance !== null) {
|
|
return;
|
|
}
|
|
const loadInstance = (instances: any[]) => {
|
|
const instanceWrapper = new InstanceWrapper({
|
|
instance: new metatype(...instances),
|
|
metatype,
|
|
host: module,
|
|
});
|
|
collection.set(metatype.name, instanceWrapper);
|
|
};
|
|
await this.resolveConstructorParams(
|
|
wrapper,
|
|
module,
|
|
null,
|
|
loadInstance,
|
|
contextId,
|
|
);
|
|
}
|
|
|
|
public async loadController(
|
|
wrapper: InstanceWrapper<Controller>,
|
|
module: Module,
|
|
contextId = STATIC_CONTEXT,
|
|
) {
|
|
const controllers = module.controllers;
|
|
await this.loadInstance<Controller>(
|
|
wrapper,
|
|
controllers,
|
|
module,
|
|
contextId,
|
|
);
|
|
}
|
|
|
|
public async loadInjectable(
|
|
wrapper: InstanceWrapper<Controller>,
|
|
module: Module,
|
|
contextId = STATIC_CONTEXT,
|
|
) {
|
|
const injectables = module.injectables;
|
|
await this.loadInstance<Controller>(
|
|
wrapper,
|
|
injectables,
|
|
module,
|
|
contextId,
|
|
);
|
|
}
|
|
|
|
public async loadProvider(
|
|
wrapper: InstanceWrapper<Injectable>,
|
|
module: Module,
|
|
contextId = STATIC_CONTEXT,
|
|
) {
|
|
const providers = module.providers;
|
|
await this.loadInstance<Injectable>(wrapper, providers, module, contextId);
|
|
}
|
|
|
|
public loadPrototype<T>(
|
|
{ metatype, name }: InstanceWrapper<T>,
|
|
collection: Map<string, InstanceWrapper<T>>,
|
|
contextId = STATIC_CONTEXT,
|
|
) {
|
|
if (!collection) {
|
|
return null;
|
|
}
|
|
const target = collection.get(name);
|
|
const instanceHost = target.getInstanceByContextId(contextId);
|
|
if (
|
|
instanceHost.isResolved ||
|
|
!isNil(target.inject) ||
|
|
!metatype.prototype
|
|
) {
|
|
return null;
|
|
}
|
|
collection.set(
|
|
name,
|
|
new InstanceWrapper({
|
|
...target,
|
|
instance: Object.create(metatype.prototype),
|
|
}),
|
|
);
|
|
}
|
|
|
|
public applyDoneHook<T>(wrapper: InstancePerContext<T>): () => void {
|
|
let done: () => void;
|
|
wrapper.donePromise = new Promise<void>((resolve, reject) => {
|
|
done = resolve;
|
|
});
|
|
wrapper.isPending = true;
|
|
return done;
|
|
}
|
|
|
|
public async loadInstance<T>(
|
|
wrapper: InstanceWrapper<T>,
|
|
collection: Map<string, InstanceWrapper>,
|
|
module: Module,
|
|
contextId = STATIC_CONTEXT,
|
|
) {
|
|
const instanceHost = wrapper.getInstanceByContextId(contextId);
|
|
if (instanceHost.isPending) {
|
|
return instanceHost.donePromise;
|
|
}
|
|
const done = this.applyDoneHook(instanceHost);
|
|
const { name, inject } = wrapper;
|
|
|
|
const targetWrapper = collection.get(name);
|
|
if (isUndefined(targetWrapper)) {
|
|
throw new RuntimeException();
|
|
}
|
|
if (instanceHost.isResolved) {
|
|
return;
|
|
}
|
|
const callback = async (instances: any[]) => {
|
|
const properties = await this.resolveProperties(
|
|
wrapper,
|
|
module,
|
|
inject,
|
|
contextId,
|
|
);
|
|
const instance = await this.instantiateClass(
|
|
instances,
|
|
wrapper,
|
|
targetWrapper,
|
|
module.id,
|
|
contextId,
|
|
);
|
|
this.applyProperties(instance, properties);
|
|
done();
|
|
};
|
|
await this.resolveConstructorParams<T>(
|
|
wrapper,
|
|
module,
|
|
inject,
|
|
callback,
|
|
contextId,
|
|
);
|
|
}
|
|
|
|
public async resolveConstructorParams<T>(
|
|
wrapper: InstanceWrapper<T>,
|
|
module: Module,
|
|
inject: InjectorDependency[],
|
|
callback: (args: any[]) => void,
|
|
contextId = STATIC_CONTEXT,
|
|
) {
|
|
const metadata = wrapper.getCtorMetadata();
|
|
if (metadata) {
|
|
const dependenciesHosts = await Promise.all(
|
|
metadata.map(async item =>
|
|
this.resolveComponentHost(item.host, item, contextId),
|
|
),
|
|
);
|
|
const deps = dependenciesHosts.map(
|
|
item => item.getInstanceByContextId(contextId).instance,
|
|
);
|
|
return callback(deps);
|
|
}
|
|
|
|
const dependencies = isNil(inject)
|
|
? this.reflectConstructorParams(wrapper.metatype)
|
|
: inject;
|
|
const optionalDependenciesIds = isNil(inject)
|
|
? this.reflectOptionalParams(wrapper.metatype)
|
|
: [];
|
|
|
|
let isResolved = true;
|
|
|
|
const findOneParam = async (param, index) => {
|
|
try {
|
|
const paramWrapper = await this.resolveSingleParam<T>(
|
|
wrapper,
|
|
param,
|
|
{ index, dependencies },
|
|
module,
|
|
contextId,
|
|
);
|
|
const instanceHost = paramWrapper.getInstanceByContextId(contextId);
|
|
if (!instanceHost.isResolved && !paramWrapper.forwardRef) {
|
|
isResolved = false;
|
|
}
|
|
return instanceHost && instanceHost.instance;
|
|
} catch (err) {
|
|
const isOptional = optionalDependenciesIds.includes(index);
|
|
if (!isOptional) {
|
|
throw err;
|
|
}
|
|
return undefined;
|
|
}
|
|
};
|
|
const instances = await Promise.all(dependencies.map(findOneParam));
|
|
isResolved && (await callback(instances));
|
|
}
|
|
|
|
public reflectConstructorParams<T>(type: Type<T>): any[] {
|
|
const paramtypes = Reflect.getMetadata(PARAMTYPES_METADATA, type) || [];
|
|
const selfParams = this.reflectSelfParams<T>(type);
|
|
|
|
selfParams.forEach(({ index, param }) => (paramtypes[index] = param));
|
|
return paramtypes;
|
|
}
|
|
|
|
public reflectOptionalParams<T>(type: Type<T>): any[] {
|
|
return Reflect.getMetadata(OPTIONAL_DEPS_METADATA, type) || [];
|
|
}
|
|
|
|
public reflectSelfParams<T>(type: Type<T>): any[] {
|
|
return Reflect.getMetadata(SELF_DECLARED_DEPS_METADATA, type) || [];
|
|
}
|
|
|
|
public async resolveSingleParam<T>(
|
|
wrapper: InstanceWrapper<T>,
|
|
param: Type<any> | string | symbol | any,
|
|
dependencyContext: InjectorDependencyContext,
|
|
module: Module,
|
|
contextId = STATIC_CONTEXT,
|
|
) {
|
|
if (isUndefined(param)) {
|
|
throw new UndefinedDependencyException(
|
|
wrapper.name,
|
|
dependencyContext,
|
|
module,
|
|
);
|
|
}
|
|
const token = this.resolveParamToken(wrapper, param);
|
|
return this.resolveComponentInstance<T>(
|
|
module,
|
|
isFunction(token) ? (token as Type<any>).name : token,
|
|
dependencyContext,
|
|
wrapper,
|
|
contextId,
|
|
);
|
|
}
|
|
|
|
public resolveParamToken<T>(
|
|
wrapper: InstanceWrapper<T>,
|
|
param: Type<any> | string | symbol | any,
|
|
) {
|
|
if (!param.forwardRef) {
|
|
return param;
|
|
}
|
|
wrapper.forwardRef = true;
|
|
return param.forwardRef();
|
|
}
|
|
|
|
public async resolveComponentInstance<T>(
|
|
module: Module,
|
|
name: any,
|
|
dependencyContext: InjectorDependencyContext,
|
|
wrapper: InstanceWrapper<T>,
|
|
contextId = STATIC_CONTEXT,
|
|
): Promise<InstanceWrapper> {
|
|
const providers = module.providers;
|
|
const instanceWrapper = await this.lookupComponent(
|
|
providers,
|
|
module,
|
|
{ ...dependencyContext, name },
|
|
wrapper,
|
|
contextId,
|
|
);
|
|
return this.resolveComponentHost(module, instanceWrapper, contextId);
|
|
}
|
|
|
|
public async resolveComponentHost<T>(
|
|
module: Module,
|
|
instanceWrapper: InstanceWrapper<T>,
|
|
contextId = STATIC_CONTEXT,
|
|
): Promise<InstanceWrapper> {
|
|
const instanceHost = instanceWrapper.getInstanceByContextId(contextId);
|
|
if (!instanceHost.isResolved && !instanceWrapper.forwardRef) {
|
|
await this.loadProvider(instanceWrapper, module, contextId);
|
|
}
|
|
if (instanceWrapper.async) {
|
|
instanceWrapper.setInstanceByContextId(
|
|
contextId,
|
|
await instanceWrapper.getInstanceByContextId(contextId),
|
|
);
|
|
}
|
|
return instanceWrapper;
|
|
}
|
|
|
|
public async lookupComponent<T = any>(
|
|
providers: Map<string, InstanceWrapper>,
|
|
module: Module,
|
|
dependencyContext: InjectorDependencyContext,
|
|
wrapper: InstanceWrapper<T>,
|
|
contextId = STATIC_CONTEXT,
|
|
): Promise<InstanceWrapper<T>> {
|
|
const { name } = dependencyContext;
|
|
const scanInExports = () =>
|
|
this.lookupComponentInExports(
|
|
dependencyContext,
|
|
module,
|
|
wrapper,
|
|
contextId,
|
|
);
|
|
|
|
return providers.has(name) ? providers.get(name) : scanInExports();
|
|
}
|
|
|
|
public async lookupComponentInExports<T = any>(
|
|
dependencyContext: InjectorDependencyContext,
|
|
module: Module,
|
|
wrapper: InstanceWrapper<T>,
|
|
contextId = STATIC_CONTEXT,
|
|
) {
|
|
const instanceWrapper = await this.lookupComponentInImports(
|
|
module,
|
|
dependencyContext.name,
|
|
[],
|
|
wrapper,
|
|
contextId,
|
|
);
|
|
if (isNil(instanceWrapper)) {
|
|
throw new UnknownDependenciesException(
|
|
wrapper.name,
|
|
dependencyContext,
|
|
module,
|
|
);
|
|
}
|
|
return instanceWrapper;
|
|
}
|
|
|
|
public async lookupComponentInImports(
|
|
module: Module,
|
|
name: any,
|
|
moduleRegistry: any[] = [],
|
|
wrapper: InstanceWrapper,
|
|
contextId = STATIC_CONTEXT,
|
|
): Promise<any> {
|
|
let instanceWrapperRef: InstanceWrapper = null;
|
|
|
|
const imports: Set<Module> = module.imports || new Set();
|
|
const children = [...imports.values()].filter(item => item);
|
|
|
|
for (const relatedModule of children) {
|
|
if (moduleRegistry.includes(relatedModule.id)) {
|
|
continue;
|
|
}
|
|
moduleRegistry.push(relatedModule.id);
|
|
const { providers, exports } = relatedModule;
|
|
if (!exports.has(name) || !providers.has(name)) {
|
|
const instanceRef = await this.lookupComponentInImports(
|
|
relatedModule,
|
|
name,
|
|
moduleRegistry,
|
|
wrapper,
|
|
contextId,
|
|
);
|
|
if (instanceRef) {
|
|
return instanceRef;
|
|
}
|
|
continue;
|
|
}
|
|
instanceWrapperRef = providers.get(name);
|
|
|
|
const instanceHost = instanceWrapperRef.getInstanceByContextId(contextId);
|
|
if (!instanceHost.isResolved && !instanceWrapperRef.forwardRef) {
|
|
await this.loadProvider(instanceWrapperRef, relatedModule, contextId);
|
|
break;
|
|
}
|
|
}
|
|
return instanceWrapperRef;
|
|
}
|
|
|
|
public async resolveProperties<T>(
|
|
wrapper: InstanceWrapper<T>,
|
|
module: Module,
|
|
inject?: InjectorDependency[],
|
|
contextId = STATIC_CONTEXT,
|
|
): Promise<PropertyDependency[]> {
|
|
if (!isNil(inject)) {
|
|
return [];
|
|
}
|
|
const metadata = wrapper.getPropertiesMetadata();
|
|
if (metadata) {
|
|
const dependenciesHosts = await Promise.all(
|
|
metadata.map(async ({ wrapper: item, key }) => ({
|
|
key,
|
|
host: await this.resolveComponentHost(item.host, item, contextId),
|
|
})),
|
|
);
|
|
return dependenciesHosts.map(({ key, host }) => ({
|
|
key,
|
|
name: key,
|
|
instance: host.getInstanceByContextId(contextId).instance,
|
|
}));
|
|
}
|
|
const properties = this.reflectProperties(wrapper.metatype);
|
|
const instances = await Promise.all(
|
|
properties.map(async (item: PropertyDependency) => {
|
|
try {
|
|
const dependencyContext = {
|
|
key: item.key,
|
|
name: item.name as string,
|
|
};
|
|
const paramWrapper = await this.resolveSingleParam<T>(
|
|
wrapper,
|
|
item.name,
|
|
dependencyContext,
|
|
module,
|
|
contextId,
|
|
);
|
|
if (!paramWrapper) {
|
|
return undefined;
|
|
}
|
|
const instanceHost = paramWrapper.getInstanceByContextId(contextId);
|
|
return instanceHost.instance;
|
|
} catch (err) {
|
|
if (!item.isOptional) {
|
|
throw err;
|
|
}
|
|
return undefined;
|
|
}
|
|
}),
|
|
);
|
|
return properties.map((item: PropertyDependency, index: number) => ({
|
|
...item,
|
|
instance: instances[index],
|
|
}));
|
|
}
|
|
|
|
public reflectProperties<T>(type: Type<T>): PropertyDependency[] {
|
|
const properties = Reflect.getMetadata(PROPERTY_DEPS_METADATA, type) || [];
|
|
const optionalKeys: string[] =
|
|
Reflect.getMetadata(OPTIONAL_PROPERTY_DEPS_METADATA, type) || [];
|
|
|
|
return properties.map((item: any) => ({
|
|
...item,
|
|
name: item.type,
|
|
isOptional: optionalKeys.includes(item.key),
|
|
}));
|
|
}
|
|
|
|
public applyProperties<T = any>(
|
|
instance: T,
|
|
properties: PropertyDependency[],
|
|
): void {
|
|
if (!isObject(instance)) {
|
|
return undefined;
|
|
}
|
|
properties
|
|
.filter(item => !isNil(item.instance))
|
|
.forEach(item => (instance[item.key] = item.instance));
|
|
}
|
|
|
|
public async instantiateClass<T = any>(
|
|
instances: any[],
|
|
wrapper: InstanceWrapper,
|
|
targetMetatype: InstanceWrapper,
|
|
moduleId: string,
|
|
contextId = STATIC_CONTEXT,
|
|
): Promise<T> {
|
|
const { metatype, inject, name, scope } = wrapper;
|
|
const instanceHost = targetMetatype.getInstanceByContextId(contextId);
|
|
const instanceKey = moduleId + name;
|
|
|
|
if (isNil(inject)) {
|
|
const targetInstance = wrapper.getInstanceByContextId(contextId);
|
|
targetInstance.instance = wrapper.forwardRef
|
|
? Object.assign(targetInstance.instance, new metatype(...instances))
|
|
: new metatype(...instances);
|
|
|
|
if (scope === Scope.LAZY) {
|
|
this.mergeAsyncProxy(
|
|
instances,
|
|
targetInstance,
|
|
instanceKey,
|
|
metatype,
|
|
(...args: any[]) => new metatype(...args),
|
|
);
|
|
}
|
|
} else {
|
|
const factoryReturnValue = ((targetMetatype.metatype as any) as Function)(
|
|
...instances,
|
|
);
|
|
instanceHost.instance = await factoryReturnValue;
|
|
if (scope === Scope.LAZY) {
|
|
this.mergeAsyncProxy(
|
|
instances,
|
|
instanceHost,
|
|
instanceKey,
|
|
metatype,
|
|
(...args: any[]) =>
|
|
((targetMetatype.metatype as any) as Function)(...args),
|
|
);
|
|
}
|
|
}
|
|
instanceHost.isResolved = true;
|
|
return instanceHost.instance;
|
|
}
|
|
|
|
mergeAsyncProxy(
|
|
instances: any[],
|
|
targetInstance: InstancePerContext<any>,
|
|
instanceKey: string,
|
|
metatype: Type<any>,
|
|
factory: (...intances: any[]) => any,
|
|
) {
|
|
AsyncContext.instance.set(instanceKey, targetInstance.instance);
|
|
|
|
const proxy = (target: unknown, property: string | number | symbol) => {
|
|
if (!(property in (target as object))) {
|
|
return;
|
|
}
|
|
const cachedInstance = AsyncContext.instance.get(instanceKey);
|
|
if (cachedInstance) {
|
|
return cachedInstance[property];
|
|
}
|
|
const value = factory(...instances);
|
|
AsyncContext.instance.set(instanceKey, value);
|
|
return value[property];
|
|
};
|
|
targetInstance.instance = new Proxy(targetInstance.instance, {
|
|
get: proxy,
|
|
set: proxy,
|
|
});
|
|
}
|
|
|
|
async loadControllerPerContext<T>(
|
|
instance: Controller,
|
|
modulesContainer: ModulesContainer,
|
|
moduleKey: string,
|
|
ctx: ContextId,
|
|
): Promise<T> {
|
|
const module = modulesContainer.get(moduleKey);
|
|
|
|
const { controllers } = module;
|
|
const controller = controllers.get(instance.constructor.name);
|
|
await this.loadInstance(controller, controllers, module, ctx);
|
|
|
|
const wrapper = controller.getInstanceByContextId(ctx);
|
|
return wrapper && (wrapper.instance as T);
|
|
}
|
|
}
|