@nest-extended/prisma

The Prisma adapter: the same generic CRUD service and query language as the other adapters, backed by Prisma for PostgreSQL, MySQL and SQLite.

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

Key exports

ExportTypePurpose
NestService<T, E>classCRUD service — _find, _get, _create, _patch, _remove, and their event-firing find / get / create / patch / remove counterparts
applyFilters()functionApply parsed filters to Prisma query options
rawQuery()functionConvert a FeathersJS query to a Prisma where
GlobalExceptionFilterfilterMaps PrismaClientKnownRequestError (P2002, P2003, P2025, …) to HTTP

Usage

cats.service.ts
import { Injectable } from '@nestjs/common';
import { NestService } from '@nest-extended/prisma';
import { PrismaService } from '../prisma/prisma.service';
 
@Injectable()
export class CatsService extends NestService<any> {
  constructor(private readonly prisma: PrismaService) {
    super(prisma.cat);
  }
}

super() takes the model delegate (prisma.cat), while this.prisma stays on the instance — that is your route to the rest of the client, including raw SQL. See Raw SQL and aggregates.

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

Relations are eager-loaded with $include (Prisma's include). $iLike is fully case-insensitive on PostgreSQL — see Querying for per-database notes.

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.

Raw SQL and aggregates

find covers filtering, pagination and relations. Anything beyond it — GROUP BY, window functions, CTEs, database-specific SQL — goes through the PrismaService you already injected.

Typed aggregates, no SQL

Reach for these first: they stay type-safe and portable across PostgreSQL, MySQL and SQLite.

cats.service.ts
async statsByBreed() {
  return this.prisma.cat.groupBy({
    by: ['breed'],
    where: { deleted: false },
    _avg: { age: true },
    _count: { _all: true },
    orderBy: { _avg: { age: 'desc' } },
  });
}
 
async ageRange() {
  return this.prisma.cat.aggregate({
    where: { deleted: false },
    _min: { age: true },
    _max: { age: true },
  });
}

$queryRaw — rows back

Use the tagged template form. Interpolated values become bound parameters, so ${breed} is never spliced into the SQL string.

cats.service.ts
async topBreeds(minAge: number) {
  return this.prisma.$queryRaw<{ breed: string; avgAge: number; total: bigint }[]>`
    SELECT breed,
           AVG(age)::float AS "avgAge",
           COUNT(*)        AS total
    FROM "Cat"
    WHERE deleted IS NOT TRUE
      AND age >= ${minAge}
    GROUP BY breed
    ORDER BY "avgAge" DESC
  `;
}

$executeRaw — row count back

cats.service.ts
async retireOldCats(age: number) {
  return this.prisma.$executeRaw`
    UPDATE "Cat" SET retired = true WHERE age >= ${age} AND deleted IS NOT TRUE
  `;
}

Wrap several statements with this.prisma.$transaction([...]).

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. Raw SQL is not find or patch, so no hook runs. Call this.emit('retireOldCats', count) yourself if listeners should know — see Service Events.
  3. Shape the response. You get raw rows, not the { total, $limit, $skip, data } envelope — and COUNT(*) comes back as a BigInt, which JSON.stringify refuses. Cast it (Number(row.total)) before returning it from a controller.

Avoid $queryRawUnsafe / $executeRawUnsafe. They take a plain string and do not parameterise it, so any user-supplied value is an injection. If you need a dynamic identifier such as a column name, validate it against an allow-list first.

Identifier quoting is database-specific: "Cat" for PostgreSQL, backticks for MySQL. The examples above are PostgreSQL.

Next steps