Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(core): expose each option to get and resolve methods #10521

Merged
merged 7 commits into from Nov 7, 2022
47 changes: 47 additions & 0 deletions integration/injector/e2e/multiple-providers.spec.ts
@@ -0,0 +1,47 @@
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<string>(
'MULTI_PROVIDER',
{
each: true,
},
);

// @ts-expect-error: make sure "multiProviderInstances" is string[] not string
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<string>(
'REQ_SCOPED_MULTI_PROVIDER',
undefined,
{
each: true,
},
);

// @ts-expect-error: make sure "multiProviderInstances" is string[] not string
multiProviderInstances.charAt;

expect(multiProviderInstances).to.be.eql(['A', 'B', 'C']);
});
});
});
16 changes: 16 additions & 0 deletions 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 {}
16 changes: 16 additions & 0 deletions 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 {}
16 changes: 16 additions & 0 deletions 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 {}
@@ -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 {}
76 changes: 73 additions & 3 deletions packages/common/interfaces/nest-application-context.interface.ts
Expand Up @@ -3,6 +3,20 @@ import { LoggerService, LogLevel } from '../services/logger.service';
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;
}

/**
* Interface defining NestApplicationContext.
*
Expand All @@ -21,18 +35,74 @@ export interface INestApplicationContext {
*/
get<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | Function | string | symbol,
options?: { strict: boolean },
): TResult;
/**
* Retrieves an instance of either injectable or controller, otherwise, throws exception.
* @returns {TResult}
*/
get<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | 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<TResult>}
*/
get<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | Function | string | symbol,
options: { strict?: boolean; each: true },
): Array<TResult>;
/**
* Retrieves an instance (or a list of instances) of either injectable or controller, otherwise, throws exception.
* @returns {TResult | Array<TResult>}
*/
get<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | Function | string | symbol,
options?: GetOrResolveOptions,
): TResult | Array<TResult>;

/**
* Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception.
* @returns {Promise<TResult>}
* @returns {Array<TResult>}
*/
resolve<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | Function | string | symbol,
): Promise<TResult>;
/**
* Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception.
* @returns {Array<TResult>}
*/
resolve<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | Function | string | symbol,
contextId?: { id: number },
): Promise<TResult>;
/**
* Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception.
* @returns {Array<TResult>}
*/
resolve<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | Function | string | symbol,
contextId?: { id: number },
options?: { strict: boolean },
options?: { strict?: boolean; each?: undefined | false },
): Promise<TResult>;
/**
* Resolves transient or request-scoped instances of either injectables or controllers, otherwise, throws exception.
* @returns {Array<TResult>}
*/
resolve<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | Function | string | symbol,
contextId?: { id: number },
options?: { strict?: boolean; each: true },
): Promise<Array<TResult>>;
/**
* Resolves transient or request-scoped instance (or a list of instances) of either injectable or controller, otherwise, throws exception.
* @returns {Promise<TResult | Array<TResult>>}
*/
resolve<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | Function | string | symbol,
contextId?: { id: number },
options?: GetOrResolveOptions,
): Promise<TResult | Array<TResult>>;

/**
* Registers the request/context object for a given context ID (DI container sub-tree).
Expand Down
86 changes: 86 additions & 0 deletions packages/core/injector/abstract-instance-resolver.ts
@@ -0,0 +1,86 @@
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<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | Function | string | symbol,
options?: GetOrResolveOptions,
): TResult | Array<TResult>;

protected find<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | Abstract<TInput> | string | symbol,
options: { moduleId?: string; each?: boolean },
): TResult | Array<TResult> {
const instanceLinkOrArray = this.instanceLinksHost.get<TResult>(
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<TInput = any, TResult = TInput>(
typeOrToken: Type<TInput> | Abstract<TInput> | string | symbol,
contextModule: Module,
contextId: ContextId,
options?: GetOrResolveOptions,
): Promise<TResult | Array<TResult>> {
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)) {
return Promise.all(
instanceLinkOrArray.map(instanceLink => pluckInstance(instanceLink)),
);
}
return pluckInstance(instanceLinkOrArray);
}
}
2 changes: 1 addition & 1 deletion packages/core/injector/container.ts
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion 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';
27 changes: 21 additions & 6 deletions packages/core/injector/instance-links-host.ts
Expand Up @@ -20,15 +20,30 @@ export class InstanceLinksHost {
this.initialize();
}

get<T = any>(token: InstanceToken, moduleId?: string): InstanceLink<T> {
const modulesMap = this.instanceLinks.get(token);
get<T = any>(token: InstanceToken): InstanceLink<T>;
get<T = any>(
token: InstanceToken,
options?: { moduleId?: string; each?: boolean },
): InstanceLink<T> | Array<InstanceLink<T>>;
get<T = any>(
token: InstanceToken,
options: { moduleId?: string; each?: boolean } = {},
): InstanceLink<T> | Array<InstanceLink<T>> {
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));
Expand Down
2 changes: 1 addition & 1 deletion packages/core/injector/instance-loader.ts
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions packages/core/injector/internal-core-module/index.ts
@@ -0,0 +1 @@
export * from './internal-core-module';
@@ -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(
Expand Down
Expand Up @@ -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,
Expand Down