Nest Module `forRootAsync` is invoked before `forFeatureAsync`

119 views Asked by At

I'm trying to create a custom NestJS module. I'm providing 4 different methods forRoot, forRootAsync, forFeature, forFeatureAsync, although I'm using only two of it forRootAsync and forFeatureAsync.

I've imported module in AppModule and configured using forRootAsync, then also imported same module in a feature module UploadModule and configured using forFeatureAsync.

The problem is that undesirably forFeatureAsync is being invoked before forRootAsync. Mean my module is first initialized in UploadModule and then in AppModule, although purposefully it should do exactly opposite of it.

Please pay an attention to the source code of FileStorageService. This service should desirably first configure the baseDir - base directory of every upload. Then in feature, or even for the app module it should create every directory provided in the options.

Custom Module Source Code

file-storage.module.ts

import { DynamicModule, Module } from '@nestjs/common';
import { FileStorageService } from './file-storage.service';
import {
  ASYNC_OPTIONS_TYPE,
  MODULE_OPTIONS_TOKEN,
  OPTIONS_TYPE,
} from './file-storage.module-defination';

@Module({
  providers: [],
  exports: [],
})
export class FileStorageModule {
  static forRoot(options: typeof OPTIONS_TYPE): DynamicModule {
    // omit for better clarity
  }

  static forRootAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
    return {
      module: FileStorageModule,
      imports: options.imports,

      providers: [
        {
          provide: MODULE_OPTIONS_TOKEN,
          useClass: options.useClass,
          useFactory: options.useFactory,
          inject: options.inject,
        },

        FileStorageService,
      ],

      exports: [FileStorageService],
    };
  }

  static forFeature(options: typeof OPTIONS_TYPE): DynamicModule {
    // omit for better clarity
  }

  static forFeatureAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
    return {
      module: FileStorageModule,
      imports: options.imports,

      providers: [
        {
          provide: MODULE_OPTIONS_TOKEN,
          useClass: options.useClass,
          useFactory: options.useFactory,
          inject: options.inject,
        },

        FileStorageService,
      ],

      exports: [FileStorageService],
    };
  }
}

file-storage.service.ts

import { Inject, Injectable, Logger } from '@nestjs/common';
import {
  FileStorageModuleOptions,
  MODULE_OPTIONS_TOKEN,
} from './file-storage.module-defination';
import * as path from 'path';
import { mkdirSync, statSync } from 'fs';

@Injectable()
export class FileStorageService {
  private readonly logger = new Logger(FileStorageService.name);
  private static baseDir = '';
  directories = new Map<string, string>();

  constructor(
    @Inject(MODULE_OPTIONS_TOKEN) private options: FileStorageModuleOptions,
  ) {
    this.logger.log(options);

    if (options.baseDirectory) {
      this.logger.log(`Base Directory: ${options.baseDirectory}`);
      FileStorageService.baseDir = options.baseDirectory;
      syncDirectory(this.baseDirectory);
    }

    if (!options.directories?.length) return;

    for (const dir of options.directories) {
      try {
        let directory = this.baseDirectory;

        if (typeof dir.path == 'string') {
          directory = path.join(directory, dir.path);
        } else {
          for (const p of dir.path) {
            directory = path.join(directory, p);
          }
        }

        syncDirectory(directory);
        this.directories.set(dir.name, directory);
        this.logger.log(
          `Synced Directory: ${dir.name} | ${dir.path} | ${directory}`,
        );
      } catch {
        this.logger.warn(`Error Syncing Directory: ${dir.name} | ${dir.path}`);
      }
    }
  }
}


function syncDirectory(dir: string) {
  try {
    mkdirSync(dir, { recursive: true });
  } catch (err) {
    // omit for better clarity
  }
}

AppModule and UploadModule Source Code

app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SequelizeModule } from '@nestjs/sequelize';
import { AuthModule } from './auth/auth.module';
import { SharedModule } from './shared/shared.module';
import { TodoModule } from './todo/todo.module';
import { User } from './shared/user.model';
import { Todo } from './todo/todo.model';
import { UploadModule } from './upload/upload.module';
import { FileStorageModule } from './file-storage/file-storage.module';

@Module({
  imports: [
    ..., // N modules imported

    FileStorageModule.forRootAsync({
      imports: [ConfigModule],

      useFactory: (config: ConfigService) => {
        return {
          baseDirectory: config.get('STORAGE_DIR'),
        };
      },

      inject: [ConfigService],
    }),

    ..., // N modules imported

    UploadModule, // It is definitely last in Queue
  ],

  controllers: [],
  providers: [],
})
export class AppModule {}

upload.module.ts

import { BadRequestException, Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { UploadService } from './upload.service';
import { UploadController } from './upload.controller';
import { storageDirectories } from 'src/config';
import * as multer from 'multer';
import { v4 as uuidv4 } from 'uuid';
import * as mime from 'mime-types';
import { FileStorageModule } from 'src/file-storage/file-storage.module';
import { UploadDirectories } from './upload.interface';

@Module({
  imports: [
    ..., // N modules imported

    FileStorageModule.forFeatureAsync({
      useFactory: () => ({
        directories: [
          {
            name: UploadDirectories.ProfilePicture,
            path: UploadDirectories.ProfilePicture,
          },
        ],
      }),
    }),
  ],

  providers: [...],
  controllers: [...],
})
export class UploadModule {}

Logs of the application

Most of the logs are stripped, we're interested in the order of exection of-course.

[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [NestFactory] Starting Nest application...
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader] AppModule dependencies initialized +39ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader] SequelizeModule dependencies initialized +0ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader] PassportModule dependencies initialized +0ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader] MulterModule dependencies initialized +1ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [FileStorageService] Object:
{
  "directories": [
    {
      "name": "user/profile-picture",
      "path": "user/profile-picture"
    }
  ]
}

[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [FileStorageService] Synced Directory: user/profile-picture | user/profile-picture | user/profile-pict
ure
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader] ConfigHostModule dependencies initialized +1ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader] FileStorageModule dependencies initialized +0ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader] ConfigModule dependencies initialized +2ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader] ConfigModule dependencies initialized +0ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader] UploadModule dependencies initialized +1ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [FileStorageService] Object:
{
  "baseDirectory": "/var/cool-todo"
}
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [FileStorageService] Base Directory: /var/cool-todo
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader] FileStorageModule dependencies initialized +2ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader] JwtModule dependencies initialized +0ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [InstanceLoader]
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [RoutesResolver] UploadController {/api/v1/upload}: +0ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [RouterExplorer] Mapped {/api/v1/upload, POST} route +1ms
[Nest] 355  - 12/08/2023, 3:25:56 PM     LOG [NestApplication] Nest application successfully started +5ms

I've tried:

  • Arrange module imports in AppModule, placed UploadModule after FileStorageModule. Definitely I suspect that Async has something to do with it, so no way out.
  • Googling, not found anywhere mentioned.

I expect:

  • If issue is due to Async, explain the behavior, so I would never see myself cornered again.
  • Definitely the solution, I need to get out of it.
  • I cannot sacrifice async, as I need ConfigService dependency, I want to set the storage directory from environment.
0

There are 0 answers