@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
| Export | Type | Purpose |
|---|---|---|
NestService<T, E> | class | CRUD service wrapping a TypeORM Repository — underscore methods plus their event-firing counterparts |
applyFilters() | function | Apply parsed filters to TypeORM find-options |
rawQuery() | function | Convert a FeathersJS query to a TypeORM where (Equal, MoreThan, ILike, …) |
GlobalExceptionFilter | filter | Maps QueryFailedError and driver codes (Postgres / MySQL / SQLite) to HTTP |
Usage
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.NestServiceholds its own private copy, so this is how you reach TypeORM directly — see Query builder and raw SQL.
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 TypeORMrelations.
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.
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
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
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:
- Filter soft-deleted rows.
deletedis nullable, sodeleted IS NOT TRUE(notdeleted = false) is the faithful equivalent of the{ deleted: { $ne: true } }filter the adapter merges in — see Soft Delete & Auditing. - Fire events. A query builder call is not
findorpatch, so no hook runs. Callthis.emit('statsByBreed', rows)yourself if listeners should know — see Service Events. - 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 tablecat. If you configure a different naming strategy, use the mapped name in raw SQL —this.catRepository.metadata.tableNamegives it to you at runtime.