nest icon indicating copy to clipboard operation
nest copied to clipboard

Dynamic modules with forwardRef in imports doesn't allow app to start

Open moeroach94 opened this issue 2 years ago • 3 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Current behavior

When the application graph includes a dynamic module and that dynamic module has a forwardRef in its list of imports, TypeError: metatype is not a constructor is thrown from the InstanceLoader during app startup.

Minimum reproduction code

https://github.com/moeroach94/dynamic-module-imports

Steps to reproduce

  1. npm install
  2. nest start
  3. see error

Expected behavior

The app should start successfully.

Package

  • [ ] I don't know. Or some 3rd-party package
  • [ ] @nestjs/common
  • [X] @nestjs/core
  • [ ] @nestjs/microservices
  • [ ] @nestjs/platform-express
  • [ ] @nestjs/platform-fastify
  • [ ] @nestjs/platform-socket.io
  • [ ] @nestjs/platform-ws
  • [ ] @nestjs/testing
  • [ ] @nestjs/websockets
  • [ ] Other (see below)

Other package

No response

NestJS version

9.0.11

Packages versions

{
    "@nestjs/common": "9.0.11",
    "@nestjs/core": "9.0.11",
    "@nestjs/platform-express": "9.0.11",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^6.6.3"
  }

Node.js version

14.20.0

In which operating systems have you tested?

  • [X] macOS
  • [ ] Windows
  • [ ] Linux

Other

This issue is similar to what raised an issue in my application code

moeroach94 avatar Oct 03 '22 21:10 moeroach94

for reference, this is the stacktrace:

TypeError: metatype is not a constructor
    at Injector.instantiateClass (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/injector.js:340:19)
    at callback (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/injector.js:53:45)
    at Injector.resolveConstructorParams (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/injector.js:132:24)
    at Injector.loadInstance (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/injector.js:57:13)
    at Injector.loadProvider (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/injector.js:84:9)
    at async Promise.all (index 0)
    at InstanceLoader.createInstancesOfProviders (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/instance-loader.js:47:9)
    at /tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/instance-loader.js:32:13
    at async Promise.all (index 3)
    at InstanceLoader.createInstances (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/instance-loader.js:31:9)

micalevisk avatar Oct 03 '22 21:10 micalevisk

Okay, so this works without the forwardRef, and your reproduction shows that using a forwardRef in the dynamic module, does indeed, cause this error. Can you explain your use case where you have a circular dependency on a dynamic module? I've not run into this use case before

jmcdo29 avatar Oct 03 '22 21:10 jmcdo29

So I've got a dynamic module that takes Sequelize entities and other modules as parameters and creates specialized service classes from them and wraps it all up with the necessary providers. I've got a case of SpecialServiceModuleA importing SpecialServiceModuleB which in turn needs to import SpecialServiceModuleA. Then this error Error: Nest cannot create the module instance. Often, this is because of a circular dependency between modules. Use forwardRef() to avoid it is printed. Then changing SpecialServiceModuleA import list to have forwardRef(() => SepcialServiceModuleB) results in the metatype error from above.

Here's some simplified snippets below. AccessGroupService and AccessRoleService are the service classes that rely on their counterpart service being injected into their constructors

