iced
iced

Reputation: 318

Proper way to manually instantiate Nest.js providers

I think I might be misunderstanding Nest.js's IoC container, or maybe DI as a whole.

I have a class, JSONDatabase, that I want to instantiate myself based on some config value (can either be JSON or SQL).

My DatabaseService provider:

  constructor(common: CommonService, logger: LoggerService) {

    // eslint-disable-next-line prettier/prettier
    const databaseType: DatabaseType = common.serverConfig.dbType as DatabaseType;

    if (databaseType === DatabaseType.JSON) {
      this.loadDatabase<JSONDatabase>(new JSONDatabase());
    } else if (databaseType === DatabaseType.SQL) {
      this.loadDatabase<SQLDatabase>(new SQLDatabase());
    } else {
      logger.error('Unknown database type.');
    }
  }

My JSONDatabase class:

export class JSONDatabase implements IDatabase {
  dbType = DatabaseType.JSON;
  constructor(logger: LoggerService, io: IOService) {
    logger.log(`Doing something...`)
  }
}

However, the problem with this is that if I want my JSONDatabase to take advantage of injection, ie. it requires both IOService and LoggerService, I need to add the parameters from the DatabaseService constructor rather than inject them through Nest's IoC containers.

Expected 2 arguments, but got 0 [ts(2554)]
json.database.ts(7, 15): An argument for 'logger' was not provided.

Is this the proper way to do this? I feel like manually passing these references through is incorrect, and I should use Nest's custom providers, however, I don't really understand the Nest docs on this subject. I essentially want to be able to new JSONDatabase() without having to pass in references into the constructor and have the Nest.js IoC container inject the existing singletons already (runtime dependency injection?).

I might be completely off base with my thinking here, but I haven't used Nest all that much, so I'm mostly working off of instinct. Any help is appreciated.

Upvotes: 2

Views: 3854

Answers (1)

V. Lovato
V. Lovato

Reputation: 744

The issue you have right now is because you are instantiating JSONDatabase manually when you call new JSONDatabase() not leveraging the DI provided by NestJS. Since the constructor expects 2 arguments (LoggerService, and IOService) and you are providing none, it fails with the message

Expected 2 arguments, but got 0 [ts(2554)]

I think depending on your use case you can try a couple of different options

  1. If you fetch your configuration on startup and set the database once in the application lifetime you can use use a Custom provider with the useFactory syntax.
const providers = [
  {
    provide: DatabaseService,
    useFactory: (logger: LoggerService, io: IOService, config: YourConfigService): IDatabase => {
      if (config.databaseType === DatabaseType.JSON) {
          return new JSONDatabase(logger, io);
      } else if (databaseType === DatabaseType.SQL) {
          return new SQLDatabase(logger, io);
      } else {
        logger.error('Unknown database type.');
      }
    },
    inject: [LoggerService, IOService, YourConfigService]
  },
];

@Module({
  providers,
  exports: providers
})
export class YourModule {}

If you have LoggerService, IOService and YourConfigurationService annotated with @Injectable() NestJS will inject them in the useFactory context. There you can check the databaseType and manually instantiate the correct IDatabase implementation. The drawback with this approach is that you can't easily change the database in runtime. (This might work just fine for your use case)

  1. You can use strategy/factory pattern to get the correct implementation based on a type. Let say you have a method that saves to different databases based on an specific parameter.
@Injectable()
export class SomeService {
  constructor(private readonly databaseFactory: DatabaseFactory){}

  method(objectToSave: Object, type: DatabaseType) {
    databaseFactory.getService(type).save(objectToSave);
  }
}


@Injectable()
export class DatabaseFactory {

  constructor(private readonly moduleRef: ModuleRef) {}

  getService(type: DatabaseType): IDatabase {
    this.moduleRef.get(`${type}Database`);
  }
}

The idea of the code above is, based on the database type, get the correct singleton from NestJS scope. This way it's easy to add a new database if you want - just add a new type and it's implementation. (and your code can handle multiple databases at the same time!)

  1. I also believe you can simply pass the already injected LoggerService and IOService to the DatabasesService you create manually (You would need to add IOService as a dependency of DatabaseServce
@Injectable()
export class DatabaseService {
  constructor(common: CommonService, logger: LoggerService, ioService: IOService) {

    // eslint-disable-next-line prettier/prettier
    const databaseType: DatabaseType = common.serverConfig.dbType as DatabaseType;

    if (databaseType === DatabaseType.JSON) {
      this.loadDatabase<JSONDatabase>(new JSONDatabase(logger, ioService));
    } else if (databaseType === DatabaseType.SQL) {
      this.loadDatabase<SQLDatabase>(new SQLDatabase(logger, ioService));
    } else {
      logger.error('Unknown database type.');
    }
  }
}

Upvotes: 4

Related Questions