Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to import a registerAsync in a dynamic Nestjs module?

been looking into the dynamic modules using the Advanced NestJS: How to build completely dynamic NestJS modules.

From what I've seen, most people use this guide to build a sync/async dynamic module.

But my question is, that if I use the registerAsync method, and my dynamic module needs to import HttpModule, and HttpModule's register-options are provided by my dynamic module.

How do you import a module within a dynamic module, where the options are provided by the dynamic module? Or is it the wrong way to handle this issue? if so, how would you structure it?


Here's the code. Which is practically a carbon copy of the tutorial. As you can see in the register method, it's simple - I just pass in the options. registerAsync however, I'm having trouble figuring out what to do.


Any help is much appreciated :)

import { Module, DynamicModule, Provider, HttpModule } from "@nestjs/common";
import { InvoicesHealth } from "./invoices/invoices.health";
import { InvoicesResolver, InvoicesService } from "./invoices";
import {
  CustomerInvoicesOptions,
  CustomerInvoicesAsyncOptions,
  CustomerInvoicesOptionsFactory,
} from "./interfaces";
import { CUSTOMER_INVOICES_OPTIONS } from "./constants";
import { createCustomerInvoicesProviders } from "./providers/customer-invoices.providers";

@Module({
  imports: [],
  controllers: [],
  providers: [InvoicesHealth, InvoicesResolver, InvoicesService],
  exports: [InvoicesHealth],
})
export class CustomerInvoicesModule {
  /**
   * Registers a configured customer-invoices Module for import into the current module
   */
  public static register(options: CustomerInvoicesOptions): DynamicModule {
    return {
      imports: [
        HttpModule.register({
          url: options.url,
          auth: {
            username: options.username,
            password: options.password,
          },
        }),
      ],
      module: CustomerInvoicesModule,
      providers: createCustomerInvoicesProviders(options),
    };
  }

  /**
   * Registers a configured customer-invoices Module for import into the current module
   * using dynamic options (factory, etc)
   */
  public static registerAsync(
    options: CustomerInvoicesAsyncOptions,
  ): DynamicModule {
    return {
      module: CustomerInvoicesModule,
      imports: options.imports || [],
      providers: [...this.createProviders(options)],
    };
  }

  private static createProviders(
    options: CustomerInvoicesAsyncOptions,
  ): Provider[] {
    if (options.useExisting || options.useFactory) {
      return [this.createOptionsProvider(options)];
    }

    return [
      this.createOptionsProvider(options),
      {
        provide: options.useClass,
        useClass: options.useClass,
      },
    ];
  }

  private static createOptionsProvider(
    options: CustomerInvoicesAsyncOptions,
  ): Provider {
    if (options.useFactory) {
      return {
        provide: CUSTOMER_INVOICES_OPTIONS,
        useFactory: options.useFactory,
        inject: options.inject || [],
      };
    }

    // For useExisting...
    return {
      provide: CUSTOMER_INVOICES_OPTIONS,
      useFactory: async (optionsFactory: CustomerInvoicesOptionsFactory) =>
        await optionsFactory.createFtNestCustomerInvoicesOptions(),
      inject: [options.useExisting || options.useClass],
    };
  }
}
like image 892
Jóhann Østerø Avatar asked Aug 11 '20 10:08

Jóhann Østerø


2 Answers

Okay, buckle in, cause there isn't an easy answer, but there are kind of ways around it.

First and foremost, there's no way to call one module's asynchronous registration method from another module's asynchronous registration method. At least not using the asynchronous configuration that was passed in, so instead I'll show you what can be done.

Option One. imports

Probably the easiest of the three options that still works with the async registration method. The imports array that is passed to the asynchronous config is completely available in the module, so you can end up doing something like

CustomerInvoicesModule.registerAsync({
  imports: [
    HttpModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: httpConfigFactory,
    }),
    ConfigModule
  ],
  inject: [ConfigService],
  useFacotry: customerInvoicesConfigFactory,
})

This will expose the HttpService in its full configuration. Only thing to be aware/careful of is making sure you're keeping track of the registration levels.

Option Two. asyncConfigInterface

This is the fun one. You can change your asynchronous config options to be something along the lines of

export interface CustomInvoiceModuleAsyncOptions {
  http: HttpModuleAsyncOptions;
  useClass?: RegularValueHere;
  useFacotry?: RegularValueHere;
  useValue?: RegularValueHere;
  inject: injectionArray;
  imports: importsArray;
}

And now in your registerAsync method you can do

static registerASync(options: CustomerInvoiceModuleAsyncOptions): DynamicModule {
  return {
    module: CustomerInvoicesModule,
    imports: [HttpModule.registerAsync(options.http)]
    providers: [...this.createProvider(options)],
    exports: [...this.createProvider(options)],
  }
}

Now, this means that the config is passed for the HttpModule inside of the options for your module, which kind of looks ugly, but it gets the options to the right module.

Option Three. register/forRoot

Just don't use the ConfgModule and use the register or forRoot methods directly. This way you can pass config values down quickly and easily. Very straightforward if you don't mind not using the ConfigModule.

like image 138
Jay McDoniel Avatar answered Nov 14 '22 18:11

Jay McDoniel


Expanding on Jay's answer, there may be an Option Four that works for you.

Option Four. extraProviders

Not all modules support this (but HttpModule seems to - so you're in luck).

public static registerAsync(options: CustomerInvoicesAsyncOptions,): DynamicModule {
    const providers = this.createProviders(options);
    return {
      module: CustomerInvoicesModule,
      imports: [
          HttpModule.registerAsync({
              imports: options.imports || [],
              // Factory is passed the injected CustomerInvoicesOptions as argument
              // which is based on the result of the extra providers
              useFactory: async (options: CustomerInvoicesOptions) => ({
                  url: options.url,
                  auth: {
                      username: options.username,
                      password: options.password,
                  },
              }),
              inject: [CUSTOMER_INVOICES_OPTIONS],
              extraProviders: providers,
          }),
          ...(options.imports || []),
        ],
        providers: [...providers],
    };
}

Jay may be able to clarify if this approach is correct, but it seems to work for me.

Fallback. @Global()/export

Instances where the module does not expose an extraProviders property in their AsyncOptions you may be able to circumvent this by making the module @Global() and exporting the CUSTOMER_INVOICES_OPTIONS provider.

e.g for the PassportModule (which unfortunately does not expose an extraProviders property)

@Global()
@Module({...})
export class CustomerInvoicesModule {
    ....
    public static registerAsync(options: CustomerInvoicesAsyncOptions,): DynamicModule {
        return {
            module: CustomerInvoicesModule,
            imports: [
                PassportModule.registerAsync({
                    useFactory: async (options: CustomerInvoicesOptions) => ({
                        defaultStrategy: options.oauthEnabled ? 'jwt' : 'no-auth',
                    }),
                    inject: [CUSTOMER_INVOICES_OPTIONS],
                }),
                ...(options.imports || []),
            ],
            providers: [...this.createProviders(options)],
            exports: [CUSTOMER_INVOICES_OPTIONS],
        };
    }
    ....

This @Global() approach also works, but isn't particularly nice.

Other things I have tried, which fail:

  • Circular import of the outer dynamic module into the inner, with the options provider key injected - the injected provider key cannot be resolved.
  • Same circular import, but with a forwardRef(() => MyModule) - this always fails with a metatype is not a constructor error.

Hope this insight helps, and any NestJS experts can correct issues with either approach.

like image 8
Oliver Sanders Avatar answered Nov 14 '22 19:11

Oliver Sanders