NestJS onModuleInit lifecycle hook behaviour for async providers

Viewed 2347

I am trying to initialize a ConfigurationService using the onModuleInit hook. Referring to the NestJS Lifecycle Hooks Page I expect the hook to be called on module creation. This is not the case, however. Basically, I have this:

ConfigurationService:

import { Injectable, OnModuleInit } from '@nestjs/common';

import { ApplicationConfiguration } from './interfaces/application-configuration.interface';

import { ServerConfiguration } from './interfaces/server-configuration.interface';

import * as path from 'path';

@Injectable()
export class ConfigurationService implements OnModuleInit {
    private _configuration: ApplicationConfiguration;

    public async getServerConfigurationAsync(): Promise<ServerConfiguration> {
        await this.ensureConfigurationIsLoadedAsync();

        return new Promise<ServerConfiguration>((resolve, reject) => {
            const serverConfiguration = this._configuration.server;

            if (!serverConfiguration) {
                reject(new Error('Missing server configuration.'));
            }

            return resolve(serverConfiguration);
        });
    }

    private async ensureConfigurationIsLoadedAsync(): Promise<void> {
        if (!this._configuration) {
            this._configuration = await this.fetchAppConfigurationAsync();
        }
    }

    private fetchAppConfigurationAsync(): Promise<ApplicationConfiguration> {
        const configDir = process.env.CONFIG_DIR;

        let configurationPath: string;

        if (configDir) {
            configurationPath = path.resolve(`configuration/${configDir}/config.json`);
        }
        else {
            configurationPath = path.resolve('configuration/config.json');
        }

        return new Promise<ApplicationConfiguration>((resolve, reject) => {
            try {
                const configuration = require(configurationPath);

                const mappedConfig: ApplicationConfiguration = {
                    server: configuration.server
                };

                resolve(mappedConfig);
            }
            catch (error) {
                console.log(`Can't fetch configuration at ${configurationPath}.`);

                reject(error);
            }
        });
    }

    // public async onModuleInit(): Promise<void> {
    //     console.log('ConfigurationService onModuleInit called.');

    //     await this.ensureConfigurationIsLoadedAsync();
    // }

    public  onModuleInit(): void {
        console.log('ConfigurationService onModuleInit called.');
    }
}

Specific Async Provider:

export const SERVER_CONFIGURATION_PROVIDER: Provider = {
    provide: CONFIGURATION_TOKENS.SERVER_CONFIGURATION,
    useFactory: async (configurationService: ConfigurationService): Promise<ServerConfiguration> => {
        console.log('SERVER_CONFIGURATION_PROVIDER useFactory called.');

        return await configurationService.getServerConfigurationAsync();
    },
    inject: [ConfigurationService]
}

Configuration Module:

@Module({
    providers: [
        ConfigurationService,
        SERVER_CONFIGURATION_PROVIDER
    ],
    exports: [
        SERVER_CONFIGURATION_PROVIDER
    ]
})
export class ConfigurationModule { 

    // public async onModuleInit(): Promise<void> {
    //     console.log('ConfigurationModule onModuleInit called.');
    // }
    public onModuleInit(): void {
        console.log('ConfigurationModule onModuleInit called.');
    }
}

And this is how I am trying to consume it:

@Controller('app')
export class AppController {
    private _serverConfig: ServerConfiguration;

    constructor(@Inject(CONFIGURATION_TOKENS.SERVER_CONFIGURATION) serverConfig: ServerConfiguration) {
        this._serverConfig = serverConfig;
    }

    @Get()
    public async getResult(): Promise<any> {
        return this._serverConfig;
    }
}

Now, the thing is I want to scrap the await this.ensureConfigurationIsLoadedAsync(); call in the getServerConfigurationAsync() method and initialize the Configuration through the onModuleInit hook. However, when running the application, the hook gets called long after the ConfigurationService gets consumed by the async provider. Am I missing something? Shouldn't the hook be invoked before the service gets consumed? The documentation states:

Called once the host module's dependencies have been resolved.

And

For each module, after module initialization:

await child controller & provider onModuleInit() methods

await module onModuleInit() method

However, I also found this in the source code:

nest/packages/core/nest-application.ts

public async init(): Promise<this> {
    this.applyOptions();
    await this.httpAdapter?.init();

    const useBodyParser =
      this.appOptions && this.appOptions.bodyParser !== false;
    useBodyParser && this.registerParserMiddleware();

    await this.registerModules();
    await this.registerRouter();
    await this.callInitHook();
    await this.registerRouterHooks();
    await this.callBootstrapHook();

    this.isInitialized = true;
    this.logger.log(MESSAGES.APPLICATION_READY);
    return this;
}

So I am not sure what to expect and if I get the lifecycle right. Here is my log when I start the Application:

Before app create
[Nest] 28132   - 10/05/2020, 4:12:38 PM   [NestFactory] Starting Nest application...
SERVER_CONFIGURATION_PROVIDER useFactory called.
[Nest] 28132   - 10/05/2020, 4:12:38 PM   [InstanceLoader] ConfigurationModule dependencies initialized +10ms
[Nest] 28132   - 10/05/2020, 4:12:38 PM   [InstanceLoader] AppModule dependencies initialized +1ms
After app create
before listen
[Nest] 28132   - 10/05/2020, 4:12:38 PM   [RoutesResolver] AppController {/api/app}: +5ms
[Nest] 28132   - 10/05/2020, 4:12:38 PM   [RouterExplorer] Mapped {/api/app, GET} route +4ms
ConfigurationService onModuleInit called.
ConfigurationModule onModuleInit called.
[Nest] 28132   - 10/05/2020, 4:12:38 PM   [NestApplication] Nest application successfully started +2ms
after listen
Listening at port 5000.
0 Answers
Related