#Root module not accessable in external NestJS module

40 messages · Page 1 of 1 (latest)

gray sparrow
#

Hi,

I followed the guide to implement the HealthModule with Terminus. This works fine.

Because I need this module in all my microservices, I used this code to create an external, custom module. The external module is recognized correctly by NestJS, but the TypeORMHealthIndicator misses the database connection.

I created the TypeORM connection by the forRoot() function before importing my custom module in my MicroserviceModule. How to pass the db connection the my custom module? I thought this is happening automatically, because it is available in the root context.

#

Root module not accessable in external NestJS module

lucid pagoda
#

How are you creating this library and using it across your microservices?

gray sparrow
#

health-check.module-definition.ts

import { ConfigurableModuleBuilder } from '@nestjs/common';
import { HealthCheckModuleOptions } from './health-check.module-interface';

export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<HealthCheckModuleOptions>().build();
#

health-check.module-interface.ts

import { ModuleRef } from "@nestjs/core";
import { MicroserviceHealthIndicatorOptions } from "@nestjs/terminus";

export interface HealthCheckModuleOptions {
    checkDatabase?: ModuleRef;
    checkRabbitMq?: MicroserviceHealthIndicatorOptions;
}
#

health-check.module.ts

import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { ConfigurableModuleClass } from './health-check.module-definition';
import { HealthCheckController } from './health-check.controller';
import { HealthCheckService } from './health-check.service';

@Module({
    imports: [TerminusModule],
    controllers: [HealthCheckController],
    providers: [HealthCheckService],
    exports: [HealthCheckService]
})
export class HealthCheckModule extends ConfigurableModuleClass {}
#

health-check.controller.ts

import { Controller, Get } from '@nestjs/common';
import { HealthCheck } from '@nestjs/terminus';
import { HealthCheckService } from './health-check.service';

@Controller()
export class HealthCheckController {
    
  constructor(private healthCheckService: HealthCheckService) {}

  /**
   * Endpoint that calls healthCheckService.
   * Per default, the response will not be cached.
   * 
   * @returns HealthCheckResult
   */
  @Get()
  @HealthCheck()
  check() {
    return this.healthCheckService.check();
  }
}
#

health-check.service.ts (don't mix it up with the original HealthCheckService from NestJs, which is used internally - it's called TerminusHealthCheckService here)

import { Inject, Injectable } from '@nestjs/common';
import { HealthCheckService as TerminusHealthCheckService, TypeOrmHealthIndicator, MicroserviceHealthIndicator, HealthIndicatorFunction } from '@nestjs/terminus';
import { MODULE_OPTIONS_TOKEN } from './health-check.module-definition';
import { HealthCheckModuleOptions } from './health-check.module-interface';

@Injectable()
export class HealthCheckService {
    constructor(
      @Inject(MODULE_OPTIONS_TOKEN) private healthCheckModuleOptions: HealthCheckModuleOptions,
      private terminusHealthCheckService: TerminusHealthCheckService,
      private microservice: MicroserviceHealthIndicator
    ) {}

  /**
   * Run health checks for the enabled use cases.
   * 
   * @returns HealthCheckResult
   */
  check() {
    let healthIndicatorFunctions: HealthIndicatorFunction[] = [];

    if(this.healthCheckModuleOptions) {
      
      // check rabbitmq
      if(this.healthCheckModuleOptions.checkRabbitMq) {
        healthIndicatorFunctions.push(
          async () => this.microservice.pingCheck('rabbitmq_connection', this.healthCheckModuleOptions.checkRabbitMq)
        );
      }

      // check database
      if(this.healthCheckModuleOptions.checkDatabase) {
        const typeOrmHealthIndicator = new TypeOrmHealthIndicator(this.healthCheckModuleOptions.checkDatabase);

        healthIndicatorFunctions.push(
          async () => typeOrmHealthIndicator.pingCheck('database')
        );
      }
    }

    return this.terminusHealthCheckService.check(healthIndicatorFunctions);
  }
}
#

