Repository: nestjs/nest
Pull request #9935 (2022-07-16 15:47:34)

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.

## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [x] Docs have been added / updated (for bug fixes / features)


## PR Type
What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Other... Please describe:

## What is the current behavior?

Right now it is not easy to have a hierarchy of async modules because options' providers are not passed down the hierarchy. [This](https://stackoverflow.com/questions/65875076/nestjs-nested-async-inits-in-dynamic-modules) is an example of the problem. The solution I gave (pushing the options' providers after creating the module) breaks as soon you have more than one level of nesting.


## What is the new behavior?

In an ideal application all modules *should* be imported asynchronously and each options' provider should be passed down the hierarchy of modules in order to resolve every `inject` dependency.
So I added an optional `provideInjectionTokensFrom` to the interface `ConfigurableModuleAsyncOptions`, which takes the parent module's providers, and only injects in the child module those providers that are recursively required by the child module `inject` array (usually the parent options' provider).
Right now you can implement my new solution with the `extras` feature, but since this use case is basically universal I think it should be part of the core feature.
Now you can define a hierarchy of modules like this:

```typescript
import { ConfigurableModuleBuilder, Module } from '@nestjs/common';

interface AOptions {
  a: string;
}

export const {
  ConfigurableModuleClass: A_Module,
  MODULE_OPTIONS_TOKEN: A_TOKEN,
  ASYNC_OPTIONS_TYPE: A_ASYNC_OPTIONS_TYPE,
} = new ConfigurableModuleBuilder<AOptions>({
  moduleName: 'A',
}).build();

export class A extends A_Module {}

interface BOptions {
  b: string;
}

export const {
  ConfigurableModuleClass: B_Module,
  MODULE_OPTIONS_TOKEN: B_TOKEN,
  ASYNC_OPTIONS_TYPE: B_ASYNC_OPTIONS_TYPE,
} = new ConfigurableModuleBuilder<BOptions>({
  moduleName: 'B',
}).build();

export class B extends B_Module {
  static registerAsync(opt: typeof B_ASYNC_OPTIONS_TYPE) {
    const m = super.registerAsync(opt);
    m.imports ??= [];
    m.imports.push(
      A.registerAsync({
        useFactory: (opt: BOptions) => {
          return { a: opt.b };
        },
        inject: [B_TOKEN],
        // the B_TOKEN provider and his dependencies
        // (inject array, in this example the C_TOKEN provider)
        // will be present in the A module providers
        provideInjectionTokensFrom: m.providers,
      }),
    );
    return m;
  }
}

interface COptions {
  c: string;
}

export const {
  ConfigurableModuleClass: C_Module,
  MODULE_OPTIONS_TOKEN: C_TOKEN,
  ASYNC_OPTIONS_TYPE: C_ASYNC_OPTIONS_TYPE,
} = new ConfigurableModuleBuilder<COptions>({
  moduleName: 'C',
}).build();

export class C extends C_Module {
  static registerAsync(opt: typeof C_ASYNC_OPTIONS_TYPE) {
    const m = super.registerAsync(opt);
    m.imports ??= [];
    m.imports.push(
      B.registerAsync({
        useFactory: (opt: COptions) => {
          return { b: opt.c };
        },
        inject: [C_TOKEN],
        // the C_TOKEN provider and his dependencies (inject array)
        // will be present in the B module providers
        provideInjectionTokensFrom: m.providers,
      }),
    );
    return m;
  }
}
```

The only problem is when options' tokens have the same name string down the path. This can be avoided by using symbols as tokens.


## Does this PR introduce a breaking change?
- [ ] Yes
- [x] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->


## Other information


Patch:
diff --git a/packages/common/module-utils/configurable-module.builder.ts b/packages/common/module-utils/configurable-module.builder.ts
index 0e3f1318f..7b93f3dd2 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<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 [
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 56f7040e4..b1300ab61 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 000000000..26743580b
--- /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;
+}


Test patch:
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 3d3623271..3cca9a33f 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 000000000..fa9d29fce
--- /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]]);
+  });
+});