Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to create dynamic, async, and configurable modules (chapter) #223

Closed
kamilmysliwiec opened this issue Jan 4, 2019 · 17 comments
Closed

Comments

@kamilmysliwiec
Copy link
Member

I'm submitting a...


[ ] Regression 
[ ] Bug report
[ ] Feature request
[x] Documentation issue or request (new chapter/page)
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

Expected behavior

Ref nestjs/nest#1415

Minimal reproduction of the problem with instructions

What is the motivation / use case for changing the behavior?

Environment


For Tooling issues:
- Node version: XX  
- Platform:  

Others:

@iangregsondev
Copy link

@kamilmysliwiec Any update on this ? Or a simple example ?

Still confused how to inject ?

I need to provide Services to my module using useFactory and inject ?

@genert
Copy link

genert commented Apr 10, 2019

Bump

@Sieabah
Copy link

Sieabah commented May 28, 2019

Any updates on this @kamilmysliwiec?

@webberwang
Copy link

forRootAsync interfaces don't have imports as a parameter, which is confusing because it does take that parameter. I'm sure there are other undocumented parameters as well that could help clarify usage.

@BrunnerLivio
Copy link
Member

BrunnerLivio commented Jul 28, 2019

@webberwang As you can see here the interface inherits the imports option from ModuleMetadata.

@webberwang
Copy link

@BrunnerLivio ah! thanks for that

@johnbiundo
Copy link
Member

With the updated Custom Providers chapter, I think we can close this. I'll give it a couple of days to see if there are any takers for remaining questions.

@kamilmysliwiec
Copy link
Member Author

  • these articles

@AndyGura
Copy link

It is still not clear how to implement own forRootAsync method, just like it was asked here: nestjs/nest#1415 and there is link to this issue. I want to do something like this:

MyModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        someOption: configService.get<string>('SOME_ENV_VAR'),
      }),
    }),

I want it to work in the same way as

MyModule.forRoot({
      someOption: process.env.SOME_ENV_VAR,
    }),

And after a digging documentation for a while, I still can not find any suggestion how to do that. It would be cool to add code sample to official documentation. Thank you!

@AndyGura
Copy link

For all who struggle with it, here is working solution (you don't need any forRootAsync):

In my example we push firebase credentials from the outside of module.

Constant somewhere:

export const FIREBASE_OPTIONS_INJECT_TOKEN = 'RTDB_FIREBASE_OPTIONS';

FirebaseService:

constructor(
    @Inject(FIREBASE_OPTIONS_INJECT_TOKEN)
    private readonly options: FirebaseOptions,
  ) {
    this.app = firebase.initializeApp(options);
  }

Sub-module, which provides FirebaseService:

static forRoot(options: RtdbOptions): DynamicModule {
    const providers = [];
    // ....
    providers.push({
      provide: FIREBASE_OPTIONS_INJECT_TOKEN,
      ...options.firebaseOptionsFactory,
    });
    // ....
    return {
      module: RealtimeDatabaseModule,
      providers: providers,
    };
  }

Add in imports of super-module:

RealtimeDatabaseModule.forRoot({
      // ...
      firebaseOptionsFactory: {
        inject: [ConfigService],
        useFactory: (configService: ConfigService) => {
          return {
            apiKey: configService.get<string>('FIREBASE_API_KEY'),
            // ...
          };
        },
      },
      // ...
    }),

Hope it will help to someone

@ShiftyMcCool
Copy link

ShiftyMcCool commented May 27, 2021

@AndyGura I've been struggling with this for days and it feels like I'm so close now that you made this comment. I'm having trouble with this syntax:

   providers.push({
     provide: FIREBASE_OPTIONS_INJECT_TOKEN,
     ...options.firebaseOptionsFactory,
   });

normally, this would have a "provide" property and a "useClass" or "useFactory" as the next argument, but you're using a spread operator before the factory. When I use your syntax, I get the following error (I'm using this for a Salesforce connection, but the concept seems to be the same, I just need values from the @nestjs/config service):

Error: Nest cannot export a provider/module that is not a part of the currently processed module (SalesforceModule). 
Please verify whether the exported SALESFORCE_CONNECTION_OPTIONS is available in this particular context.

Possible Solutions:
- Is SALESFORCE_CONNECTION_OPTIONS part of the relevant providers/imports within SalesforceModule?

if I use "useFactory" instead of the spread I get this error:
TypeError: Cannot read property 'get' of undefined

The parts of my app that match yours are:
export const SALESFORCE_CONNECTION_OPTIONS_TOKEN = 'SALESFORCE_CONNECTION_OPTIONS'

SalesforceService

  constructor(
    @Inject(SALESFORCE_CONNECTION_OPTIONS_TOKEN)
    private options: SalesforceConnectionOptions,
  ) {
    this.connection = new SalesforceConnection(this.options)
  }

Sub-module which provides SalesForceService:

