Skip to content

Commit

Permalink
Merge pull request #10521 from nestjs/feat/add-each-to-resolve-and-get
Browse files Browse the repository at this point in the history
feat(core): expose each option to get and resolve methods
  • Loading branch information
kamilmysliwiec committed Nov 7, 2022
2 parents f421800 + 92c37dd commit 8039161
Show file tree
Hide file tree
Showing 24 changed files with 538 additions and 175 deletions.
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
File renamed without changes.
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

0 comments on commit 8039161

Please sign in to comment.