From b0e813ace4fedf532a7d8da176969d490bb175a5 Mon Sep 17 00:00:00 2001 From: Roberto Sero <> Date: Sat, 16 Jul 2022 17:18:11 +0200 Subject: [PATCH] feat(common): pass options to nested async modules Pass parent module providers to 'provideInjectionTokensFrom' in ConfigurableModuleAsyncOptions in order to pass options down in nested async modules. Only necessary providers are taken by recursively looking on the 'inject' array. --- .../configurable-module.builder.ts | 10 ++++ ...igurable-module-async-options.interface.ts | 13 ++++- .../utils/get-injection-providers.util.ts | 50 +++++++++++++++++++ .../configurable-module.builder.spec.ts | 28 ++++++++++- .../get-injection-providers.util.spec.ts | 49 ++++++++++++++++++ 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 packages/common/module-utils/utils/get-injection-providers.util.ts create mode 100644 packages/common/test/module-utils/utils/get-injection-providers.util.spec.ts 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 56f7040e45c..b1300ab61a3 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]]); + }); +});