Within the microservice main.module.ts I want to add my HealthCheckModule. In my first attemp I used a simple boolean in the health-check.module-interface.ts for the field checkDatabase, because I thought that the TypeOrm session is passed automatically to my HealthCheckModule when using forRoot() at the registration. But this doens't work, so my next attemp was to change the boolean to a ModuleReference, because the error message was about a missing module reference at the TypeOrmHealthIdicator. This didn't worked either, because I didn't know how to get it. I tried to assert the TypeOrmModule to an varaible, to pass teh reference, but this isn't a ModuleRef as I tought before.

lucid pagoda
#

What does your root module look like?

gray sparrow
#

main.module.ts (microservice application)

import { DynamicModule, Module } from '@nestjs/common';
import { ModuleRef, RouterModule, Routes } from '@nestjs/core';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AppService } from './app.service';
import { ConfigService, ConfigModule as DotEnvConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as Joi from 'joi';
import { HealthModule } from './health/health.module';
import { HealthCheckModule, HealthCheckModuleOptions } from '@myscope/health-check';

let typeOrmModule: DynamicModule;

const routes: Routes = [
  {
    path: '/configs',
    module: ConfigModule
  },
  {
    path: '/health',
    module: HealthCheckModule
  }
];

@Module({
  imports: [
    DotEnvConfigModule.forRoot({
      envFilePath: ['config/.env', 'config/.env.global'],
      isGlobal: true,
      validationSchema: Joi.object({ ... })
    }),
    RouterModule.register(routes),
    ClientsModule.registerAsync([ ... ]),
    (typeOrmModule = TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        type: 'mariadb',
        host: configService.get('MS_DB_HOST'),
        ...
      }),
      inject: [ConfigService]
    })),
    HealthCheckModule.registerAsync(
      {
        useFactory: () => {
          const healthCheckModuleOptions: HealthCheckModuleOptions = {
            checkDatabase: typeOrmModule.module, // TODO probably a ModuleRef required
            checkRabbitMq: {
              transport: Transport.RMQ,
              options: {
                queue: 'GID_MS_REVERSE_PROXY',
                queueOptions: {
                  durable: false
                },
              },
            }
          };
          return healthCheckModuleOptions;
        }
      }
    ),
  ],
  providers: [AppService]
})
export class AppModule {}

lucid pagoda
#

