@nest-extended/typeorm

The TypeORM adapter: the same generic CRUD service and FeathersJS query language as the Prisma adapter, translating operators to TypeORM FindOperators.

$npm install @nest-extended/core @nest-extended/typeorm @nest-extended/decorators nestjs-cls

Key exports

ExportTypePurpose
NestService<T, E>classCRUD service wrapping a TypeORM Repository — underscore methods plus their event-firing counterparts
applyFilters()functionApply parsed filters to TypeORM find-options
rawQuery()functionConvert a FeathersJS query to a TypeORM where (Equal, MoreThan, ILike, …)
GlobalExceptionFilterfilterMaps QueryFailedError and driver codes (Postgres / MySQL / SQLite) to HTTP

Usage

cats.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { NestService } from '@nest-extended/typeorm';
import { Cat } from './entities/cat.entity';
 
@Injectable()
export class CatsService extends NestService<Cat> {
  constructor(
    @InjectRepository(Cat) private readonly catRepository: Repository<Cat>,
  ) {
    super(catRepository);
  }
}

Keep the injected repository on the instance (private readonly catRepository), as the generator does. NestService holds its own private copy, so this is how you reach TypeORM directly — see Query builder and raw SQL.

Querying
await catsService.find({
  name: { $iLike: 'kitty' },
  age: { $gt: 5 },
  $include: { owner: true },
  $sort: { createdAt: -1 },
  $limit: 10,
});

The query API is identical to @nest-extended/prisma — relations use $include, mapped to TypeORM relations.

Service events

Since 1.5.0 this service also exposes find / get / create / patch / remove — the same operations, but they dispatch to an attached events class. Name it as the last generic to have emit() type-checked:

export class CatsService extends NestService<Cat, CatsEvents> {}

See Service Events.

Query builder and raw SQL

find covers filtering, pagination and relations. Anything beyond it — GROUP BY, joins you want to shape yourself, window functions, database-specific SQL — goes through the repository you already injected.

Query builder

Composable and still parameterised, which makes it the right default.

cats.service.ts
async statsByBreed() {
  return this.catRepository
    .createQueryBuilder('cat')
    .select('cat.breed', 'breed')
    .addSelect('AVG(cat.age)', 'avgAge')
    .addSelect('COUNT(*)', 'total')
    .leftJoin('cat.owner', 'owner')
    .where('cat.deleted IS NOT TRUE')
    .andWhere('cat.age >= :minAge', { minAge: 1 })
    .groupBy('cat.breed')
    .orderBy('"avgAge"', 'DESC')
    .getRawMany<{ breed: string; avgAge: string; total: string }>();
}

getRawMany() returns plain rows; use getMany() when you want hydrated Cat entities instead.

Raw SQL

cats.service.ts
async topBreeds(limit: number) {
  return this.catRepository.query(
    `SELECT breed, COUNT(*) AS total
     FROM cat
     WHERE deleted IS NOT TRUE
     GROUP BY breed
     ORDER BY total DESC
     LIMIT $1`,
    [limit],
  );
}

Always pass values through the parameter array — never string-concatenate them. The placeholder syntax is driver-specific: $1, $2 on PostgreSQL, ? on MySQL and SQLite.

Transactions

cats.service.ts
async rehome(catId: string, ownerId: string) {
  return this.catRepository.manager.transaction(async (manager) => {
    await manager.update(Cat, { id: catId }, { owner: { id: ownerId } });
    await manager.increment(Owner, { id: ownerId }, 'catCount', 1);
  });
}

Three things the adapter stops doing

Raw access bypasses the wrapper entirely, so it is on you to:

  1. Filter soft-deleted rows. deleted is nullable, so deleted IS NOT TRUE (not deleted = false) is the faithful equivalent of the { deleted: { $ne: true } } filter the adapter merges in — see Soft Delete & Auditing.
  2. Fire events. A query builder call is not find or patch, so no hook runs. Call this.emit('statsByBreed', rows) yourself if listeners should know — see Service Events.
  3. Shape the response. You get raw rows, not the { total, $limit, $skip, data } envelope, and aggregate columns come back as strings on most drivers — cast them (Number(row.avgAge)) before returning them from a controller.

The generated entity is Cat, which TypeORM's default naming strategy maps to the table cat. If you configure a different naming strategy, use the mapped name in raw SQL — this.catRepository.metadata.tableName gives it to you at runtime.

Next steps