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

1

22

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.

Inexpugnable answered 14/12, 2018 at 9:32 Comment(0)
R
11

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
        ],
    }
];
Reede answered 4/11, 2019 at 18:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.