Skip to content

Commit

Permalink
Merge pull request #9935 from random42/feature/configurable-module-in…
Browse files Browse the repository at this point in the history
…ject-providers

feat(common): pass options to nested async modules
  • Loading branch information
kamilmysliwiec committed Jul 28, 2022
2 parents 7222cd1 + b0e813a commit 23fa0f9
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
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
@@ -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
@@ -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;
}
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
@@ -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 23fa0f9

Please sign in to comment.