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 afterFileStorageModule
. Definitely I suspect thatAsync
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.