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.

Antes — escrito à mão
@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) };
}
Depois — nestjs-rest-query
// 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.

AdapterStatusFactory de source
TypeORMEstáveltypeormSource(repository) — de nestjs-rest-query/typeorm. Adapter de referência.
PrismaEstávelprismaSource({ client, model, manifest }) — de nestjs-rest-query/prisma. Operadores de padrão são recusados em SQL Server e SQLite.
DrizzleEstáveldrizzleSource({ db, dialect, table, relations }) — de nestjs-rest-query/drizzle. Exige drizzle-orm 1.x; 0.45.x não é aceito.

Quickstart

  1. 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
  2. 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 {}
  3. 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'] },
        },
      },
    );