Yeah, that (typeOrmModule = ... no need to do that. I also don't see why you need to call new TypeOrmHealthIndicator() yourself, rather than just injecting it and letting Nest inject the ModuleRef to it

gray sparrow
# lucid pagoda Yeah, that `(typeOrmModule = ...` no need to do that. I also don't see why you n...

I did this before, but the error message said, that the first argument of the TypeOrmHealthIndicator was missing. It looked like this:

@Injectable()
export class HealthCheckService {
    constructor(
      @Inject(MODULE_OPTIONS_TOKEN) private healthCheckModuleOptions: HealthCheckModuleOptions,
      private terminusHealthCheckService: TerminusHealthCheckService,
      private microservice: MicroserviceHealthIndicator,
      private database: TypeOrmHealthIndicator
    ) {}
lucid pagoda
#

The specific error would be helpful

gray sparrow
#

I will recreate it. Give me a minute to downgrade my lib in the microservice and rebuild the container.

gray sparrow
# lucid pagoda The specific error would be helpful
2024-01-07 19:48:02 [Nest] 24  - 01/07/2024, 6:48:02 PM   ERROR [ExceptionHandler] Nest can't resolve dependencies of the TypeOrmHealthIndicator (?). Please make sure that the argument ModuleRef at index [0] is available in the TerminusModule context.
2024-01-07 19:48:02 
2024-01-07 19:48:02 Potential solutions:
2024-01-07 19:48:02 - If ModuleRef is a provider, is it part of the current TerminusModule?
2024-01-07 19:48:02 - If ModuleRef is exported from a separate @Module, is that module imported within TerminusModule?
2024-01-07 19:48:02   @Module({
2024-01-07 19:48:02     imports: [ /* the Module containing ModuleRef */ ]
2024-01-07 19:48:02   })
blissful hillBOT
#

Please run the command npx -y @nestjs/cli info and paste the output in a code block. This will help us determine if there is a version issue in your packages and which version of nest we are triaging.

lucid pagoda
#

The fact that ModuleRef can't be resolved makes me believe this is either in a linked library, a monorepo with multiple node_modules, or multiple versions of @nestjs/core are installed in some way

gray sparrow
#

because reflect metadata stores some information in different subdirectories in the node_modules?

lucid pagoda
#

Because of the way Realms work in JavaScript. If ModuleRef is imported from two separate locations /home/user/projectA/node_modules/@nestjs/core and /home/user/libraryA/node_modules/@nestjs/core for example, moduleRef (from library) instanceof ModuleRef (from project) will come back as false and can cause errors like this

gray sparrow
#

ah ok thanks for the explaination

#

i will check, if the versions are equal

lucid pagoda
#

Are there two install locations?

#

Because even if the versions are the exact same version, two install locations will cause this same error

#

Which is why I tried to ask earlier how this was being used. Is it a library being consumed in an application, are you in a monorepo, is it all a part of the same project with no extras (no monorepo or library linking)

gray sparrow
#

it's an library

lucid pagoda
#

Are you using link?

#

Or is the library published? And if it's published, are you properly using peerDependencies?

gray sparrow
#

the lib is published to the private gh registry

#

no I don't use peer dependencies yet

lucid pagoda
gray sparrow
#

but it seems to make sense to specify peerdepencies in the lib

gray sparrow
lucid pagoda
#

Well that would be problem 1 😆

gray sparrow
#

Problem 2 is that I can't pass the default ConfigService of NestJs to my external module. I get the following error message:
Nest can't resolve dependencies of the TypeOrmModuleOptions (?). Please make sure that the argument ConfigModule at index [0] is available in the TypeOrmCoreModule context.

#

My MainModule.ts of the microservice looks like:

ConfigModule.forRoot({
  envFilePath: ['config/.env', 'config/.env.global'],
  isGlobal: true,
  validationSchema: Joi.object({ ... })
})
TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => ({
    type: 'mariadb',
    host: configService.get('MS_DB_HOST'),
    port: configService.get('MS_DB_PORT'),
    username: configService.get('MS_DB_USER'),
    password: configService.get('MS_DB_PASSWORD'),
    database: configService.get('MS_DB_NAME'),
    entities: [Config, ConfigChild],
    synchronize: false
  }),
  inject: [ConfigModule]
}),
HealthCheckModule.registerAsync({
  imports: [ConfigModule],
  useFactory: (configService: ConfigService) => {
    const healthCheckModuleOptions: HealthCheckModuleOptions = {
      checkDatabase: true,
      checkRabbitMq: {
        transport: Transport.RMQ,
        options: {
          urls: [configService.get('MS_RABBITMQ_URL') as string],
          queue: '
MS_CONFIG',
          queueOptions: {
            durable: false
          },
        },
      }
    };
    return healthCheckModuleOptions;
  },
  inject: [ConfigService]
}),
#

Injecting the ConfigService to the TypeOrmModule works fine, but if I do the same with my own HealthCheckModule, I get the error from above.

#

Do I have to pepare the Injection in the HealthCheckModule?

It currently looks like this:

import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { ConfigurableModuleClass } from './health-check.module-definition';
import { HealthCheckController } from './health-check.controller';
import { HealthCheckService } from './health-check.service';

@Module({
    imports: [TerminusModule],
    controllers: [HealthCheckController],
    providers: [HealthCheckService],
    exports: [HealthCheckService]
})
export class HealthCheckModule extends ConfigurableModuleClass {}