Search code examples
nestjstypeorm

NestJS: dynamically call various services for batch processing


I have some Service classes as follows:

//Cat Service:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, getManager } from 'typeorm';
import { CatRepo } from '../tables/catrepo.entity';
import { CatInterface } from './cat.interface';

@Injectable()
export class CatService {
  constructor(
    @InjectRepository(CatRepo)
    private catRepo: Repository<CatRepo>,
    
  ) {}

  async customFindAll(offset:number, limit: number): Promise<CatRepo[]> {
    const entityManager = getManager();
    const catRows = await entityManager.query(
      `
      SELECT * FROM CATREPO
      ${offset ? ` OFFSET ${offset} ROWS ` : ''}
      ${limit ? `FETCH NEXT ${limit} ROWS ONLY` : ''}
      `,
    );
    return catRows;
  }

  formResponse(cats: CatRepo[]): CatInterface[] {
   const catsResults: CatInterface[] = [];
   .
   //form cat response etc.
   .

   //then return 
   return catsResults;
  }
}

//Pet Service:
import { Injectable } from '@nestjs/common';
import { getManager } from 'typeorm';
import { PetInterface } from './pet.interface';


@Injectable()
export class PetService {

async customFindAll(offset:number, limit: number) {
    const entityManager = getManager();
    const petRows = await entityManager.query(
      `
        JOIN ON TABLES......
        ${offset ? ` OFFSET ${offset} ROWS ` : ''}
        ${limit ? `FETCH NEXT ${limit} ROWS ONLY` : ''}
        `,
    );

    //returns list of objects
    return petRows;
  }

  formResponse(pets): PetInteface[] {
   const petsResults: PetInteface[] = [];
   .
   . //form pet response etc.
   .

   //then return 
   return petsResults;
  }

 }

I am running a cron BatchService that uses these two services subsequently saving the data into respective batch files.

I'm calling CatService and PetService from the BatchService as follows:

/Start the Batch job for Cats.
if(resource === "Cat") {
//Call Cat Service
result = await this.catService.findAllWithOffest(startFrom, fetchRows);          
finalResult = this.catService.formResponse(result);
}
//Start the batch job for Pets.
if(resource === "Pet") {
//Call Pet Service
result = await this.petService.findAllWithOffest(startFrom, fetchRows);          
finalResult = this.petService.formResponse(result);
}

However, instead of the above I want to use these Services dynamically. In order to achieve the CatService and PetService now extends AbstractService...

export abstract class AbstractService {
    public batchForResource(startFrom, fetchRows) {}
}

//The new CatService is as follows:

export class CatService extends AbstractService{
  constructor(
    @InjectRepository(CatRepo)
    private catRepo: Repository<CatRepo>,
    
  ) {}
  .
  .
  .
  }

//the new PetService is:
export class PetService extends AbstractService{
  constructor(
  ) {super()}
.
.
.
}

//the BatchService...

public getService(context: string) : AbstractService {
  switch(context) {
      case 'Cat': return new CatService();
      case 'Pet': return new PetService();
      default: throw new Error(`No service found for: "${context}"`);
  }
}

However in the CatService I'm getting the a compilation error...(Expected 1 Argument but got 0). What should be the argument passed in the CatService.

Also, the larger question is if this can be achieved by using NestJS useValue/useFactory...If so how to do it?


Solution

  • You can probably use useFactory to dynamically retrieve your dependencies but there are some gotcha's.

    • You must make the lifecycle of your services transient, since NestJS dependencies are registered as singletons by default. If not, you would get the same first service injected each time, regardless of the context of subsequent calls.

    • Your context must come from another injected dependency - ExecutionContext, Request or something similarly dynamic, or something you register yourself.

    Alternative

    As an alternative, you can implement the "servicelocator/factory" pattern. You're already halfway there with your BatchService. Instead of your service creating instances of the CatService and PetService, you have it injected and just return the injected services depending on the context. Like so:

    @Injectable()
    export class BatchService {
        constructor(
            private readonly catService: CatService,
            private readonly petService: PetService
        )
    
        public getService(context: string) : AbstractService {
            switch(context) {
                case 'Cat': return this.catService;
                case 'Pet': return this.petService;
                default: throw new Error(`No service found for: "${context}"`);
            }
        }
    }
    

    The alternative is more flexible than using useFactory, since your context is not limited to what is available in the DI container. On the negative side, it does expose some (usually unwanted) infrastructure details to the calling code, but that's the tradeoff you'll have to make.