@Module({})
export class SalesforceModule {
  static forRoot(options: any): DynamicModule {
    const providers = []

    providers.push({
      provide: SALESFORCE_CONNECTION_OPTIONS_TOKEN,
      ...options.useFactory,
    })

    // In my case, I have to provide SalesforceService to the super-module, maybe this is where my problem is?
    providers.push(SalesforceService)

    return {
      module: SalesforceModule,
      providers: providers,
      exports: providers
    }
  }

super-module:

    SalesforceModule.forRoot({
      sfFactory: {
        inject: [ConfigService],
        useFactory: (service: ConfigService) => {
          return {
            username: service.get('SALES_FORCE_USERNAME'),
            password: service.get('SALES_FORCE_PASSWORD'),
            securityToken: service.get('SALES_FORCE_SECURITY_TOKEN'),
          }
        },
      },
    }),

I'm also not entirely clear on how the "inject" property in the above "inject: [ConfigService]" is used on the receiving side (sub-module).

Any help you can provide would be awesome, THANKS!

@AndyGura
Copy link

@ShiftyMcCool Looks like you should use spread operator on options.sfFactory, not the options.useFactory

Since you have

options = {
      sfFactory: {
        inject: [ConfigService],
        useFactory: (service: ConfigService) => {
          return {
            username: service.get('SALES_FORCE_USERNAME'),
            password: service.get('SALES_FORCE_PASSWORD'),
            securityToken: service.get('SALES_FORCE_SECURITY_TOKEN'),
          }
        },
      },
    };

Code

