From f4d1a1d978414fba037a354f6718f9c3a3636385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Sat, 5 Nov 2022 16:09:47 +0100 Subject: [PATCH 1/7] feat(core): expose each option to get and resolve methods --- .../nest-application-context.interface.ts | 67 ++++++++- .../injector/abstract-instance-resolver.ts | 85 +++++++++++ packages/core/injector/container.ts | 2 +- .../injector/{ => helpers}/silent-logger.ts | 0 packages/core/injector/index.ts | 2 +- packages/core/injector/instance-links-host.ts | 27 +++- packages/core/injector/instance-loader.ts | 2 +- .../internal-core-module-factory.ts | 16 +- .../internal-core-module.ts | 6 +- .../lazy-module-loader-options.interface.ts | 0 .../lazy-module-loader.ts | 14 +- packages/core/injector/module-ref.ts | 140 ++++++++++-------- packages/core/injector/module.ts | 25 +++- packages/core/nest-application-context.ts | 85 +++-------- packages/core/repl/repl-context.ts | 2 +- packages/core/scanner.ts | 2 +- .../internal-core-module-factory.spec.ts | 12 +- .../lazy-module-loader.spec.ts | 8 +- 18 files changed, 320 insertions(+), 175 deletions(-) create mode 100644 packages/core/injector/abstract-instance-resolver.ts rename packages/core/injector/{ => helpers}/silent-logger.ts (100%) rename packages/core/injector/{ => internal-core-module}/internal-core-module-factory.ts (71%) rename packages/core/injector/{ => internal-core-module}/internal-core-module.ts (80%) rename packages/core/injector/{ => lazy-module-loader}/lazy-module-loader-options.interface.ts (100%) rename packages/core/injector/{ => lazy-module-loader}/lazy-module-loader.ts (86%) rename packages/core/test/injector/{ => internal-core-module}/internal-core-module-factory.spec.ts (66%) rename packages/core/test/injector/{ => lazy-module-loader}/lazy-module-loader.spec.ts (89%) diff --git a/packages/common/interfaces/nest-application-context.interface.ts b/packages/common/interfaces/nest-application-context.interface.ts index e10dfc3063b..5d48b4aec13 100644 --- a/packages/common/interfaces/nest-application-context.interface.ts +++ b/packages/common/interfaces/nest-application-context.interface.ts @@ -3,6 +3,11 @@ import { LoggerService, LogLevel } from '../services/logger.service'; import { DynamicModule } from './modules'; import { Type } from './type.interface'; +export interface GetOrResolveOptions { + strict?: boolean; + each?: boolean; +} + /** * Interface defining NestApplicationContext. * @@ -21,18 +26,74 @@ export interface INestApplicationContext { */ get( typeOrToken: Type | Function | string | symbol, - options?: { strict: boolean }, ): TResult; + /** + * Retrieves an instance of either injectable or controller, otherwise, throws exception. + * @returns {TResult} + */ + get( + typeOrToken: Type | Function | string | symbol, + options: { strict?: boolean; each?: undefined | false }, + ): TResult; + /** + * Retrieves a list of instances of either injectables or controllers, otherwise, throws exception. + * @returns {Array} + */ + get( + typeOrToken: Type | Function | string | symbol, + options: { strict?: boolean; each: true }, + ): Array; + /** + * Retrieves an instance (or a list of instances) of either injectable or controller, otherwise, throws exception. + * @returns {TResult | Array} + */ + get( + typeOrToken: Type | Function | string | symbol, + options?: GetOrResolveOptions, + ): TResult | Array; /** * Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception. - * @returns {Promise} + * @returns {Array} + */ + resolve( + typeOrToken: Type | Function | string | symbol, + ): Promise; + /** + * Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception. + * @returns {Array} + */ + resolve( + typeOrToken: Type | Function | string | symbol, + contextId?: { id: number }, + ): Promise; + /** + * Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception. + * @returns {Array} */ resolve( typeOrToken: Type | Function | string | symbol, contextId?: { id: number }, - options?: { strict: boolean }, + options?: { strict?: boolean; each?: undefined | false }, ): Promise; + /** + * Resolves transient or request-scoped instances of either injectables or controllers, otherwise, throws exception. + * @returns {Array} + */ + resolve( + typeOrToken: Type | Function | string | symbol, + contextId?: { id: number }, + options?: { strict?: boolean; each: true }, + ): Promise>; + /** + * Resolves transient or request-scoped instance (or a list of instances) of either injectable or controller, otherwise, throws exception. + * @returns {Promise>} + */ + resolve( + typeOrToken: Type | Function | string | symbol, + contextId?: { id: number }, + options?: GetOrResolveOptions, + ): Promise>; /** * Registers the request/context object for a given context ID (DI container sub-tree). diff --git a/packages/core/injector/abstract-instance-resolver.ts b/packages/core/injector/abstract-instance-resolver.ts new file mode 100644 index 00000000000..1775c24f19a --- /dev/null +++ b/packages/core/injector/abstract-instance-resolver.ts @@ -0,0 +1,85 @@ +import { Abstract, Scope, Type } from '@nestjs/common'; +import { GetOrResolveOptions } from '@nestjs/common/interfaces'; +import { + InvalidClassScopeException, + UnknownElementException, +} from '../errors/exceptions'; +import { Injector } from './injector'; +import { InstanceLink, InstanceLinksHost } from './instance-links-host'; +import { ContextId } from './instance-wrapper'; +import { Module } from './module'; + +export abstract class AbstractInstanceResolver { + protected abstract instanceLinksHost: InstanceLinksHost; + protected abstract injector: Injector; + + protected abstract get( + typeOrToken: Type | Function | string | symbol, + options?: GetOrResolveOptions, + ): TResult | Array; + + protected find( + typeOrToken: Type | Abstract | string | symbol, + options: { moduleId?: string; each?: boolean }, + ): TResult | Array { + const instanceLinkOrArray = this.instanceLinksHost.get( + typeOrToken, + options, + ); + const pluckInstance = ({ wrapperRef }: InstanceLink) => { + if ( + wrapperRef.scope === Scope.REQUEST || + wrapperRef.scope === Scope.TRANSIENT + ) { + throw new InvalidClassScopeException(typeOrToken); + } + return wrapperRef.instance; + }; + if (Array.isArray(instanceLinkOrArray)) { + return instanceLinkOrArray.map(pluckInstance); + } + return pluckInstance(instanceLinkOrArray); + } + + protected async resolvePerContext( + typeOrToken: Type | Abstract | string | symbol, + contextModule: Module, + contextId: ContextId, + options?: GetOrResolveOptions, + ): Promise> { + const instanceLinkOrArray = options?.strict + ? this.instanceLinksHost.get(typeOrToken, { + moduleId: contextModule.id, + each: options.each, + }) + : this.instanceLinksHost.get(typeOrToken, { + each: options.each, + }); + + const pluckInstance = async (instanceLink: InstanceLink) => { + const { wrapperRef, collection } = instanceLink; + if (wrapperRef.isDependencyTreeStatic() && !wrapperRef.isTransient) { + return this.get(typeOrToken, { strict: options.strict }); + } + + const ctorHost = wrapperRef.instance || { constructor: typeOrToken }; + const instance = await this.injector.loadPerContext( + ctorHost, + wrapperRef.host, + collection, + contextId, + wrapperRef, + ); + if (!instance) { + throw new UnknownElementException(); + } + return instance; + }; + + if (Array.isArray(instanceLinkOrArray)) { + // todo + return; + } + return pluckInstance(instanceLinkOrArray); + } +} diff --git a/packages/core/injector/container.ts b/packages/core/injector/container.ts index 1898d31f1c4..6fed38280e5 100644 --- a/packages/core/injector/container.ts +++ b/packages/core/injector/container.ts @@ -11,7 +11,7 @@ import { import { REQUEST } from '../router/request/request-constants'; import { ModuleCompiler } from './compiler'; import { ContextId } from './instance-wrapper'; -import { InternalCoreModule } from './internal-core-module'; +import { InternalCoreModule } from './internal-core-module/internal-core-module'; import { InternalProvidersStorage } from './internal-providers-storage'; import { Module } from './module'; import { ModuleTokenFactory } from './module-token-factory'; diff --git a/packages/core/injector/silent-logger.ts b/packages/core/injector/helpers/silent-logger.ts similarity index 100% rename from packages/core/injector/silent-logger.ts rename to packages/core/injector/helpers/silent-logger.ts diff --git a/packages/core/injector/index.ts b/packages/core/injector/index.ts index 4abbe0bb6ef..21e406d0661 100644 --- a/packages/core/injector/index.ts +++ b/packages/core/injector/index.ts @@ -1,6 +1,6 @@ export * from './container'; export * from './inquirer'; export { ContextId, HostComponentInfo } from './instance-wrapper'; -export * from './lazy-module-loader'; +export * from './lazy-module-loader/lazy-module-loader'; export * from './module-ref'; export * from './modules-container'; diff --git a/packages/core/injector/instance-links-host.ts b/packages/core/injector/instance-links-host.ts index a07617d28aa..e70f2e642fb 100644 --- a/packages/core/injector/instance-links-host.ts +++ b/packages/core/injector/instance-links-host.ts @@ -20,15 +20,30 @@ export class InstanceLinksHost { this.initialize(); } - get(token: InstanceToken, moduleId?: string): InstanceLink { - const modulesMap = this.instanceLinks.get(token); + get(token: InstanceToken): InstanceLink; + get( + token: InstanceToken, + options?: { moduleId?: string; each?: boolean }, + ): InstanceLink | Array>; + get( + token: InstanceToken, + options: { moduleId?: string; each?: boolean } = {}, + ): InstanceLink | Array> { + const instanceLinksForGivenToken = this.instanceLinks.get(token); - if (!modulesMap) { + if (!instanceLinksForGivenToken) { throw new UnknownElementException(this.getInstanceNameByToken(token)); } - const instanceLink = moduleId - ? modulesMap.find(item => item.moduleId === moduleId) - : modulesMap[modulesMap.length - 1]; + + if (options.each) { + return instanceLinksForGivenToken; + } + + const instanceLink = options.moduleId + ? instanceLinksForGivenToken.find( + item => item.moduleId === options.moduleId, + ) + : instanceLinksForGivenToken[instanceLinksForGivenToken.length - 1]; if (!instanceLink) { throw new UnknownElementException(this.getInstanceNameByToken(token)); diff --git a/packages/core/injector/instance-loader.ts b/packages/core/injector/instance-loader.ts index 0ae7687fdca..1b9ecb656e9 100644 --- a/packages/core/injector/instance-loader.ts +++ b/packages/core/injector/instance-loader.ts @@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common/interfaces/injectable.interface'; import { MODULE_INIT_MESSAGE } from '../helpers/messages'; import { NestContainer } from './container'; import { Injector } from './injector'; -import { InternalCoreModule } from './internal-core-module'; +import { InternalCoreModule } from './internal-core-module/internal-core-module'; import { Module } from './module'; export class InstanceLoader { diff --git a/packages/core/injector/internal-core-module-factory.ts b/packages/core/injector/internal-core-module/internal-core-module-factory.ts similarity index 71% rename from packages/core/injector/internal-core-module-factory.ts rename to packages/core/injector/internal-core-module/internal-core-module-factory.ts index 99ecd589a35..c1564e937bb 100644 --- a/packages/core/injector/internal-core-module-factory.ts +++ b/packages/core/injector/internal-core-module/internal-core-module-factory.ts @@ -1,13 +1,13 @@ import { Logger } from '@nestjs/common'; -import { ExternalContextCreator } from '../helpers/external-context-creator'; -import { HttpAdapterHost } from '../helpers/http-adapter-host'; -import { DependenciesScanner } from '../scanner'; -import { ModuleCompiler } from './compiler'; -import { NestContainer } from './container'; -import { InstanceLoader } from './instance-loader'; +import { ExternalContextCreator } from '../../helpers/external-context-creator'; +import { HttpAdapterHost } from '../../helpers/http-adapter-host'; +import { DependenciesScanner } from '../../scanner'; +import { ModuleCompiler } from '../compiler'; +import { NestContainer } from '../container'; +import { InstanceLoader } from '../instance-loader'; +import { LazyModuleLoader } from '../lazy-module-loader/lazy-module-loader'; +import { ModulesContainer } from '../modules-container'; import { InternalCoreModule } from './internal-core-module'; -import { LazyModuleLoader } from './lazy-module-loader'; -import { ModulesContainer } from './modules-container'; export class InternalCoreModuleFactory { static create( diff --git a/packages/core/injector/internal-core-module.ts b/packages/core/injector/internal-core-module/internal-core-module.ts similarity index 80% rename from packages/core/injector/internal-core-module.ts rename to packages/core/injector/internal-core-module/internal-core-module.ts index 3e3a6156fa6..1788183377b 100644 --- a/packages/core/injector/internal-core-module.ts +++ b/packages/core/injector/internal-core-module/internal-core-module.ts @@ -4,9 +4,9 @@ import { FactoryProvider, ValueProvider, } from '@nestjs/common/interfaces'; -import { requestProvider } from '../router/request/request-providers'; -import { Reflector } from '../services'; -import { inquirerProvider } from './inquirer/inquirer-providers'; +import { requestProvider } from '../../router/request/request-providers'; +import { Reflector } from '../../services'; +import { inquirerProvider } from '../inquirer/inquirer-providers'; const ReflectorAliasProvider = { provide: Reflector.name, diff --git a/packages/core/injector/lazy-module-loader-options.interface.ts b/packages/core/injector/lazy-module-loader/lazy-module-loader-options.interface.ts similarity index 100% rename from packages/core/injector/lazy-module-loader-options.interface.ts rename to packages/core/injector/lazy-module-loader/lazy-module-loader-options.interface.ts diff --git a/packages/core/injector/lazy-module-loader.ts b/packages/core/injector/lazy-module-loader/lazy-module-loader.ts similarity index 86% rename from packages/core/injector/lazy-module-loader.ts rename to packages/core/injector/lazy-module-loader/lazy-module-loader.ts index a9f6e29da24..df145e05ef4 100644 --- a/packages/core/injector/lazy-module-loader.ts +++ b/packages/core/injector/lazy-module-loader/lazy-module-loader.ts @@ -1,12 +1,12 @@ import { DynamicModule, Type } from '@nestjs/common'; -import { DependenciesScanner } from '../scanner'; -import { ModuleCompiler } from './compiler'; -import { InstanceLoader } from './instance-loader'; -import { SilentLogger } from './silent-logger'; +import { DependenciesScanner } from '../../scanner'; +import { ModuleCompiler } from '../compiler'; +import { SilentLogger } from '../helpers/silent-logger'; +import { InstanceLoader } from '../instance-loader'; +import { Module } from '../module'; +import { ModuleRef } from '../module-ref'; +import { ModulesContainer } from '../modules-container'; import { LazyModuleLoaderLoadOptions } from './lazy-module-loader-options.interface'; -import { Module } from './module'; -import { ModuleRef } from './module-ref'; -import { ModulesContainer } from './modules-container'; export class LazyModuleLoader { constructor( diff --git a/packages/core/injector/module-ref.ts b/packages/core/injector/module-ref.ts index 73d0d2a236f..d512e0afc25 100644 --- a/packages/core/injector/module-ref.ts +++ b/packages/core/injector/module-ref.ts @@ -1,38 +1,104 @@ import { IntrospectionResult, Scope, Type } from '@nestjs/common'; -import { - InvalidClassScopeException, - UnknownElementException, -} from '../errors/exceptions'; +import { GetOrResolveOptions } from '@nestjs/common/interfaces'; import { getClassScope } from '../helpers/get-class-scope'; import { isDurable } from '../helpers/is-durable'; +import { AbstractInstanceResolver } from './abstract-instance-resolver'; import { NestContainer } from './container'; import { Injector } from './injector'; import { InstanceLinksHost } from './instance-links-host'; import { ContextId, InstanceWrapper } from './instance-wrapper'; import { Module } from './module'; -export abstract class ModuleRef { - private readonly injector = new Injector(); +export abstract class ModuleRef extends AbstractInstanceResolver { + protected readonly injector = new Injector(); private _instanceLinksHost: InstanceLinksHost; - private get instanceLinksHost() { + protected get instanceLinksHost() { if (!this._instanceLinksHost) { this._instanceLinksHost = new InstanceLinksHost(this.container); } return this._instanceLinksHost; } - constructor(protected readonly container: NestContainer) {} + constructor(protected readonly container: NestContainer) { + super(); + } - public abstract get( + /** + * Retrieves an instance of either injectable or controller, otherwise, throws exception. + * @returns {TResult} + */ + abstract get( + typeOrToken: Type | Function | string | symbol, + ): TResult; + /** + * Retrieves an instance of either injectable or controller, otherwise, throws exception. + * @returns {TResult} + */ + abstract get( typeOrToken: Type | Function | string | symbol, - options?: { strict: boolean }, + options: { strict?: boolean; each?: undefined | false }, ): TResult; - public abstract resolve( + /** + * Retrieves a list of instances of either injectables or controllers, otherwise, throws exception. + * @returns {Array} + */ + abstract get( + typeOrToken: Type | Function | string | symbol, + options: { strict?: boolean; each: true }, + ): Array; + /** + * Retrieves an instance (or a list of instances) of either injectable or controller, otherwise, throws exception. + * @returns {TResult | Array} + */ + abstract get( + typeOrToken: Type | Function | string | symbol, + options?: GetOrResolveOptions, + ): TResult | Array; + + /** + * Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception. + * @returns {Array} + */ + abstract resolve( typeOrToken: Type | Function | string | symbol, - contextId?: ContextId, - options?: { strict: boolean }, ): Promise; + /** + * Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception. + * @returns {Array} + */ + abstract resolve( + typeOrToken: Type | Function | string | symbol, + contextId?: { id: number }, + ): Promise; + /** + * Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception. + * @returns {Array} + */ + abstract resolve( + typeOrToken: Type | Function | string | symbol, + contextId?: { id: number }, + options?: { strict?: boolean; each?: undefined | false }, + ): Promise; + /** + * Resolves transient or request-scoped instances of either injectables or controllers, otherwise, throws exception. + * @returns {Array} + */ + abstract resolve( + typeOrToken: Type | Function | string | symbol, + contextId?: { id: number }, + options?: { strict?: boolean; each: true }, + ): Promise>; + /** + * Resolves transient or request-scoped instance (or a list of instances) of either injectable or controller, otherwise, throws exception. + * @returns {Promise>} + */ + abstract resolve( + typeOrToken: Type | Function | string | symbol, + contextId?: { id: number }, + options?: GetOrResolveOptions, + ): Promise>; + public abstract create(type: Type): Promise; public introspect( @@ -53,54 +119,6 @@ export abstract class ModuleRef { this.container.registerRequestProvider(request, contextId); } - protected find( - typeOrToken: Type | string | symbol, - contextModule?: Module, - ): TResult { - const moduleId = contextModule && contextModule.id; - const { wrapperRef } = this.instanceLinksHost.get( - typeOrToken, - moduleId, - ); - if ( - wrapperRef.scope === Scope.REQUEST || - wrapperRef.scope === Scope.TRANSIENT - ) { - throw new InvalidClassScopeException(typeOrToken); - } - return wrapperRef.instance; - } - - protected async resolvePerContext( - typeOrToken: Type | string | symbol, - contextModule: Module, - contextId: ContextId, - options?: { strict: boolean }, - ): Promise { - const isStrictModeEnabled = options && options.strict; - const instanceLink = isStrictModeEnabled - ? this.instanceLinksHost.get(typeOrToken, contextModule.id) - : this.instanceLinksHost.get(typeOrToken); - - const { wrapperRef, collection } = instanceLink; - if (wrapperRef.isDependencyTreeStatic() && !wrapperRef.isTransient) { - return this.get(typeOrToken, options); - } - - const ctorHost = wrapperRef.instance || { constructor: typeOrToken }; - const instance = await this.injector.loadPerContext( - ctorHost, - wrapperRef.host, - collection, - contextId, - wrapperRef, - ); - if (!instance) { - throw new UnknownElementException(); - } - return instance; - } - protected async instantiateClass( type: Type, moduleRef: Module, diff --git a/packages/core/injector/module.ts b/packages/core/injector/module.ts index b7c73fca9ca..b8f408f7fd5 100644 --- a/packages/core/injector/module.ts +++ b/packages/core/injector/module.ts @@ -4,12 +4,13 @@ import { DynamicModule, ExistingProvider, FactoryProvider, + GetOrResolveOptions, Injectable, InjectionToken, NestModule, Provider, - ValueProvider, Type, + ValueProvider, } from '@nestjs/common/interfaces'; import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; import { @@ -520,19 +521,27 @@ export class Module { public get( typeOrToken: Type | string | symbol, - options: { strict: boolean } = { strict: true }, - ): TResult { + options: GetOrResolveOptions = { strict: true }, + ): TResult | Array { return !(options && options.strict) - ? this.find(typeOrToken) - : this.find(typeOrToken, self); + ? this.find(typeOrToken, options) + : this.find(typeOrToken, { + moduleId: self.id, + each: options.each, + }); } public resolve( typeOrToken: Type | string | symbol, contextId = createContextId(), - options: { strict: boolean } = { strict: true }, - ): Promise { - return this.resolvePerContext(typeOrToken, self, contextId, options); + options: GetOrResolveOptions = { strict: true }, + ): Promise> { + return this.resolvePerContext( + typeOrToken, + self, + contextId, + options, + ); } public async create(type: Type): Promise { diff --git a/packages/core/nest-application-context.ts b/packages/core/nest-application-context.ts index 18c8c6ca7dc..0ea5b2d55db 100644 --- a/packages/core/nest-application-context.ts +++ b/packages/core/nest-application-context.ts @@ -8,17 +8,13 @@ import { import { Abstract, DynamicModule, - Scope, + GetOrResolveOptions, Type, } from '@nestjs/common/interfaces'; import { isEmpty } from '@nestjs/common/utils/shared.utils'; import { iterate } from 'iterare'; import { MESSAGES } from './constants'; -import { - InvalidClassScopeException, - UnknownElementException, - UnknownModuleException, -} from './errors/exceptions'; +import { UnknownModuleException } from './errors/exceptions'; import { createContextId } from './helpers/context-id-factory'; import { callAppShutdownHook, @@ -27,6 +23,7 @@ import { callModuleDestroyHook, callModuleInitHook, } from './hooks'; +import { AbstractInstanceResolver } from './injector/abstract-instance-resolver'; import { ModuleCompiler } from './injector/compiler'; import { NestContainer } from './injector/container'; import { Injector } from './injector/injector'; @@ -37,7 +34,10 @@ import { Module } from './injector/module'; /** * @publicApi */ -export class NestApplicationContext implements INestApplicationContext { +export class NestApplicationContext + extends AbstractInstanceResolver + implements INestApplicationContext +{ protected isInitialized = false; protected readonly injector = new Injector(); @@ -48,7 +48,7 @@ export class NestApplicationContext implements INestApplicationContext { private _instanceLinksHost: InstanceLinksHost; private _moduleRefsByDistance?: Array; - private get instanceLinksHost() { + protected get instanceLinksHost() { if (!this._instanceLinksHost) { this._instanceLinksHost = new InstanceLinksHost(this.container); } @@ -59,7 +59,9 @@ export class NestApplicationContext implements INestApplicationContext { protected readonly container: NestContainer, private readonly scope = new Array>(), private contextModule: Module = null, - ) {} + ) { + super(); + } public selectContextModule() { const modules = this.container.getModules().values(); @@ -87,19 +89,22 @@ export class NestApplicationContext implements INestApplicationContext { public get( typeOrToken: Type | Abstract | string | symbol, - options: { strict: boolean } = { strict: false }, - ): TResult { + options: GetOrResolveOptions = { strict: false }, + ): TResult | Array { return !(options && options.strict) - ? this.find(typeOrToken) - : this.find(typeOrToken, this.contextModule); + ? this.find(typeOrToken, options) + : this.find(typeOrToken, { + moduleId: this.contextModule?.id, + each: options.each, + }); } public resolve( typeOrToken: Type | Abstract | string | symbol, contextId = createContextId(), - options: { strict: boolean } = { strict: false }, - ): Promise { - return this.resolvePerContext( + options: GetOrResolveOptions = { strict: false }, + ): Promise> { + return this.resolvePerContext( typeOrToken, this.contextModule, contextId, @@ -290,54 +295,6 @@ export class NestApplicationContext implements INestApplicationContext { } } - protected find( - typeOrToken: Type | Abstract | string | symbol, - contextModule?: Module, - ): TResult { - const moduleId = contextModule && contextModule.id; - const { wrapperRef } = this.instanceLinksHost.get( - typeOrToken, - moduleId, - ); - if ( - wrapperRef.scope === Scope.REQUEST || - wrapperRef.scope === Scope.TRANSIENT - ) { - throw new InvalidClassScopeException(typeOrToken); - } - return wrapperRef.instance; - } - - protected async resolvePerContext( - typeOrToken: Type | Abstract | string | symbol, - contextModule: Module, - contextId: ContextId, - options?: { strict: boolean }, - ): Promise { - const isStrictModeEnabled = options && options.strict; - const instanceLink = isStrictModeEnabled - ? this.instanceLinksHost.get(typeOrToken, contextModule.id) - : this.instanceLinksHost.get(typeOrToken); - - const { wrapperRef, collection } = instanceLink; - if (wrapperRef.isDependencyTreeStatic() && !wrapperRef.isTransient) { - return this.get(typeOrToken, options); - } - - const ctorHost = wrapperRef.instance || { constructor: typeOrToken }; - const instance = await this.injector.loadPerContext( - ctorHost, - wrapperRef.host, - collection, - contextId, - wrapperRef, - ); - if (!instance) { - throw new UnknownElementException(); - } - return instance; - } - private getModulesSortedByDistance(): Module[] { if (this._moduleRefsByDistance) { return this._moduleRefsByDistance; diff --git a/packages/core/repl/repl-context.ts b/packages/core/repl/repl-context.ts index bfc5ef234e3..4745b8f8e06 100644 --- a/packages/core/repl/repl-context.ts +++ b/packages/core/repl/repl-context.ts @@ -5,7 +5,7 @@ import { } from '@nestjs/common'; import { ApplicationConfig } from '../application-config'; import { ModuleRef, NestContainer } from '../injector'; -import { InternalCoreModule } from '../injector/internal-core-module'; +import { InternalCoreModule } from '../injector/internal-core-module/internal-core-module'; import { Module } from '../injector/module'; import { DebugReplFn, diff --git a/packages/core/scanner.ts b/packages/core/scanner.ts index 97a78f66dea..0bf28015f95 100644 --- a/packages/core/scanner.ts +++ b/packages/core/scanner.ts @@ -46,7 +46,7 @@ import { UndefinedModuleException } from './errors/exceptions/undefined-module.e 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-factory'; +import { InternalCoreModuleFactory } from './injector/internal-core-module/internal-core-module-factory'; import { Module } from './injector/module'; import { MetadataScanner } from './metadata-scanner'; diff --git a/packages/core/test/injector/internal-core-module-factory.spec.ts b/packages/core/test/injector/internal-core-module/internal-core-module-factory.spec.ts similarity index 66% rename from packages/core/test/injector/internal-core-module-factory.spec.ts rename to packages/core/test/injector/internal-core-module/internal-core-module-factory.spec.ts index 92499cf9567..6222e4d2ae2 100644 --- a/packages/core/test/injector/internal-core-module-factory.spec.ts +++ b/packages/core/test/injector/internal-core-module/internal-core-module-factory.spec.ts @@ -1,11 +1,11 @@ import { ClassProvider, FactoryProvider } from '@nestjs/common'; import { expect } from 'chai'; -import { ExternalContextCreator } from '../../helpers/external-context-creator'; -import { HttpAdapterHost } from '../../helpers/http-adapter-host'; -import { LazyModuleLoader, ModulesContainer } from '../../injector'; -import { NestContainer } from '../../injector/container'; -import { InternalCoreModule } from '../../injector/internal-core-module'; -import { InternalCoreModuleFactory } from '../../injector/internal-core-module-factory'; +import { ExternalContextCreator } from '../../../helpers/external-context-creator'; +import { HttpAdapterHost } from '../../../helpers/http-adapter-host'; +import { LazyModuleLoader, ModulesContainer } from '../../../injector'; +import { NestContainer } from '../../../injector/container'; +import { InternalCoreModule } from '../../../injector/internal-core-module/internal-core-module'; +import { InternalCoreModuleFactory } from '../../../injector/internal-core-module/internal-core-module-factory'; describe('InternalCoreModuleFactory', () => { it('should return the internal core module definition', () => { diff --git a/packages/core/test/injector/lazy-module-loader.spec.ts b/packages/core/test/injector/lazy-module-loader/lazy-module-loader.spec.ts similarity index 89% rename from packages/core/test/injector/lazy-module-loader.spec.ts rename to packages/core/test/injector/lazy-module-loader/lazy-module-loader.spec.ts index 35b0f8a45a5..24cb6f7af90 100644 --- a/packages/core/test/injector/lazy-module-loader.spec.ts +++ b/packages/core/test/injector/lazy-module-loader/lazy-module-loader.spec.ts @@ -5,10 +5,10 @@ import { ModuleRef, ModulesContainer, NestContainer, -} from '../../injector'; -import { InstanceLoader } from '../../injector/instance-loader'; -import { MetadataScanner } from '../../metadata-scanner'; -import { DependenciesScanner } from '../../scanner'; +} from '../../../injector'; +import { InstanceLoader } from '../../../injector/instance-loader'; +import { MetadataScanner } from '../../../metadata-scanner'; +import { DependenciesScanner } from '../../../scanner'; describe('LazyModuleLoader', () => { let lazyModuleLoader: LazyModuleLoader; From debf451b0dac2ec556e9c9025e90729853a6ff1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Sat, 5 Nov 2022 16:22:54 +0100 Subject: [PATCH 2/7] chore(core): add comments/method overloads for better dev experience --- packages/core/nest-application-context.ts | 103 ++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/packages/core/nest-application-context.ts b/packages/core/nest-application-context.ts index 0ea5b2d55db..d050b51d099 100644 --- a/packages/core/nest-application-context.ts +++ b/packages/core/nest-application-context.ts @@ -68,6 +68,10 @@ export class NestApplicationContext this.contextModule = modules.next().value; } + /** + * Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module. + * @returns {INestApplicationContext} + */ public select( moduleType: Type | DynamicModule, ): INestApplicationContext { @@ -87,6 +91,39 @@ export class NestApplicationContext return new NestApplicationContext(this.container, scope, selectedModule); } + /** + * Retrieves an instance of either injectable or controller, otherwise, throws exception. + * @returns {TResult} + */ + public get( + typeOrToken: Type | Function | string | symbol, + ): TResult; + /** + * Retrieves an instance of either injectable or controller, otherwise, throws exception. + * @returns {TResult} + */ + public get( + typeOrToken: Type | Function | string | symbol, + options: { + strict?: boolean; + each?: undefined | false; + }, + ): TResult; + /** + * Retrieves a list of instances of either injectables or controllers, otherwise, throws exception. + * @returns {Array} + */ + public get( + typeOrToken: Type | Function | string | symbol, + options: { + strict?: boolean; + each: true; + }, + ): Array; + /** + * Retrieves an instance (or a list of instances) of either injectable or controller, otherwise, throws exception. + * @returns {TResult | Array} + */ public get( typeOrToken: Type | Abstract | string | symbol, options: GetOrResolveOptions = { strict: false }, @@ -99,6 +136,55 @@ export class NestApplicationContext }); } + /** + * Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception. + * @returns {Array} + */ + public resolve( + typeOrToken: Type | Function | string | symbol, + ): Promise; + /** + * Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception. + * @returns {Array} + */ + public resolve( + typeOrToken: Type | Function | string | symbol, + contextId?: { + id: number; + }, + ): Promise; + /** + * Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception. + * @returns {Array} + */ + public resolve( + typeOrToken: Type | Function | string | symbol, + contextId?: { + id: number; + }, + options?: { + strict?: boolean; + each?: undefined | false; + }, + ): Promise; + /** + * Resolves transient or request-scoped instances of either injectables or controllers, otherwise, throws exception. + * @returns {Array} + */ + public resolve( + typeOrToken: Type | Function | string | symbol, + contextId?: { + id: number; + }, + options?: { + strict?: boolean; + each: true; + }, + ): Promise>; + /** + * Resolves transient or request-scoped instance (or a list of instances) of either injectable or controller, otherwise, throws exception. + * @returns {Promise>} + */ public resolve( typeOrToken: Type | Abstract | string | symbol, contextId = createContextId(), @@ -112,6 +198,10 @@ export class NestApplicationContext ); } + /** + * Registers the request/context object for a given context ID (DI container sub-tree). + * @returns {void} + */ public registerRequestByContextId(request: T, contextId: ContextId) { this.container.registerRequestProvider(request, contextId); } @@ -133,6 +223,10 @@ export class NestApplicationContext return this; } + /** + * Terminates the application + * @returns {Promise} + */ public async close(): Promise { await this.callDestroyHook(); await this.callBeforeShutdownHook(); @@ -141,6 +235,11 @@ export class NestApplicationContext this.unsubscribeFromProcessSignals(); } + /** + * Sets custom logger service. + * Flushes buffered logs if auto flush is on. + * @returns {void} + */ public useLogger(logger: LoggerService | LogLevel[] | false) { Logger.overrideLogger(logger); @@ -149,6 +248,10 @@ export class NestApplicationContext } } + /** + * Prints buffered logs and detaches buffer. + * @returns {void} + */ public flushLogs() { Logger.flush(); } From ef590d3f67479f080f1d09b74f626d033e42c73b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Mon, 7 Nov 2022 09:04:12 +0100 Subject: [PATCH 3/7] fix: add index.ts file for backward compatibility --- packages/core/injector/internal-core-module/index.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/core/injector/internal-core-module/index.ts diff --git a/packages/core/injector/internal-core-module/index.ts b/packages/core/injector/internal-core-module/index.ts new file mode 100644 index 00000000000..a863c2132ae --- /dev/null +++ b/packages/core/injector/internal-core-module/index.ts @@ -0,0 +1 @@ +export * from './internal-core-module'; From c9d7f7b0b06f7cbe68f597e88de4a6f08025d633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Mon, 7 Nov 2022 09:32:04 +0100 Subject: [PATCH 4/7] chore: add comment to the get or resolve options object --- .../interfaces/nest-application-context.interface.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/common/interfaces/nest-application-context.interface.ts b/packages/common/interfaces/nest-application-context.interface.ts index 5d48b4aec13..554fa1d1b67 100644 --- a/packages/common/interfaces/nest-application-context.interface.ts +++ b/packages/common/interfaces/nest-application-context.interface.ts @@ -4,7 +4,16 @@ import { DynamicModule } from './modules'; import { Type } from './type.interface'; export interface GetOrResolveOptions { + /** + * If enabled, lookup will only be performed in the host module. + * @default false + */ strict?: boolean; + /** + * If enabled, instead of returning a first instance registered under a given token, + * a list of instances will be returned. + * @default false + */ each?: boolean; } From ba75fd7a7192a8636ea969f9bcbc1d754bf66d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Mon, 7 Nov 2022 10:13:57 +0100 Subject: [PATCH 5/7] feat(core): implement resolve when each is set to true --- packages/core/injector/abstract-instance-resolver.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/injector/abstract-instance-resolver.ts b/packages/core/injector/abstract-instance-resolver.ts index 1775c24f19a..0308574d60f 100644 --- a/packages/core/injector/abstract-instance-resolver.ts +++ b/packages/core/injector/abstract-instance-resolver.ts @@ -77,8 +77,9 @@ export abstract class AbstractInstanceResolver { }; if (Array.isArray(instanceLinkOrArray)) { - // todo - return; + return Promise.all( + instanceLinkOrArray.map(instanceLink => pluckInstance(instanceLink)), + ); } return pluckInstance(instanceLinkOrArray); } From a8e5bc6c9b3b2d8322b258fa0cf82476ebb07098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Mon, 7 Nov 2022 10:14:22 +0100 Subject: [PATCH 6/7] test(core): add multiple providers resolution integration tests --- .../injector/e2e/multiple-providers.spec.ts | 49 +++++++++++++++++++ .../src/multiple-providers/a.module.ts | 16 ++++++ .../src/multiple-providers/b.module.ts | 16 ++++++ .../src/multiple-providers/c.module.ts | 16 ++++++ .../multiple-providers.module.ts | 9 ++++ 5 files changed, 106 insertions(+) create mode 100644 integration/injector/e2e/multiple-providers.spec.ts create mode 100644 integration/injector/src/multiple-providers/a.module.ts create mode 100644 integration/injector/src/multiple-providers/b.module.ts create mode 100644 integration/injector/src/multiple-providers/c.module.ts create mode 100644 integration/injector/src/multiple-providers/multiple-providers.module.ts diff --git a/integration/injector/e2e/multiple-providers.spec.ts b/integration/injector/e2e/multiple-providers.spec.ts new file mode 100644 index 00000000000..c702fa77f2d --- /dev/null +++ b/integration/injector/e2e/multiple-providers.spec.ts @@ -0,0 +1,49 @@ +import { Test } from '@nestjs/testing'; +import { expect } from 'chai'; +import { MultipleProvidersModule } from '../src/multiple-providers/multiple-providers.module'; + +describe('Multiple providers under the same token ("each" feature)', () => { + describe('get()', () => { + it('should return an array of providers', async () => { + const builder = Test.createTestingModule({ + imports: [MultipleProvidersModule], + }); + const testingModule = await builder.compile(); + + const multiProviderInstances = testingModule.get( + 'MULTI_PROVIDER', + { + each: true, + }, + ); + + // make sure "multiProviderInstances" is string[] not string + // @ts-expect-error + multiProviderInstances.charAt; + + expect(multiProviderInstances).to.be.eql(['A', 'B', 'C']); + }); + }); + describe('resolve()', () => { + it('should return an array of providers', async () => { + const builder = Test.createTestingModule({ + imports: [MultipleProvidersModule], + }); + const testingModule = await builder.compile(); + + const multiProviderInstances = await testingModule.resolve( + 'REQ_SCOPED_MULTI_PROVIDER', + undefined, + { + each: true, + }, + ); + + // make sure "multiProviderInstances" is string[] not string + // @ts-expect-error + multiProviderInstances.charAt; + + expect(multiProviderInstances).to.be.eql(['A', 'B', 'C']); + }); + }); +}); diff --git a/integration/injector/src/multiple-providers/a.module.ts b/integration/injector/src/multiple-providers/a.module.ts new file mode 100644 index 00000000000..95ae5eea136 --- /dev/null +++ b/integration/injector/src/multiple-providers/a.module.ts @@ -0,0 +1,16 @@ +import { Module, Scope } from '@nestjs/common'; + +@Module({ + providers: [ + { + provide: 'MULTI_PROVIDER', + useValue: 'A', + }, + { + provide: 'REQ_SCOPED_MULTI_PROVIDER', + useFactory: () => 'A', + scope: Scope.REQUEST, + }, + ], +}) +export class AModule {} diff --git a/integration/injector/src/multiple-providers/b.module.ts b/integration/injector/src/multiple-providers/b.module.ts new file mode 100644 index 00000000000..1ed8ac423aa --- /dev/null +++ b/integration/injector/src/multiple-providers/b.module.ts @@ -0,0 +1,16 @@ +import { Module, Scope } from '@nestjs/common'; + +@Module({ + providers: [ + { + provide: 'MULTI_PROVIDER', + useValue: 'B', + }, + { + provide: 'REQ_SCOPED_MULTI_PROVIDER', + useFactory: () => 'B', + scope: Scope.REQUEST, + }, + ], +}) +export class BModule {} diff --git a/integration/injector/src/multiple-providers/c.module.ts b/integration/injector/src/multiple-providers/c.module.ts new file mode 100644 index 00000000000..4def2a71a3d --- /dev/null +++ b/integration/injector/src/multiple-providers/c.module.ts @@ -0,0 +1,16 @@ +import { Module, Scope } from '@nestjs/common'; + +@Module({ + providers: [ + { + provide: 'MULTI_PROVIDER', + useValue: 'C', + }, + { + provide: 'REQ_SCOPED_MULTI_PROVIDER', + useFactory: () => 'C', + scope: Scope.REQUEST, + }, + ], +}) +export class CModule {} diff --git a/integration/injector/src/multiple-providers/multiple-providers.module.ts b/integration/injector/src/multiple-providers/multiple-providers.module.ts new file mode 100644 index 00000000000..1bb2345eeec --- /dev/null +++ b/integration/injector/src/multiple-providers/multiple-providers.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AModule } from './a.module'; +import { BModule } from './b.module'; +import { CModule } from './c.module'; + +@Module({ + imports: [AModule, BModule, CModule], +}) +export class MultipleProvidersModule {} From 92c37ddc3b9d5321c2600d5b36b34f830942cdd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20My=C5=9Bliwiec?= Date: Mon, 7 Nov 2022 10:23:24 +0100 Subject: [PATCH 7/7] style: address linter errors, move ts-expect-error desc --- integration/injector/e2e/multiple-providers.spec.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/integration/injector/e2e/multiple-providers.spec.ts b/integration/injector/e2e/multiple-providers.spec.ts index c702fa77f2d..26daa0e437a 100644 --- a/integration/injector/e2e/multiple-providers.spec.ts +++ b/integration/injector/e2e/multiple-providers.spec.ts @@ -17,8 +17,7 @@ describe('Multiple providers under the same token ("each" feature)', () => { }, ); - // make sure "multiProviderInstances" is string[] not string - // @ts-expect-error + // @ts-expect-error: make sure "multiProviderInstances" is string[] not string multiProviderInstances.charAt; expect(multiProviderInstances).to.be.eql(['A', 'B', 'C']); @@ -39,8 +38,7 @@ describe('Multiple providers under the same token ("each" feature)', () => { }, ); - // make sure "multiProviderInstances" is string[] not string - // @ts-expect-error + // @ts-expect-error: make sure "multiProviderInstances" is string[] not string multiProviderInstances.charAt; expect(multiProviderInstances).to.be.eql(['A', 'B', 'C']);