Search code examples
dependency-injectionnestjscircular-dependency

nestjs : interface in DI results in circular references, whereas using class is fine


In order to be able to inject different repositories, I have created a generic abstract class. This class implements a simple generic interface.

All repo classes for the app entities therefore should extend this abstract class and implements at least the generic Repo interface..

Entities are TypeOrm entities, nothing fancy here.

I need now to inject the entity repository in the service that access the data of this entity.

If I direclty inject the entity Repository class, that extends the abstract repo class, this is fine. No error and all is working well.

But If I want to inject the repository via a token and using the repo interface in the constructor of the service instead of the repo class itself, I got circular reference issue.

Below is the code. The ProductsService needs the ProductsRepository to be injected. This ProductsRepository extends the abstract repo class that itself extends a generic interface

Please note that when no error, in the ProductsService constructor, the _repo constructor parameter is of type ProductsRepository. When circular error occurs, the type of the -repo is IProductsRepository and I'm injecting a token.

I really do not understand where the circular ref is:

the interface of the abstract class repo and the abstract class

export interface IBaseRepository<T> {
  findAll(options?: FindManyOptions<T>): Promise<T[]>;
} 

export abstract class BaseAbstractRepository<T> implements IBaseRepository<T> {
  private entity: Repository<T>;
  protected constructor(entity: Repository<T>) {
    this.entity = entity;
  }

  public async findAll(options?: FindManyOptions<T>): Promise<T[]> {
    return await this.entity.find(options);
  }
}

The ProductsRepository class that extends the abstract repo and implements the interface

export type IProductsRepository = IBaseRepository<ProductsEntity>;

@Injectable()
    export class ProductsRepository extends BaseAbstractRepository<ProductsEntity> implements IProductsRepository {
      constructor(@Inject(EDatabaseProviders.DATABASE_CONNECTION) readonly _dbConnection: DataSource) {
        super(_dbConnection.getRepository<ProductsEntity>(ProductsEntity));
      }
    }

The providers of the ProductsModule

export enum EProductsProviders {
  VALUE = "VALUE",
  REPOSITORY = "REPOSITORY",
}

export const productsProviders: Array<Provider> = [
  {
    provide: EProductsProviders.VALUE,
    useValue: 32,
  },

  {
    provide: EProductsProviders.REPOSITORY,
    useClass: ProductsRepository,
  },
  ProductsRepository,
  ProductsService,
];

The ProductsService that doesn't trigger the circular error (please note the type of the repo private parameter in constructor)

@Injectable()
export class ProductsService {
  constructor(private readonly _repo: ProductsRepository) {} //<--- no error when using the class

  async getAll(): Promise<TProductsDtoOutput[]> {
    const resu = await this._repo.findAll({
      relations: {
        transactions: true,
      },
    });

    return resu.map((r) => {
      const { id, ...rest } = r;
      return rest as TProductsDtoOutput;
    });
  }
}

the ProductService that triggers the error of circular dependency

        @Injectable()
        export class ProductsService {
          constructor(
    @Inject(EProductsProviders.REPOSITORY) 
private readonly _repo: IProductsRepository) {} // <--- error  A circular dependency has been detected inside ProductsModule
        
        async getAll(): Promise<TProductsDtoOutput[]> {
            const resu = await this._repo.findAll({
              relations: {
                transactions: true,
              },
            });
        
            return resu.map((r) => {
              const { id, ...rest } = r;
              return rest as TProductsDtoOutput;
            });
          }

finally the ProductsModule

@Module({
  imports: [DatabaseModule, AuthenticationModule],
  controllers: [ProductsController],
  providers: [...productsProviders],
})

export class ProductsModule {}

Solution

  • Looking at your providers, this is expected. The issue here is that your ProductsService imports a value from the products.providers file, but the products.providers file imports the products.service file which is where eProductsService is declared. To fix this, move your EProductsProviders enum to a products.constants file and have the products.service and products.providers import the enum from that file, thus eliminating the circular file import.