Adapter Prisma
Use o nestjs-rest-query com o Prisma - o client gerado passado direto, um manifesto escrito à mão e os operadores que dois dialetos não conseguem atender.
O adapter Prisma compila o plano de query em argumentos de findMany/count.
Relação many usa some/none, relação one usa is/isNot, e o perfil
textual portável consulta colunas dobradas, então mode: 'insensitive' nunca é
emitido.
Instalação
pnpm add @prisma/client
pnpm add -D prismaA faixa suportada é ^6.19.0 || ^7.0.0. O alvo da matriz é o Prisma 7.8.0
(CLI e client na mesma versão), com o driver adapter oficial por dialeto.
Três artefatos
Não existe ferramenta que os derive. O generator que leria o schema.prisma é
lacuna declarada para a 3.1.0, então os três são escritos à mão e mantidos
em par com o schema por revisão.
1. O schema lógico de todo model alcançável
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,
// Sem isto o endpoint é recusado na subida com CAPABILITY_UNAVAILABLE: o
// desempate de paginação é sempre sobre a chave primária, e uuid não tem
// ordem total portável.
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],
]);O path do campo é o nome da propriedade do client, não o da coluna — é
ele que vai no where/select/orderBy que o adapter monta. Com
@map("name_folded") no schema.prisma, a API HTTP fica camelCase e o banco
segue o snake_case do perfil certificado.
2. O manifesto
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({
// Decide o dialeto e, com ele, o escape de padrão.
provider: 'postgresql',
registry: APP_SCHEMAS,
models: {
company: { delegate: 'company' }, // liga o model a prisma.company
user: { delegate: 'user' },
post: { delegate: 'post' },
},
});O createPrismaManifest valida o manifesto contra si mesmo na inicialização:
model sem entrada no registry, ou sem delegate, falha com
SOURCE_CONFIGURATION_INVALID. O delegate nomeado pelo manifesto é validado
outra vez na construção da source — uma checagem que um tipo nunca poderia
fazer aqui, porque o nome do delegate vem de dado, não de código.
3. A source, por requisição
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,
);
}O prismaSource({ client }) aceita o PrismaClient gerado direto — sem
cast e sem fachada própria:
@Injectable()
export class PrismaService extends PrismaClient {}Vale dizer porque não era assim até a 3.0.0: o client era tipado
Readonly<Record<string, PrismaDelegate>>, e nenhum PrismaClient real
satisfazia esse tipo — classe não recebe index signature implícita em
TypeScript, então todo consumidor precisava de uma afirmação.
O prismaSource<TRow> é genérico no tipo da linha. Nada nas opções carrega esse
tipo — o delegate vem do manifesto, não de um client tipado —, então esta é a
única fábrica em que você mesmo nomeia o argumento de tipo:
prismaSource<UserDto>({ client, model, manifest }) deixa o método ser anotado
Promise<NormalizedQueryResult<UserDto>> sem cast. Omitindo, você fica com
object, como acima.
Operadores recusados, por provider
Esta é a única divergência declarada da v3. Sob a gramática da v3, %, _ e
\ são caracteres literais: filter[name][like]=100% procura o texto "100%". O
TypeORM e o Drizzle emitem LIKE ... ESCAPE, então honram isso diretamente. O
Prisma compila contains para LIKE ('%' || ? || '%') sem cláusula
ESCAPE, e o client tipado não expõe forma de fornecê-la. Sobra apenas o
caractere de escape default do dialeto, o que divide os providers em dois:
provider | like, notLike, ilike, notIlike, search | Por quê |
|---|---|---|
postgresql, mysql | Funcionam, e %/_ são literais — idêntico a TypeORM e Drizzle | \ é o escape default do LIKE no dialeto, então escapar o valor basta |
sqlite, sqlserver | Recusados: 400 CAPABILITY_UNAVAILABLE | sem caractere de escape default, a literalidade não pode ser honrada — recusar é melhor que devolver as linhas erradas |
A recusa é por requisição, não na inicialização, então o resto da gramática
continua utilizável nesses dialetos. Declarar o provider errado no manifesto é,
portanto, a diferença entre um endpoint correto e um endpoint silenciosamente
errado.
Medido no Prisma 7.8.0 + PostgreSQL pelo
04-app-with-prisma:
GET /posts?filter[title][like]=100%25 devolve só "Desconto de 100% na conta de
luz", e filter[title][like]=a_b devolve só "Circuito a_b revisado" — os
metacaracteres são literais.
Se você está migrando um consumidor Prisma em SQL Server, esta é a parte quebrada: cinco operadores deixam de existir. Planeje.
O que o Prisma não verifica
O PrismaAdapter.describe() devolve o schema do manifesto na íntegra. Então
nada compara o seu schema lógico com o schema.prisma nem com o banco: um
nome de campo errado aparece como erro do Prisma na primeira requisição que o
tocar, onde o caminho do TypeORM teria falhado na subida. A garantia de
"metadado ausente falha fechado" não se estende ao Prisma.
O customize recebe os argumentos da query
O contexto nativo é uma query por vez, marcada com qual delas é:
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',
}
);Ser chamado uma vez por query — em vez de receber as duas juntas — é o que faz o
'both', o default seguro, realmente atingir dados e contagem.
Prisma 7, se você vem da 6
Seis coisas mudam e nenhuma é opcional:
urlsaiu dodatasource. Manter o schema da v2 faz a geração falhar comP1012: The datasource property 'url' is no longer supported in schema files. As URLs de conexão para CLI/Migrate vão para oprisma.config.ts; o client recebe a conexão de um driver adapter.- O generator mudou.
provider = "prisma-client-js"viraprisma-client, comoutputemoduleFormatobrigatórios, e o client deixa de existir em@prisma/client:import { PrismaClient } from '@prisma/client'passa a serTS2305: Module '"@prisma/client"' has no exported member 'PrismaClient'. new PrismaClient()precisa de driver adapter. Adicione o adapter do seu dialeto na mesma major do client (@prisma/adapter-pg) mais o driver (pg), e passe{ adapter: new PrismaPg({ connectionString }) }ao construtor.- O client gerado é TypeScript de verdade, não
.d.ts. Gerar fora dorootDirdo build quebra onest buildcomTS6059. Gere dentro desrc(output = "../src/generated/prisma"). - O runtime do Prisma 7 se carrega por
import()dinâmico, então qualquer runner Jest que o toque precisa deNODE_OPTIONS=--experimental-vm-modules. A receita ESM do ts-jest (useESM: true+extensionsToTreatAsEsm) falha contra o client gerado comReferenceError: exports is not defined; o que funciona é ts-jest em CommonJS mais a flag. - O
.envnão é mais lido pelo Prisma (consequência do item 1): carreguedotenve valide aDATABASE_URLvocê mesmo.
Próximos passos
- Guia de Uso para todos os parâmetros, operadores e códigos de erro.
- Visão geral dos adapters para a tabela completa de divergências.
- O guia de migração completo
para o caminho
2.x→3.x.
Adapter Drizzle
Use o nestjs-rest-query com o Drizzle ORM - descritor lógico declarado, dialeto explícito e os limites que ele declara.
Escrevendo o seu Adapter
O contrato de adapter da v3, o que o núcleo garante antes de um adapter rodar, e por que adapters de terceiros ainda não são um ponto de extensão suportado.