export class LchemyQuerySequelizeModule {
  static forFeature({
    entities,
    imports = [],
    providers = [],
  }: ForFeatureModuleOptions): DynamicModule {
    const modelServicePairs: ModelServicePair[] = entities.map((entity) =>
        ({
            model: entity,
            service: LchemyQueryService(entity),
          })
    );

    const sequelizeModule = SequelizeModule.forFeature(
      modelServicePairs.map(({ model }) => model),
    );

    const serviceProviders: ClassProvider[] = modelServicePairs.map(
      ({ model, service }) => ({
        provide: `${model.name}QueryService`,
        useClass: service,
      }),
    );

    return {
      module: LchemyQuerySequelizeModule,
      imports: [sequelizeModule, ...imports],
      providers: [...serviceProviders, ...providers],
      exports: [sequelizeModule, ...serviceProviders, ...providers],
    };
  }
}
const accessGroupLchemyQueryModule = LchemyQuerySequelizeModule.forFeature({
  entities: [{ model: AccessGroup, service: AccessGroupService }],
  imports: [
    forwardRef(() => AccessRolesModule),
  ],
});
@Module({
  imports: [accessGroupLchemyQueryModule],
  controllers: [AccessGroupsController],
  exports: [accessGroupLchemyQueryModule],
})
export class AccessGroupsModule {}
export const accessRoleLchemyQueryModule =
  LchemyQuerySequelizeModule.forFeature({
    entities: [
      { model: AccessRole, service: AccessRoleService },
    ],
    imports: [
      AccessGroupsModule,
    ],
  });
@Module({
  imports: [accessRoleLchemyQueryModule],
  controllers: [AccessRolesController],
  providers: [AccessRoleSerializerService],
  exports: [accessRoleLchemyQueryModule],
})
export class AccessRolesModule {}

moeroach94 avatar Oct 04 '22 16:10 moeroach94

I have the same problem!

Celend avatar Nov 23 '22 05:11 Celend

I am also experiencing the same issue! Are there any recommendations to fixing this? Are there alternatives to forwardRef that can be used with a Dynamic Module?

jmoore240 avatar Nov 29 '22 15:11 jmoore240

I am also experiencing the same issue! Are there any recommendations to fixing this? Are there alternatives to forwardRef that can be used with a Dynamic Module?

Me too! This is an important issue. Hopefully we can get some answers.

rgprom avatar Nov 29 '22 17:11 rgprom

@moeroach94

I simplified the example with the 3 modules and the circular dependencies:

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

export class LchemyQuerySequelizeModule {
  static forFeature({ imports = [] }): DynamicModule {
    return {
      module: LchemyQuerySequelizeModule,
      imports: [...imports],
    };
  }
}

const accessGroupLchemyQueryModule = LchemyQuerySequelizeModule.forFeature({
  imports: [forwardRef(() => AccessRolesModule)],
});
@Module({
  imports: [accessGroupLchemyQueryModule],
  exports: [accessGroupLchemyQueryModule],
})
export class AccessGroupsModule {}

export const accessRoleLchemyQueryModule =
  LchemyQuerySequelizeModule.forFeature({
    imports: [AccessGroupsModule],
  });
@Module({
  imports: [accessRoleLchemyQueryModule],
  exports: [accessRoleLchemyQueryModule],
})
export class AccessRolesModule {}

The forwardRef should work but throw the error TypeError: metatype is not a constructor

But I found the module instances created with the Dynamic module can also be used with forwardRef if I move the files and resolve the circular dependency of class, we can create the module instance in the botton of the file and use the forwardRef

Solved:

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

export class LchemyQuerySequelizeModule {
  static forFeature({ imports = [] }): DynamicModule {
    return {
      module: LchemyQuerySequelizeModule,
      imports: [...imports],
    };
  }
}

@Module({
  imports: [forwardRef(() => accessGroupLchemyQueryModule)],
})
export class AccessGroupsModule {}

export const accessRoleLchemyQueryModule =
  LchemyQuerySequelizeModule.forFeature({
    imports: [AccessGroupsModule],
  });

@Module({
  imports: [accessRoleLchemyQueryModule],
})
export class AccessRolesModule {}

const accessGroupLchemyQueryModule = LchemyQuerySequelizeModule.forFeature({
  imports: [AccessRolesModule],
});

Nestjs resolve all dependencies correctly.

MiguelSavignano avatar Jan 10 '23 00:01 MiguelSavignano

Let's track this here https://github.com/nestjs/nest/pull/11031

kamilmysliwiec avatar Feb 03 '23 10:02 kamilmysliwiec