 providers.push({
      provide: SALESFORCE_CONNECTION_OPTIONS_TOKEN,
      ...options.sfFactory,
    })

Will create the valid structure in providers:

{
      provide: SALESFORCE_CONNECTION_OPTIONS_TOKEN,
      inject: [ConfigService],
      useFactory: (service: ConfigService) => {
        return {
          username: service.get('SALES_FORCE_USERNAME'),
          password: service.get('SALES_FORCE_PASSWORD'),
          securityToken: service.get('SALES_FORCE_SECURITY_TOKEN'),
        }
      },
}

Exactly this approach worked for me. Hope it will help you

@ShiftyMcCool
Copy link

@AndyGura Thank you so much for your quick reply! I was just able to try your suggestions and it worked! I had to make one change though. After I fixed the useFactory/sfFactory switch (nice catch), I was still getting the Please verify whether the exported SALESFORCE_CONNECTION_OPTIONS is available in this particular context error. To fix this, I had to move the const providers = [] outside the forRoot() method and reference it in my module:

const providers = []

@Module({
  providers
})
export class SalesforceModule {
  static forRoot(options: any): DynamicModule {
    providers.push({
      provide: SALESFORCE_CONNECTION_OPTIONS_TOKEN,
      ...options.sfFactory,
    })

    providers.push(SalesforceService)

    return {
      module: SalesforceModule,
      providers: providers,
      exports: providers,
    }
  }
}

Not sure if this could be the issue, but I'm developing my SalesForce module as an NPM package that is being used in a NestJS app. Doesn't seem to me that it would cause any issues, but I thought I would mention it for anybody else looking for a solution to this issue.

Thanks again @AndyGura, you saved my butt 😁

P.S.
This is such an easy solution, it should be in the official NestJS documentation (or maybe it is and I missed it 🤷 )

@linusbrolin
Copy link

@ShiftyMcCool I just tested your method of moving the const providers = [] out from the forRoot() method.
But that creates two instances of the service if I import with forRoot() in my AppModule and without forRoot() in my FeatureModule.

For example, consider this:

const providers: Array<any> = [];

@Module({
  providers: providers,
  exports: providers
})
export class AuthModule {
  static forRoot(environment: Environment): DynamicModule {
    providers.push(BasicAuthService);

    if (environment &&
        environment.jwt &&
        environment.jwt.secret) {
      providers.push(JwtService);
    }

    return {
      module: AuthModule,
      providers: providers,
      exports: providers
    };
  }
}

@Module({
  imports: [
    AuthModule
  ],
  providers: [
    UsersService
  ],
  exports: [
    UsersService
  ]
})
export class UsersModule {
}

@Module({
  imports: [
    AuthModule.forRoot(environment),
    UsersModule
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

This will create two separate instances of JwtService, which is not what I want.
I want JwtService to be a singleton, but NOT having to use @Global() in AuthModule.

Also, doing the following does not work at all, it will cause JwtService to be null/undefined in UsersService:

@Module({})
export class AuthModule {
  static forRoot(environment: Environment): DynamicModule {
    let providers: Array<any> = [
      BasicAuthService
    ];

    if (environment &&
        environment.jwt &&
        environment.jwt.secret) {
      providers.push(JwtService);
    }

    return {
      module: AuthModule,
      providers: providers,
      exports: providers
    };
  }
}

@Module({
  imports: [
    AuthModule
  ],
  providers: [
    UsersService
  ],
  exports: [
    UsersService
  ]
})
export class UsersModule {
}

@Module({
  imports: [
    AuthModule.forRoot(environment),
    UsersModule
  ],
  providers: [
    AppService
  ]
})
export class AppModule {
}

Does anyone have any idea/solution for how to acheive this kind of inheritance, without having to make AuthModule global, and without having to import AuthModule.forRoot() everywhere I need it?

@Falven
Copy link

Falven commented Jan 30, 2022

@kamilmysliwiec

I'm not sure why this thread is closed; I think in general the NestJs framework STILL lacks both documentation AND methods for clearly and concisely setting the DI lifetime/scope of modules. It is confusing for both new and seasoned developers to try to understand the relationship between module lifetimes and provider lifetimes and both can get mixed up/confused easily. Also, the forRoot pattern is still not well documented nor does it connect with the framework in any way as it is just a made up convention with a static function returning an object.

One such complexity that is hard to grasp is that when you are importing a module with imports: [MyModule.forRoot(...)] you are not really certain if this module will actually be a singleton accessible by other modules who later imports: [MyModule] given that you are not registering the MyModule type itself as the injection token, but rather the return value of the forRoot function which is an object of DynamicModule type.

To add to the complexity there are forRootAsync and forFeature patterns which are also not documented and a developer basically has to go clone and try to understand complicated framework wrapper modules, such as @nestjs/typeorm or @nestjs/mongoose, as examples for usage of these patterns.

It is also extremely hard to debug the IoC Container (what is injected, instantiated, where etc.) in NestJs. I still haven't found a way how, except through something like nestjs-spelunker which, overall, is not a great debugging experience.

I think at a minimum, if you choose to go this direction with module configuration, the framework should provide an interface with these methods that modules implement. However, I still fundamentally disagree with this direction as it shouldn't be this complicated to simply want to pass in some data to a module.

Edit:
The article you posted on Advanced DynamicModules helps, but it barely scratches the surface of what is truly a major underlying problem with the framework. It's also very badly SEO'd and took forever to find. It's upsetting that a framework that looks so optimal/friendly to use at a high level ends up having such a boilerplate and unfriendly underbelly when you get into advanced concepts.

@shadowmint
Copy link

shadowmint commented Jun 12, 2022

The documentation provided for this seems over complicated and largely irrelevant to the basic desired use case:

I want to dynamically generate the configuration of my module, using the ConfigService.

Bluntly: you can't. The module configuration cannot access the config service.

However, that's probably not what you actually want. If you look carefully at what https://github.com/nestjsplus/massive/blob/master/src/massive.module.ts does, you'll see that what it does is it creates a set of dynamic providers, that can be injected into services at runtime.

To do this, you register a factory function, that returns the provider value and can take injected values, like this:

{
  provide: MASSIVE_CONFIG_OPTIONS,
  useFactory: options.useFactory,
  inject: options.inject || [],
};

That's the only way you can do this. That's what all the implementations do.

The documentation and examples around this do a lot of back and forth with options and options factory providers and so on, but basically, you just do this:

import { Controller, forwardRef, Get, Inject } from "@nestjs/common";
import { DynamicModule, Injectable, Module, Provider } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { ModuleRef } from "@nestjs/core";

export interface TestService {
  foo(): string;
  opt: TestOptions;
}

export interface TestOptions {
  setting: string;
}

@Injectable()
export class TestAService implements TestService {
  constructor(@Inject(forwardRef(() => "TestConfig")) public opt: TestOptions) {}
  foo(): string {
    return `TestAService:${this.opt.setting}`;
  }
}

@Injectable()
export class TestBService implements TestService {
  constructor(@Inject(forwardRef(() => "TestConfig")) public opt: TestOptions) {}
  foo(): string {
    return `TestBService:${this.opt.setting}`;
  }
}

@Controller("tests")
export class TestsController {
  constructor(@Inject("TestService") private service: TestService) {}
  @Get("test")
  public test() {
    return this.service.foo();
  }
}

@Module({})
export class TestModule {
  public static registerAsync(): DynamicModule {
    return {
      module: TestModule,
      controllers: [TestsController],
      providers: [TestAService, TestBService, ...this.createConnectProviders()],
      imports: [ConfigModule],
      exports: [],
    };
  }

  private static createConnectProviders(): Provider[] {
    return [
      {
        provide: "TestConfig",
        useFactory: (config: ConfigService) => ({ setting: config.get<string>("USE_SERVICE_FLAG") }),
        inject: [ConfigService],
      },
      {
        provide: "TestService",
        useFactory: async (config: ConfigService, module: ModuleRef) => {
          const useA = config.get<string>("USE_SERVICE_FLAG") == "A";
          return useA ? await module.get(TestAService) : await module.get(TestBService);
        },
        inject: [ConfigService, ModuleRef],
      },
    ];
  }
}

Note: forwardRef is used here because providers can't depend on each other; you typically don't require that, but in the interests of providing a total end-to-end, this is it.

Now, you can extend this to take options with a custom factory function and a custom set of inject values, but, honestly, you almost never want to do that. Don't do it.

It's unnecessary over complication unless you're writing a library; if you're just configuring your own modules, just do it is simply, as shown.

@kamilmysliwiec
Copy link
Member Author

Creating dynamic, async, and configurable modules will become significantly simpler in v9 thanks to this feature nestjs/nest#9534. Docs will be updated very soon.

@nestjs nestjs locked and limited conversation to collaborators Jun 14, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests