• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nestjs / nest / f21074a3-8fcf-4ec1-a93c-75675d264f01

22 Oct 2025 12:34AM UTC coverage: 88.726% (-0.2%) from 88.888%
f21074a3-8fcf-4ec1-a93c-75675d264f01

Pull #15815

circleci

mag123c
test(core): add nested transient isolation integration test
Pull Request #15815: fix(core): ensure nested transient provider isolation

2742 of 3477 branches covered (78.86%)

7 of 7 new or added lines in 1 file covered. (100.0%)

52 existing lines in 5 files now uncovered.

7280 of 8205 relevant lines covered (88.73%)

17.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.16
/packages/common/module-utils/configurable-module.builder.ts
1
/* eslint-disable @typescript-eslint/no-empty-object-type */
2
import { DynamicModule, Provider } from '../interfaces';
3
import { Logger } from '../services/logger.service';
1✔
4
import { randomStringGenerator } from '../utils/random-string-generator.util';
1✔
5
import {
1✔
6
  ASYNC_METHOD_SUFFIX,
7
  CONFIGURABLE_MODULE_ID,
8
  DEFAULT_FACTORY_CLASS_METHOD_KEY,
9
  DEFAULT_METHOD_KEY,
10
} from './constants';
11
import {
12
  ConfigurableModuleAsyncOptions,
13
  ConfigurableModuleCls,
14
  ConfigurableModuleHost,
15
  ConfigurableModuleOptionsFactory,
16
} from './interfaces';
17
import { generateOptionsInjectionToken, getInjectionProviders } from './utils';
1✔
18

19
/**
20
 * @publicApi
21
 */
22
export interface ConfigurableModuleBuilderOptions {
23
  /**
24
   * Specifies what injection token should be used for the module options provider.
25
   * By default, an auto-generated UUID will be used.
26
   */
27
  optionsInjectionToken?: string | symbol;
28
  /**
29
   * By default, an UUID will be used as a module options provider token.
30
   * Explicitly specifying the "moduleName" will instruct the "ConfigurableModuleBuilder"
31
   * to use a more descriptive provider token.
32
   *
33
   * For example, `moduleName: "Cache"` will auto-generate the provider token: "CACHE_MODULE_OPTIONS".
34
   */
35
  moduleName?: string;
36
  /**
37
   * Indicates whether module should always be "transient" - meaning,
38
   * every time you call the static method to construct a dynamic module,
39
   * regardless of what arguments you pass in, a new "unique" module will be created.
40
   *
41
   * @default false
42
   */
43
  alwaysTransient?: boolean;
44
}
45

46
/**
47
 * Factory that lets you create configurable modules and
48
 * provides a way to reduce the majority of dynamic module boilerplate.
49
 *
50
 * @publicApi
51
 */
52
export class ConfigurableModuleBuilder<
1✔
53
  ModuleOptions,
54
  StaticMethodKey extends string = typeof DEFAULT_METHOD_KEY,
55
  FactoryClassMethodKey extends
56
    string = typeof DEFAULT_FACTORY_CLASS_METHOD_KEY,
57
  ExtraModuleDefinitionOptions = {},
