nestjs-rest-querynestjs-rest-query

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 prisma

A 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

query/schemas.ts
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

query/manifest.ts
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

users/users.business.ts
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:

prisma/prisma.service.ts
@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:

providerlike, notLike, ilike, notIlike, searchPor quê
postgresql, mysqlFuncionam, e %/_ são literais — idêntico a TypeORM e Drizzle\ é o escape default do LIKE no dialeto, então escapar o valor basta
sqlite, sqlserverRecusados: 400 CAPABILITY_UNAVAILABLEsem 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:

  1. url saiu do datasource. Manter o schema da v2 faz a geração falhar com P1012: The datasource property 'url' is no longer supported in schema files. As URLs de conexão para CLI/Migrate vão para o prisma.config.ts; o client recebe a conexão de um driver adapter.
  2. O generator mudou. provider = "prisma-client-js" vira prisma-client, com output e moduleFormat obrigatórios, e o client deixa de existir em @prisma/client: import { PrismaClient } from '@prisma/client' passa a ser TS2305: Module '"@prisma/client"' has no exported member 'PrismaClient'.
  3. 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.
  4. O client gerado é TypeScript de verdade, não .d.ts. Gerar fora do rootDir do build quebra o nest build com TS6059. Gere dentro de src (output = "../src/generated/prisma").
  5. O runtime do Prisma 7 se carrega por import() dinâmico, então qualquer runner Jest que o toque precisa de NODE_OPTIONS=--experimental-vm-modules. A receita ESM do ts-jest (useESM: true + extensionsToTreatAsEsm) falha contra o client gerado com ReferenceError: exports is not defined; o que funciona é ts-jest em CommonJS mais a flag.
  6. O .env não é mais lido pelo Prisma (consequência do item 1): carregue dotenv e valide a DATABASE_URL você mesmo.

Próximos passos

Editar esta página no GitHub

On this page