Prisma Adapter
Use nestjs-rest-query with Prisma - the generated client passed directly, a hand-written manifest, and the operators two dialects cannot serve.
The Prisma adapter compiles the query plan into findMany/count arguments. A
many relation uses some/none, a one relation uses is/isNot, and the
portable text profile queries folded columns so mode: 'insensitive' is never
emitted.
Install
pnpm add @prisma/client
pnpm add -D prismaThe supported range is ^6.19.0 || ^7.0.0. The matrix target is Prisma 7.8.0
(CLI and client on the same version), with the official driver adapter per
dialect.
Three artefacts
There is no tool that derives them. The generator that would read
schema.prisma is a declared gap for 3.1.0, so all three are written by
hand and kept in step with the schema by review.
1. The logical schema of every reachable model
import { defineQuerySchema } from 'nestjs-rest-query';
import type { QuerySchema, SchemaRegistry } from 'nestjs-rest-query';
const postSchema: QuerySchema = defineQuerySchema({
model: 'post',
primaryKey: ['id'],
fields: [
{
path: 'id',
kind: 'uuid',
nullable: false,
primaryKey: true,
// Without this the endpoint is refused at startup with
// CAPABILITY_UNAVAILABLE: the pagination tie-break is always over the
// primary key, and uuid has no portable total order.
portableOrderField: 'idOrder',
},
{
path: 'idOrder',
kind: 'string',
nullable: false,
primaryKey: false,
internal: true,
},
{
path: 'title',
kind: 'string',
nullable: false,
primaryKey: false,
foldedField: 'titleFolded',
},
{
path: 'titleFolded',
kind: 'string',
nullable: false,
primaryKey: false,
internal: true,
},
{ path: 'content', kind: 'string', nullable: true, primaryKey: false },
{ path: 'userId', kind: 'integer', nullable: false, primaryKey: false },
{ path: 'createdAt', kind: 'datetime', nullable: false, primaryKey: false },
],
relations: [
{ path: 'user', target: 'user', cardinality: 'one', nullable: false },
],
});
export const APP_SCHEMAS: SchemaRegistry = new Map([
['company', companySchema],
['user', userSchema],
['post', postSchema],
]);Field path is the client property name, not the column name — it is what
goes into the where/select/orderBy the adapter builds. With
@map("name_folded") in schema.prisma, the HTTP API stays camelCase while
the database keeps the certified profile's snake_case.
2. The manifest
import { createPrismaManifest } from 'nestjs-rest-query/prisma';
import type { PrismaManifest } from 'nestjs-rest-query/prisma';
import { APP_SCHEMAS } from './schemas';
export const APP_MANIFEST: PrismaManifest = createPrismaManifest({
// Decides the dialect, and with it the pattern escape.
provider: 'postgresql',
registry: APP_SCHEMAS,
models: {
company: { delegate: 'company' }, // maps the model to prisma.company
user: { delegate: 'user' },
post: { delegate: 'post' },
},
});createPrismaManifest validates the manifest against itself at startup: a model
with no registry entry, or no delegate, fails with
SOURCE_CONFIGURATION_INVALID. The delegate named by the manifest is validated
again at source construction — a check a type could never do here, because the
delegate name comes from data, not from code.
3. The source, per request
import { prismaSource } from 'nestjs-rest-query/prisma';
async findAll(
query: DynamicQueryDto,
rules: CompiledQueryRules,
): Promise<NormalizedQueryResult<object>> {
return this.queryBuilderService.execute(
prismaSource({
client: this.prisma,
model: 'user',
manifest: APP_MANIFEST,
}),
query,
rules,
);
}prismaSource({ client }) takes the generated PrismaClient directly — no
cast, and no facade of your own:
@Injectable()
export class PrismaService extends PrismaClient {}This is worth stating because it was not true until 3.0.0: client used to be
typed Readonly<Record<string, PrismaDelegate>>, and no real PrismaClient
satisfied it — a class gets no implicit string index signature in TypeScript, so
every consumer needed an assertion.
prismaSource<TRow> is generic in the row type. Nothing in the options carries
it — the delegate comes from the manifest, not from a typed client — so this is
the one factory where you name the type argument yourself:
prismaSource<UserDto>({ client, model, manifest }) lets the method be
annotated Promise<NormalizedQueryResult<UserDto>> with no cast. Omit it and
you get object, as above.
Refused operators, by provider
This is the one declared divergence in v3. Under the v3 grammar %, _ and
\ are literal characters: filter[name][like]=100% searches for the text
"100%". TypeORM and Drizzle emit LIKE ... ESCAPE, so they honour it directly.
Prisma compiles contains to LIKE ('%' || ? || '%') with no ESCAPE
clause, and the typed delegate exposes no way to supply one. All that is left
is the dialect's default escape character, which splits the providers in two:
provider | like, notLike, ilike, notIlike, search | Why |
|---|---|---|
postgresql, mysql | Work, and %/_ are literal — identical to TypeORM and Drizzle | \ is the dialect's default LIKE escape, so escaping the value is enough |
sqlite, sqlserver | Refused: 400 CAPABILITY_UNAVAILABLE | no default escape character, so literalness cannot be honoured — refusing beats returning the wrong rows |
The refusal is per request, not at startup, so the rest of the grammar stays
usable on those dialects. Declaring the wrong provider in the manifest is
therefore the difference between a correct endpoint and a silently wrong one.
Measured on Prisma 7.8.0 + PostgreSQL through
04-app-with-prisma:
GET /posts?filter[title][like]=100%25 returns only "Desconto de 100% na conta
de luz", and filter[title][like]=a_b returns only "Circuito a_b revisado" —
the metacharacters are literal.
If you are migrating a Prisma consumer on SQL Server, this is the breaking part: five operators stop being available. Plan for it.
What Prisma does not check
PrismaAdapter.describe() returns the manifest's schema verbatim. So nothing
compares your logical schema against schema.prisma or against the database:
a mistyped field name surfaces as a Prisma error on the first request that
touches it, where the TypeORM path would have failed at boot. The "missing
metadata fails closed" guarantee does not extend to Prisma.
customize gets the query arguments
The native context is one query at a time, tagged with which one it is:
await this.queryBuilderService.execute(
prismaSource({ client: this.prisma, model: 'user', manifest: APP_MANIFEST }),
query,
rules,
{
customize: (native) => {
// native: { kind: 'data' | 'count', args }
native.args.where = { AND: [native.args.where ?? {}, { tenantId }] };
},
customizeScope: 'both',
}
);Being called once per query — instead of receiving both together — is what makes
'both', the safe default, actually reach data and count.
Prisma 7, if you are coming from 6
Six things change and none is optional:
urlleftdatasource. Keeping the v2 schema fails generation withP1012: The datasource property 'url' is no longer supported in schema files. Connection URLs for CLI/Migrate move toprisma.config.ts; the client gets its connection from a driver adapter.- The generator changed.
provider = "prisma-client-js"becomesprisma-client, withoutputandmoduleFormatrequired, and the client stops existing at@prisma/client:import { PrismaClient } from '@prisma/client'becomesTS2305: Module '"@prisma/client"' has no exported member 'PrismaClient'. new PrismaClient()needs a driver adapter. Add the adapter for your dialect on the client's major (@prisma/adapter-pg) plus the driver (pg), and pass{ adapter: new PrismaPg({ connectionString }) }to the constructor.- The generated client is real TypeScript, not
.d.ts. Generating outside the buildrootDirbreaksnest buildwithTS6059. Generate insidesrc(output = "../src/generated/prisma"). - The Prisma 7 runtime loads through dynamic
import(), so any Jest runner that touches it needsNODE_OPTIONS=--experimental-vm-modules. The ESM ts-jest recipe (useESM: true+extensionsToTreatAsEsm) fails against the generated client withReferenceError: exports is not defined; what works is ts-jest in CommonJS plus the flag. .envis no longer read by Prisma (a consequence of 1): loaddotenvand validateDATABASE_URLyourself.
Next steps
- Usage Guide for every parameter, operator and error code.
- Adapters overview for the full divergence table.
- The full migration guide
for the
2.x→3.xpath.
Drizzle Adapter
Use nestjs-rest-query with Drizzle ORM - a declared logical descriptor, an explicit dialect, and the limits it declares.
Writing your own Adapter
The v3 adapter contract, what the core guarantees before an adapter runs, and why third-party adapters are not a supported extension point yet.