58
> {
59
  protected staticMethodKey: StaticMethodKey;
60
  protected factoryClassMethodKey: FactoryClassMethodKey;
61
  protected extras: ExtraModuleDefinitionOptions;
62
  protected transformModuleDefinition: (
63
    definition: DynamicModule,
64
    extraOptions: ExtraModuleDefinitionOptions,
65
  ) => DynamicModule;
66

67
  protected readonly logger = new Logger(ConfigurableModuleBuilder.name);
10✔
68

69
  constructor(
70
    protected readonly options: ConfigurableModuleBuilderOptions = {},
10✔
71
    parentBuilder?: ConfigurableModuleBuilder<ModuleOptions>,
72
  ) {
73
    if (parentBuilder) {
10✔
74
      this.staticMethodKey = parentBuilder.staticMethodKey as StaticMethodKey;
6✔
75
      this.factoryClassMethodKey =
6✔
76
        parentBuilder.factoryClassMethodKey as FactoryClassMethodKey;
77
      this.transformModuleDefinition =
6✔
78
        parentBuilder.transformModuleDefinition as (
79
          definition: DynamicModule,
80
          extraOptions: ExtraModuleDefinitionOptions,
81
        ) => DynamicModule;
82
      this.extras = parentBuilder.extras as ExtraModuleDefinitionOptions;
6✔
83
    }
84
  }
85

86
  /**
87
   * Registers the "extras" object (a set of extra options that can be used to modify the dynamic module definition).
88
   * Values you specify within the "extras" object will be used as default values (that can be overridden by module consumers).
89
   *
90
   * This method also applies the so-called "module definition transform function" that takes the auto-generated
91
   * dynamic module object ("DynamicModule") and the actual consumer "extras" object as input parameters.
92
   * The "extras" object consists of values explicitly specified by module consumers and default values.
93
   *
94
   * @example
95
   * ```typescript
96
   * .setExtras<{ isGlobal?: boolean }>({ isGlobal: false }, (definition, extras) =>
97
   *    ({ ...definition, global: extras.isGlobal })
98
   * )
99
   * ```
100
   */
101
  setExtras<ExtraModuleDefinitionOptions>(
102
    extras: ExtraModuleDefinitionOptions,
103
    transformDefinition: (
104
      definition: DynamicModule,
105
      extras: ExtraModuleDefinitionOptions,
UNCOV
106
    ) => DynamicModule = def => def,
×
107
  ) {
108
    const builder = new ConfigurableModuleBuilder<
2✔
109
      ModuleOptions,
110
      StaticMethodKey,
111
      FactoryClassMethodKey,
112
      ExtraModuleDefinitionOptions
113
    >(this.options, this as any);
114
    builder.extras = extras;
2✔
115
    builder.transformModuleDefinition = transformDefinition;
2✔
116
    return builder;
2✔
117
  }
118

119
  /**
120
   * Dynamic modules must expose public static methods that let you pass in
121
   * configuration parameters (control the module's behavior from the outside).
122
   * Some frequently used names that you may have seen in other modules are:
123
   * "forRoot", "forFeature", "register", "configure".
124
   *
125
   * This method "setClassMethodName" lets you specify the name of the
126
   * method that will be auto-generated.
127
   *
128
   * @param key name of the method
129
   */
130
  setClassMethodName<StaticMethodKey extends string>(key: StaticMethodKey) {
131
    const builder = new ConfigurableModuleBuilder<
2✔
132
      ModuleOptions,
133
      StaticMethodKey,
134
      FactoryClassMethodKey,
135
      ExtraModuleDefinitionOptions
136
    >(this.options, this as any);
137
    builder.staticMethodKey = key;
2✔
138
    return builder;
2✔
139
  }
140

141
  /**
142
   * Asynchronously configured modules (that rely on other modules, i.e. "ConfigModule")
143
   * let you pass the configuration factory class that will be registered and instantiated as a provider.
144
   * This provider then will be used to retrieve the module's configuration. To provide the configuration,
145
   * the corresponding factory method must be implemented.
146
   *
147
   * This method ("setFactoryMethodName") lets you control what method name will have to be
148
   * implemented by the config factory (default is "create").
149
   *
150
   * @param key name of the method
151
   */
152
  setFactoryMethodName<FactoryClassMethodKey extends string>(
153
    key: FactoryClassMethodKey,
154
  ) {
155
    const builder = new ConfigurableModuleBuilder<
2✔
156
      ModuleOptions,
157
      StaticMethodKey,
158
      FactoryClassMethodKey,
159
      ExtraModuleDefinitionOptions
160
    >(this.options, this as any);
161
    builder.factoryClassMethodKey = key;
2✔
162
    return builder;
2✔
163
  }
164

165
  /**
166
   * Returns an object consisting of multiple properties that lets you
167
   * easily construct dynamic configurable modules. See "ConfigurableModuleHost" interface for more details.
168
   */
169
  build(): ConfigurableModuleHost<
170
    ModuleOptions,
171
    StaticMethodKey,
172
    FactoryClassMethodKey,
173
    ExtraModuleDefinitionOptions
174
  > {
175
    this.staticMethodKey ??= DEFAULT_METHOD_KEY as StaticMethodKey;
4✔
176
    this.factoryClassMethodKey ??=
4✔
177
      DEFAULT_FACTORY_CLASS_METHOD_KEY as FactoryClassMethodKey;
178
    this.options.optionsInjectionToken ??= this.options.moduleName
4✔
179
      ? this.constructInjectionTokenString()
4✔
180
      : generateOptionsInjectionToken();
181
    this.transformModuleDefinition ??= definition => definition;
4✔
182

183
    return {
4✔
184
      ConfigurableModuleClass:
185
        this.createConfigurableModuleCls<ModuleOptions>(),
186
      MODULE_OPTIONS_TOKEN: this.options.optionsInjectionToken,
187
      ASYNC_OPTIONS_TYPE: this.createTypeProxy('ASYNC_OPTIONS_TYPE'),
188
      OPTIONS_TYPE: this.createTypeProxy('OPTIONS_TYPE'),
189
    };
190
  }
191

192
  private constructInjectionTokenString(): string {
193
    const moduleNameInSnakeCase = this.options
1✔
194
      .moduleName!.trim()
195
      .split(/(?=[A-Z])/)
196
      .join('_')
197
      .toUpperCase();
198
    return `${moduleNameInSnakeCase}_MODULE_OPTIONS`;
1✔
199
  }
200

201
  private createConfigurableModuleCls<ModuleOptions>(): ConfigurableModuleCls<
202
    ModuleOptions,
203
    StaticMethodKey,
204
    FactoryClassMethodKey
205
  > {
206
    // eslint-disable-next-line @typescript-eslint/no-this-alias
207
    const self = this;
4✔
208
    const asyncMethodKey = this.staticMethodKey + ASYNC_METHOD_SUFFIX;
4✔
209

210
    class InternalModuleClass {
211
      static [self.staticMethodKey](
212
        options: ModuleOptions & ExtraModuleDefinitionOptions,
213
      ): DynamicModule {
214
        const providers: Array<Provider> = [
1✔
215
          {
216
            provide: self.options.optionsInjectionToken!,
217
            useValue: this.omitExtras(options, self.extras),
218
          },
219
        ];
220
        if (self.options.alwaysTransient) {
1!
UNCOV
221
          providers.push({
×
222
            provide: CONFIGURABLE_MODULE_ID,
223
            useValue: randomStringGenerator(),
224
          });
225
        }
226
        return self.transformModuleDefinition(
1✔
227
          {
228
            module: this,
229
            providers,
230
          },
231
          {
232
            ...self.extras,
233
            ...options,
234
          },
235
        );
236
      }
237

238
      static [asyncMethodKey](
239
        options: ConfigurableModuleAsyncOptions<ModuleOptions> &
240
          ExtraModuleDefinitionOptions,
241
      ): DynamicModule {
242
        const providers = this.createAsyncProviders(options);
2✔
243
        if (self.options.alwaysTransient) {
2✔
244
          providers.push({
1✔
245
            provide: CONFIGURABLE_MODULE_ID,
246
            useValue: randomStringGenerator(),
247
          });
248
        }
249
        return self.transformModuleDefinition(
2✔
250
          {
251
            module: this,
252
            imports: options.imports || [],
4✔
253
            providers,
254
          },
255
          {
256
            ...self.extras,
257
            ...options,
258
          },
259
        );
260
      }
261

262
      private static omitExtras(
263
        input: ModuleOptions & ExtraModuleDefinitionOptions,
264
        extras: ExtraModuleDefinitionOptions | undefined,
265
      ): ModuleOptions {
266
        if (!extras) {
1!
UNCOV
267
          return input;
×
268
        }
269
        const moduleOptions = {};
1✔
270
        const extrasKeys = Object.keys(extras);
1✔
271

272
        Object.keys(input as object)
1✔
273
          .filter(key => !extrasKeys.includes(key))
1✔
274
          .forEach(key => {
UNCOV
275
            moduleOptions[key] = input[key];
×
276
          });
277
        return moduleOptions as ModuleOptions;
1✔
278
      }
279

280
      private static createAsyncProviders(
281
        options: ConfigurableModuleAsyncOptions<ModuleOptions>,
282
      ): Provider[] {
283
        if (options.useExisting || options.useFactory) {
2✔
284
          if (options.inject && options.provideInjectionTokensFrom) {
1!
285
            return [
1✔
286
              this.createAsyncOptionsProvider(options),
287
              ...getInjectionProviders(
288
                options.provideInjectionTokensFrom,
289
                options.inject,
290
              ),
291
            ];
292
          }
UNCOV
293
          return [this.createAsyncOptionsProvider(options)];
×
294
        }
295
        return [
1✔
296
          this.createAsyncOptionsProvider(options),
297
          {
298
            provide: options.useClass!,
299
            useClass: options.useClass!,
300
          },
301
        ];
302
      }
303

304
      private static createAsyncOptionsProvider(
305
        options: ConfigurableModuleAsyncOptions<ModuleOptions>,
306
      ): Provider {
307
        if (options.useFactory) {
2✔
308
          return {
1✔
309
            provide: self.options.optionsInjectionToken!,
310
            useFactory: options.useFactory,
311
            inject: options.inject || [],
1!
312
          };
313
        }
314
        return {
1✔
315
          provide: self.options.optionsInjectionToken!,
316
          useFactory: async (
317
            optionsFactory: ConfigurableModuleOptionsFactory<
318
              ModuleOptions,
319
              FactoryClassMethodKey
320
            >,
321
          ) =>
UNCOV
322
            await optionsFactory[
×
323
              self.factoryClassMethodKey as keyof typeof optionsFactory
324
            ](),
325
          inject: [options.useExisting || options.useClass!],
2✔
326
        };
327
      }
328
    }
329
    return InternalModuleClass as unknown as ConfigurableModuleCls<
4✔
330
      ModuleOptions,
331
      StaticMethodKey,
332
      FactoryClassMethodKey
333
    >;
334
  }
335

336
  private createTypeProxy(
337
    typeName: 'OPTIONS_TYPE' | 'ASYNC_OPTIONS_TYPE' | 'OptionsFactoryInterface',
338
  ) {
339
    const proxy = new Proxy(
8✔
340
      {},
341
      {
342
        get: () => {
343
          throw new Error(
2✔
344
            `"${typeName}" is not supposed to be used as a value.`,
345
          );
346
        },
347
      },
348
    );
349
    return proxy as any;
8✔
350
  }
351
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc