Skip to content

Commit

Permalink
feat(common): pass options to nested async modules
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Roberto Sero committed Jul 16, 2022
1 parent 009f6d2 commit b0e813a
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 2 deletions.
10 changes: 10 additions & 0 deletions packages/common/module-utils/configurable-module.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -269,6 +270,15 @@ export class ConfigurableModuleBuilder<
options: ConfigurableModuleAsyncOptions<ModuleOptions>,
): 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 [
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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[];
}
50 changes: 50 additions & 0 deletions packages/common/module-utils/utils/get-injection-providers.util.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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]]);
});
});

0 comments on commit b0e813a

Please sign in to comment.