7

Im trying to import a Module with registerAsync and configure it with a provider I have in my module, but it throws an error it cant find this provider. What am I missing?

My Code:

import { CacheModule, Module } from '@nestjs/common';

@Module({
  imports:     [
    CacheModule.registerAsync({
      useFactory: async (options) => ({
        ttl: options.ttl,
      }),
      inject: ['MY_OPTIONS'],
    }),
  ],
  providers:   [
    {
      provide:  'MY_OPTIONS',
      useValue: {
        ttl: 60,
      },
    },
  ],
})
export class AppModule {
}

Error:

Error: Nest can't resolve dependencies of the CACHE_MODULE_OPTIONS (?). Please make sure that the argument MY_OPTIONS at index [0] is available in the CacheModule context.

The example above is a simplification of my code. But the main issue stays the same: I do have a provider within the AppModule and I need it in the CacheModule.registerAsync() function.

If anyone wants to try figuring this out I made a really simple repository: https://github.com/MickL/nestjs-inject-existing-provider

Mick
  • 8,203
  • 10
  • 44
  • 66
  • The question was asked too simple I guess and might be closed. The problem actually comes in with registerAsync(). I updated my repo and there is also already an unanswered question on SO: https://stackoverflow.com/questions/63356440/how-to-import-a-registerasync-in-a-dynamic-nestjs-module – Mick Feb 08 '21 at 13:21
  • To import a Module with [registerAsync and configure](https://docs.nestjs.com/techniques/http-module#async-configuration) this will help you. – Raksha Saini Feb 12 '21 at 02:43

2 Answers2

3

This is kind of weird, but even if it is the same @Module you need to explicitly instruct dependency manager about imports and exports like so:

import { CacheModule, Module } from '@nestjs/common';

@Module({
  imports:     [
    CacheModule.registerAsync({
      imports: [AppModule], // <-- ADDED
      useFactory: async (options) => {
        console.log('MY_OPTIONS', options) // <-- LOGS FINE upon NestFactory.create(AppModule)
        return {
        ttl: options.ttl,
        }
        },
      inject: ['MY_OPTIONS'],
    }),
  ],
  providers:   [
    {
      provide:  'MY_OPTIONS',
      useValue: {
        ttl: 60,
      },
    },
  ],
  exports: ['MY_OPTIONS'] // <-- ADDED
})
export class AppModule {
}

EDITED

Based on your repo and specifically file src/my-lib.module.ts, the following seems to work (still not sure if it fully solves the underlying issue):

export class MyLibModule {
  // changed to async version by returning Promise
  static register(options: ModuleOptions): Promise<DynamicModule> {
    // keeps the ref to newly created module
    const OptionsModule = MyLibOptionsModule.register(options)
    // prints 'true', but still importing MyLibOptionsModule does not work
    console.log('who-am-i', OptionsModule.module === MyLibOptionsModule)
    return Promise.resolve({
      module: MyLibModule,
      imports: [
        // OptionsModule, // seems not needed
        HttpModule.registerAsync({
          useFactory: ((options: ModuleOptions) => {
            console.log("Works:", options);
            return {
              url: options.externalApiUrl,
              timeout: options.timeout
            };
          }),
          // importing via newly created ref does work
          imports: [OptionsModule],
          inject: [MODULE_OPTIONS]
        })
      ]
    });
  }
}
artur grzesiak
  • 20,230
  • 5
  • 46
  • 56
  • I would highly suggest against this pattern as now your `AppModule` and `CacheModule` are circularly related – Jay McDoniel Feb 06 '21 at 06:32
  • I wonder if this could work as AppModule isnt fully initialized when injecting – Mick Feb 06 '21 at 08:39
  • Not sure why but when I tried to do the same was getting a Circular Dependency error. However, after testing your code it works great, so probably I messed up something. – Alberto Martin Feb 06 '21 at 16:53
  • I dont know how this could work: AppModule imports CacheModule that imports AppModule that imports CacheModule that imports ... – Mick Feb 07 '21 at 12:27
  • @Mick Mostly likely this is XY problem :) Having circular dependencies is - in pretty all cases, most likely including this one - an anti-pattern. A truly circular dependency will never stop. A way to resolve a circular dependency is to split the loading procedure into steps forming a [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph). I am pretty sure nestjs does exactly that. But it would be better to have these steps declared in an explicit manner. I looked at your repo, but not sure what exactly you are trying to achieve. – artur grzesiak Feb 08 '21 at 10:37
  • A configurable dynamic module (like most nest modules are) that imports other dynamic modules and configures them by the config it received – Mick Feb 08 '21 at 10:51
  • Thanks this actually works for the .register() function. Even further we could use `HttpModule.register()` and dont need an additional module. Anyhow my question was asked too simple the problem actually come in with the registerAsync function. I updated my repo if you want to figure it out. There is also another unanswered question asking exactly the same: https://stackoverflow.com/questions/63356440/how-to-import-a-registerasync-in-a-dynamic-nestjs-module – Mick Feb 08 '21 at 13:20
2

I'd say the problem is "CacheModule.registerAsync" and "AppModule" work at different levels, so defining a provider on "AppModule" does not make it available for "CacheModule.registerAsync".

Assuming we move the "MY_OPTIONS" to another module: "CacheConfigModule", it could look like:

CacheConfigModule.ts

import { Module } from '@nestjs/common';

@Module({
  providers: [
    {
      provide: 'MY_OPTIONS',
      useValue: {
        ttl: 60
      }
    }
  ],
  exports: ['MY_OPTIONS']
})
export class CacheConfigModule {}

AppModule.ts

import { CacheModule, Module } from '@nestjs/common';
import { CacheConfigModule } from './CacheConfigModule';

@Module({
  imports: [
    CacheConfigModule,
    CacheModule.registerAsync({
      imports: [CacheConfigModule],
      useFactory: async (options) => ({
        ttl: options.ttl
      }),
      inject: ['MY_OPTIONS']
    })
  ]
})
export class AppModule {}
Alberto Martin
  • 556
  • 4
  • 8
  • Thank you! My example was just simplified, actually I am implementing a dynamic module and I do have the provider `MY_OPTIONS` within `AppModule`. How do I get it into `CacheConfigModule` so I can import it into the registerAsync function? – Mick Feb 05 '21 at 14:27
  • Hi @Mick, I suggested moving `MY_OPTIONS` to a different module so it's easier to inject it. In this example, since `MY_OPTIONS` is exported by `CacheConfigModule` you could still use it inside `AppModule` – Alberto Martin Feb 06 '21 at 16:43
  • I made an example repo to try this out. My idea was to add another module as you suggested and put in the options there with another register. Anyhow they are not available in the useFactory and I dont know why. If you want to take a look I would be super happy it is a really easy example: https://github.com/MickL/nestjs-inject-existing-provider – Mick Feb 07 '21 at 12:08