diff --git a/packages/common/module-utils/configurable-module.builder.ts b/packages/common/module-utils/configurable-module.builder.ts index 0e3f1318f20..7b93f3dd26f 100644 --- a/packages/common/module-utils/configurable-module.builder.ts +++ b/packages/common/module-utils/configurable-module.builder.ts @@ -14,6 +14,7 @@ import { } from './interfaces'; import { ConfigurableModuleHost } from './interfaces/configurable-module-host.interface'; import { generateOptionsInjectionToken } from './utils/generate-options-injection-token.util'; +import { getInjectionProviders } from './utils/get-injection-providers.util'; /** * @publicApi @@ -269,6 +270,15 @@ export class ConfigurableModuleBuilder< options: ConfigurableModuleAsyncOptions, ): Provider[] { if (options.useExisting || options.useFactory) { + if (options.inject && options.provideInjectionTokensFrom) { + return [ + this.createAsyncOptionsProvider(options), + ...getInjectionProviders( + options.provideInjectionTokensFrom, + options.inject, + ), + ]; + } return [this.createAsyncOptionsProvider(options)]; } return [ diff --git a/packages/common/module-utils/interfaces/configurable-module-async-options.interface.ts b/packages/common/module-utils/interfaces/configurable-module-async-options.interface.ts index a45ecceebbc..ed1ec628813 100644 --- a/packages/common/module-utils/interfaces/configurable-module-async-options.interface.ts +++ b/packages/common/module-utils/interfaces/configurable-module-async-options.interface.ts @@ -1,4 +1,9 @@ -import { FactoryProvider, ModuleMetadata, Type } from '../../interfaces'; +import { + FactoryProvider, + ModuleMetadata, + Provider, + Type, +} from '../../interfaces'; import { DEFAULT_FACTORY_CLASS_METHOD_KEY } from '../constants'; /** @@ -48,4 +53,10 @@ export interface ConfigurableModuleAsyncOptions< * Dependencies that a Factory may inject. */ inject?: FactoryProvider['inject']; + /** + * List of parent module's providers that will be filtered to only provide necessary + * providers for the 'inject' array + * useful to pass options to nested async modules + */ + provideInjectionTokensFrom?: Provider[]; } diff --git a/packages/common/module-utils/utils/get-injection-providers.util.ts b/packages/common/module-utils/utils/get-injection-providers.util.ts new file mode 100644 index 00000000000..26743580b30 --- /dev/null +++ b/packages/common/module-utils/utils/get-injection-providers.util.ts @@ -0,0 +1,50 @@ +import { + InjectionToken, + Provider, + FactoryProvider, + OptionalFactoryDependency, +} from '../../interfaces'; + +/** + * check if x is OptionalFactoryDependency, based on prototype presence + * (to avoid classes with a static 'token' field) + * @param x + * @returns x is OptionalFactoryDependency + */ +function isOptionalFactoryDependency( + x: InjectionToken | OptionalFactoryDependency, +): x is OptionalFactoryDependency { + return !!((x as any)?.token && !(x as any)?.prototype); +} + +const mapInjectToTokens = (t: InjectionToken | OptionalFactoryDependency) => + isOptionalFactoryDependency(t) ? t.token : t; + +/** + * + * @param providers List of a module's providers + * @param tokens Injection tokens needed for a useFactory function (usually the module's options' token) + * @returns All the providers needed for the tokens' injection (searched recursively) + */ +export function getInjectionProviders( + providers: Provider[], + tokens: FactoryProvider['inject'], +): Provider[] { + const result: Provider[] = []; + let search: InjectionToken[] = tokens.map(mapInjectToTokens); + while (search.length > 0) { + const match = (providers ?? []).filter( + p => + !result.includes(p) && // this prevents circular loops and duplication + (search.includes(p as any) || search.includes((p as any)?.provide)), + ); + result.push(...match); + // get injection tokens of the matched providers, if any + search = match + .filter(p => (p as any)?.inject) + .map(p => (p as FactoryProvider).inject) + .flat() + .map(mapInjectToTokens); + } + return result; +} diff --git a/packages/common/test/module-utils/configurable-module.builder.spec.ts b/packages/common/test/module-utils/configurable-module.builder.spec.ts index 3d36232718a..3cca9a33f90 100644 --- a/packages/common/test/module-utils/configurable-module.builder.spec.ts +++ b/packages/common/test/module-utils/configurable-module.builder.spec.ts @@ -77,15 +77,41 @@ describe('ConfigurableModuleBuilder', () => { ) .build(); + const provideInjectionTokensFrom: Provider[] = [ + { + provide: 'a', + useFactory: () => {}, + inject: ['b'], + }, + { + provide: 'b', + useFactory: () => {}, + inject: ['x'], + }, + { + provide: 'c', + useFactory: () => {}, + inject: ['y'], + }, + ]; const definition = ConfigurableModuleClass.forFeatureAsync({ useFactory: () => {}, + inject: ['a'], + provideInjectionTokensFrom, isGlobal: true, extraProviders: ['test' as any], }); expect(definition.global).to.equal(true); - expect(definition.providers).to.have.length(3); + expect(definition.providers).to.have.length(5); + console.log(definition.providers); expect(definition.providers).to.deep.contain('test'); + expect(definition.providers).to.include.members( + provideInjectionTokensFrom.slice(0, 2), + ); + expect(definition.providers).not.to.include( + provideInjectionTokensFrom[2], + ); expect(MODULE_OPTIONS_TOKEN).to.equal('RANDOM_TEST_MODULE_OPTIONS'); expect((definition.providers[0] as any).provide).to.equal( 'RANDOM_TEST_MODULE_OPTIONS', diff --git a/packages/common/test/module-utils/utils/get-injection-providers.util.spec.ts b/packages/common/test/module-utils/utils/get-injection-providers.util.spec.ts new file mode 100644 index 00000000000..fa9d29fce39 --- /dev/null +++ b/packages/common/test/module-utils/utils/get-injection-providers.util.spec.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai'; +import { Provider } from '../../../interfaces'; +import { getInjectionProviders } from '../../../module-utils/utils/get-injection-providers.util'; + +describe('getInjectionProviders', () => { + it('should take only required providers', () => { + class C { + static token = 'a'; + } + const p: Provider[] = [ + { + provide: 'a', + useValue: 'a', + }, + { + provide: 'b', + useValue: 'b', + }, + C, + { + provide: 'd', + useFactory: (c, b) => [c, b], + inject: [ + C, + { + token: 'b', + optional: true, + }, + 'x', + ], + }, + { + provide: 'e', + useFactory: (d, b) => [d, b], + inject: ['d', 'b'], + }, + { + provide: 'f', + useValue: 'f', + }, + ]; + // should not include 'a' and 'f' + const expected = p.slice(1, -1); + const result = getInjectionProviders(p, ['e']); + expect(result).to.have.length(expected.length); + expect(result).to.have.members(expected); + expect(result).not.to.have.members([p[0], p[5]]); + }); +});