Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle NestJS Dependency Injection when extending a class for a service?

I am trying to provide a different service based on a value from my ConfigService.

The problem I am running into is that the mongoose model that gets injected does not return any values when executing query methods such as findOne() (result is null) or countDocuments() (result is 0).

My service classes are defined as follows:

    export class BaseService {
      constructor(@InjectModel('Cat') public readonly catModel: Model<Cat>) {}

      createService(option: string) {
        if (option === 'OTHER') {
          return new OtherService(this.catModel);
        } else if (option === 'ANOTHER') {
          return new AnotherService(this.catModel);
        } else {
          return new BaseService(this.catModel);
        }
      }

      async findOne(id: string): Promise<Cat> {
        return await this.catModel.findOne({_id: id});
      }

      async count(): Promise<number> {
        return await this.catModel.countDocuments();
      }

      testClass() {
        console.log('BASE SERVICE CLASS USED');
      }
    }

    @Injectable()
    export class OtherService extends BaseService {
      constructor(@InjectModel('Cat') public readonly catModel: Model<Cat>) {
        super(catModel);
      }

       testClass() {
        console.log('OTHER SERVICE CLASS USED');
      }
    }

    @Injectable()
    export class AnotherService extends BaseService {
      constructor(@InjectModel('Cat') public readonly catModel: Model<Cat>) {
        super(catModel);
      }
      testClass() {
        console.log('ANOTHER SERVICE CLASS USED');
      }
    }

This allows me to get the correct service from my provider (testClass() prints the expected string). My provider looks like this:

    export const catProviders = [
      {
        provide: 'CatModelToken',
        useFactory: (connection: Connection) => connection.model('CAT', CatSchema),
        inject: ['DbConnectionToken'],
      },
      {
        provide: 'BaseService',
        useFactory: (ConfigService: ConfigService, connection: Connection) => {
          const options = ConfigService.get('SERVICE_TYPE');
          let model = connection.model('CAT', CatSchema);
          return new BaseService(model).createService(options);
      },
      inject: [ConfigService, 'CatModelToken', 'DbConnectionToken'],
      }
    ];

So my question is in two parts:

  • Is there a better way to handle the creation of the correct class and to avoid having to create a BaseService instance for the sole purpose of calling createService()?
  • What is the proper way to inject the mongoose model into the newly created service?

I also cannot use the useClass example from the documentation, since I need to be able to inject the ConfigService.

like image 534
cameronliam Avatar asked Dec 14 '18 09:12

cameronliam


People also ask

How does dependency injection work in NestJS?

Dependency injection is an inversion of control (IoC) technique wherein you delegate instantiation of dependencies to the IoC container (in our case, the NestJS runtime system), instead of doing it in your own code imperatively.

Is NestJS a service singleton?

NestJS Modules are Singletons by default. Their Providers are also singletons when they are provided from within the same module.

What does injectable do in NestJS?

With Nest. js' injector system, you can manage your objects without thinking about the instantiation of them, because that is already managed by the injector, which is there to resolve the dependencies of every dependent object.

What is service in NestJS?

Service. In enterprise applications, we follow the SOLID principle, where S stands for Single Responsibility . The controllers are responsible for accepting HTTP requests from the client and providing a response. For providing the response, you may need to connect to some external source for data.


1 Answers

You can solve this by using a factory approach, try this:

Interface to determine the "shape" of your services:

export interface IDatabaseService {
    findOne(id: string): Promise<Cat>;
    count(): Promise<number>;
    testClass(): void;
}

The BaseService must implement that interface:

export class BaseService implements IDatabaseService {

    constructor(@InjectModel('Cat') public readonly catModel: Model<Cat>) {}

    async findOne(id: string): Promise<Cat> {
        return await this.catModel.findOne({_id: id});
    }

    async count(): Promise<number> {
        return await this.catModel.countDocuments();
    }

    testClass() {
        console.log('BASE SERVICE CLASS USED');
    }
}

The dynamic services are not injected so they do not use the @Injectable() decorator:

export class OtherService extends BaseService {

    constructor(@InjectModel('Cat') public readonly catModel: Model<Cat>) {
        super(catModel);
    }

    testClass() {
        console.log('OTHER SERVICE CLASS USED');
    }
}

export class AnotherService extends BaseService {

    constructor(@InjectModel('Cat') public readonly catModel: Model<Cat>) {
        super(catModel);
    }

    testClass() {
        console.log('ANOTHER SERVICE CLASS USED');
    }
}

The factory class is the thing that gets injected:

@Injectable()
export class DatabaseServiceFactory {

    constructor(@InjectModel('Cat') private readonly catModel: Model<Cat>) {}

    createService(name: string) : IDatabaseService {
        switch(name) {
            case 'other': return new OtherService(this.catModel);
            case 'another': return new AnotherService(this.catModel);
            default: throw new Error(`No service has been implemented for the name "${name}"`);
        }
    }
}
export const catProviders = [
    {
        provide: 'CatModelToken',
        useFactory: (connection: Connection) => connection.model('CAT', CatSchema),
        inject: ['DbConnectionToken'],
    },
    {
        provide: 'BaseService',
        useFactory: (ConfigService: ConfigService, connection: Connection, dbFactory: DatabaseServiceFactory) => {

            const options = ConfigService.get('SERVICE_TYPE');
            let model = connection.model('CAT', CatSchema);
            
            //return new BaseService(model).createService(options);
            return dbFactory.createService(options);
        },
        inject: [
            ConfigService,
            'CatModelToken',
            'DbConnectionToken',
            DatabaseServiceFactory
        ],
    }
];
like image 116
nerdy beast Avatar answered Sep 17 '22 12:09

nerdy beast