NestJS · TypeORM · Prisma · Drizzle
Transforme query strings REST em queries seguras.
nestjs-rest-query transforma filtros, ordenação, paginação, seleção de campos, carregamento de relações e busca textual num plano de query tipado, autoriza esse plano contra uma whitelist por endpoint e deixa um adapter de ORM compilá-lo. Três adapters, um núcleo semântico, uma resposta para a mesma requisição.
Do encanamento manual de queries para duas declarações
O schema diz o que o modelo é — tipos, nulabilidade, relações. As regras de endpoint dizem o que aquela rota autoriza — paths exatos, operadores por campo, projeções. As regras compiladas são a fonte única tanto da autorização em runtime quanto dos parâmetros OpenAPI gerados, então as duas não podem divergir.
@Get()
async listCompanies(@Query() query: ListCompaniesQuery) {
const qb = this.companies.createQueryBuilder('company');
if (query.name) qb.andWhere('company.name ILIKE :n', { n: `%${query.name}%` });
if (query.cnpj) qb.andWhere('company.cnpj = :c', { c: query.cnpj });
if (query.createdFrom) qb.andWhere('company.createdAt >= :f', { f: query.createdFrom });
if (query.sort === 'name') qb.orderBy('company.name', query.dir ?? 'ASC');
if (query.sort === 'createdAt') qb.orderBy('company.createdAt', query.dir ?? 'DESC');
const page = Number(query.page ?? 1);
const perPage = Math.min(Number(query.perPage ?? 20), 100);
qb.skip((page - 1) * perPage).take(perPage);
const [data, total] = await qb.getManyAndCount();
return { data, page, perPage, total, lastPage: Math.ceil(total / perPage) };
}// company.query.ts — declarado uma vez, fora do controller
export const companyRules = defineQueryRules(COMPANY_SCHEMAS, 'company', {
filters: [
{ path: 'name', operators: ['eq', 'ilike'] },
{ path: 'cnpj', operators: ['eq', 'in'] },
{ path: 'createdAt', operators: ['gte', 'lt', 'between'] },
],
sorts: ['name', 'createdAt'],
fields: {
root: {
allowed: ['id', 'name', 'cnpj', 'createdAt'],
default: ['id', 'name', 'cnpj', 'createdAt'],
},
},
});
// company.controller.ts
@Get()
@ApiDynamicQuery(companyRules)
findAll(
@Query() query: DynamicQueryDto,
@QueryRules() rules: CompiledQueryRules,
) {
return this.qb.execute(typeormSource(this.companies), query, rules);
}Compatibilidade de adapters
Os três adapters compilam o mesmo plano de query e são medidos pelo mesmo corpus de paridade em PostgreSQL, MySQL e SQL Server. O adapter não é configuração global — ele vem da source que você passa para execute(), e cada factory de source vive no seu próprio subpath.
| Adapter | Status | Factory de source |
|---|---|---|
| TypeORM | Estável | typeormSource(repository) — de nestjs-rest-query/typeorm. Adapter de referência. |
| Prisma | Estável | prismaSource({ client, model, manifest }) — de nestjs-rest-query/prisma. Operadores de padrão são recusados em SQL Server e SQLite. |
| Drizzle | Estável | drizzleSource({ db, dialect, table, relations }) — de nestjs-rest-query/drizzle. Exige drizzle-orm 1.x; 0.45.x não é aceito. |
Quickstart
- 1Instalar
Adicione o pacote e, depois, o único ORM que você usa. Todo peer de ORM é opcional e o entrypoint raiz não carrega nenhum deles.
pnpm add nestjs-rest-query pnpm add typeorm @nestjs/typeorm - 2Registrar o módulo
Importe o DynamicQueryBuilderModule uma vez, no AppModule. Nenhum adapter entra aqui — o forRoot configura apenas política comum.
import { DynamicQueryBuilderModule } from 'nestjs-rest-query'; @Module({ imports: [ DynamicQueryBuilderModule.forRoot({ pagination: { defaultPerPage: 10, maxPerPage: 100 }, }), ], }) export class AppModule {} - 3Declarar o schema e as regras
O schema descreve o modelo; as regras descrevem o que aquele endpoint autoriza. A whitelist é exata — autorizar company não autoriza company.name.
export const companyRules = defineQueryRules( COMPANY_SCHEMAS, 'company', { filters: [{ path: 'name', operators: ['eq', 'ilike'] }], sorts: ['name', 'createdAt'], fields: { root: { allowed: ['id', 'name'], default: ['id', 'name'] }, }, }, );