jacobytes
jacobytes

Reputation: 3241

NestJS inject custom TypeOrm repository based on an Interface

I'm currently working through the database integration docs for NestJS using TypeOrm. In these docs there are examples that show how to inject a custom database repository using the app.module from NestJS. All of these examples inject classes using the actual type of the custom repository.

@Injectable()
export class AuthorService {
  constructor(private authorRepository: AuthorRepository) {}
}

This code is injected via the app.modules by providing a import like such:

@Module({
  imports: [TypeOrmModule.forFeature([AuthorRepository])],
  controller: [AuthorController],
  providers: [AuthorService],
})
export class AuthorModule {}

This works well if you are fine with programming against an implementation, but I prefer to use an interface in my classes. I've already found the solution to injecting classes via an interface with NestJS in a previous question, but when I try to inject my custom repository like that, it doesn't seem to instanciate correctly and becomes undefined.

(node:16658) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'save' of undefined

Because of this, I assume you can only inject customRepositories via the forFeature() call in the app.module, but that won't allow me to use interfaces for injection, as far as I know. Is there any other way I can inject a custom TypeOrm repository without having the replace all my interfaces for the implementation of my custom repository? Thanks in advance!

Edit

Here is my current code, I managed to get it to inject, but this still forces me to use the implementation instead of the interface each time I call the constructor. This is mainly an issue when testing due to mocking.

  @CommandHandler(FooCommand)
export class FooHandler
  implements ICommandHandler<FooCommand> {

  private fooRepository: IFooRepository; // Using Interface as a private property.
  private barEventBus: IEventBus;
  
  constructor(fooRepository: FooRepository,
     barEventBus: EventBus) { // Forced to use implementation in constructor for injection.
    this.fooRepository = fooRepository;
    this.barEventBus = barEventBus;
  }
@EntityRepository(FooEntity)
export class FooRepository extends Repository<FooEntity> implements IFooRepository {

  getFoo() {
    // Do stuff
  }
}

@Module({
  imports: [TypeOrmModule.forRoot(), TypeOrmModule.forFeature([FooRepository]],

  // Other module setup
})
export class AppModule {}

Upvotes: 2

Views: 20340

Answers (2)

Eugene
Eugene

Reputation: 366

Edit: This answer is crap, that abstract-class-as-interface hack used does not work out as the defined methods seem to be optional to implement despite being marked as abstract.

Well, kind of got it working. Based on this answer https://stackoverflow.com/a/74561702/913136 I used an abstract class as interface (you can actually implement it) for not being required to pass strings around as tokens. Only drawback is the misuse of the abstract class. Not sure yet if I like it.

Using an actual interface in the same way seems not to be possible unfortunately. Urgh.

@Module({
  imports: [
    TypeOrmModule.forRoot({
      ...dataSource.options,
      autoLoadEntities: true,
    }),
    TypeOrmModule.forFeature([Listing]),
  ],
  controllers: [ViewListingController],
  providers: [
    {
      provide: ListingRepository,
      useClass: TypeOrmListingRepository,
    },
  ],
})

makeshift interface:

import { Listing } from "./Listing";

export abstract class ListingRepository {
  abstract findMostRecent: () => Promise<Listing[]>;
}

implementation:

import { Listing, ListingRepository } from "../../Domain";
import { Injectable } from "@nestjs/common";
import { Repository, DataSource } from "typeorm";

@Injectable()
export class TypeOrmListingRepository
  extends Repository<Listing>
  implements ListingRepository
{
  constructor(private dataSource: DataSource) {
    super(Listing, dataSource.createEntityManager());
  }

  findMostRecent() {
    return this.find({});
  }
}
import { Controller, Get } from "@nestjs/common";
import { ListingRepository } from "../Domain";

@Controller("listings")
export class ViewListingController {
  constructor(private readonly listingRepo: ListingRepository) {}

  @Get("/most-recent")
  listMostRecent() {
    return this.listingRepo.findMostRecent();
  }
}

Upvotes: -1

Schutt
Schutt

Reputation: 1134

It should work with using the InjectRepository decorator where you specify the Repository but then you type is as your interface instead and when testing you just provide the IFooRepository!

Example code:

  constructor(@InjectRepository(FooRepository) fooRepository: IFooRepository,
 barEventBus: EventBus) { 

Upvotes: 5

Related Questions