AdaptersEscrevendo seu próprio Adapter
Escrevendo seu próprio Adapter
Suporte customizado para Prisma, Kysely, raw SQL, ou qualquer outro ORM.
O nestjs-rest-query é agnóstico a ORM. Se você quer suportar Prisma, Kysely, raw SQL, ou qualquer outro banco/ORM, você pode implementar a interface RestQueryAdapter.
Interface RestQueryAdapter
export interface RestQueryAdapter<TQB = any, TSource = any> {
/**
* Cria um query builder acumulador.
* Chamado uma vez por request.
*
* @param source — objeto descrevendo a tabela/modelo raiz (formato é definido por você)
* @param alias — identificador para a tabela raiz nos resultados
* @returns um query builder opaco (seu tipo interno)
*/
createQueryBuilder(source: TSource, alias: string): TQB;
/**
* Aplica um filtro.
* O valor já foi coerced (números como números, arrays como arrays, datas como Dates).
*
* @param qb — query builder
* @param alias — alias da entidade
* @param fieldPath — caminho do campo (ex: "name", "company.name")
* @param operator — operador (eq, ne, gt, gte, lt, lte, like, ilike, in, notIn, between, isNull, notLike, notIlike)
* @param value — valor já coerced
*/
applyFilter(
qb: TQB,
alias: string,
fieldPath: string,
operator: QueryOperator,
value: unknown
): void;
/**
* Aplica ordenação.
*
* @param qb — query builder
* @param alias — alias
* @param fieldPath — campo a ordenar
* @param direction — 'ASC' ou 'DESC'
*/
applySort(
qb: TQB,
alias: string,
fieldPath: string,
direction: 'ASC' | 'DESC'
): void;
/**
* Define as colunas a serem retornadas.
* Se fields está vazio, use todas as colunas da tabela raiz.
*
* @param qb — query builder
* @param alias — alias
* @param fields — lista de caminhos de campo (ex: ["id", "name", "company.name"])
* @param includes — relações a incluir (ex: ["company", "posts"])
*/
applySelect(
qb: TQB,
alias: string,
fields: string[],
includes: string[]
): void;
/**
* Registra uma relação a ser joinada (para includes ou presentation).
*
* @param qb — query builder
* @param alias — alias
* @param joinPath — caminho de relação (ex: "company")
*/
applyInclude(qb: TQB, alias: string, joinPath: string): void;
/**
* Aplica busca full-text (search).
* Procure o termo em uma lista de colunas, usando OR lógico.
*
* @param qb — query builder
* @param alias — alias
* @param columns — lista de caminhos de campo (ex: ["name", "email"])
* @param term — termo de busca (substring match esperado)
*/
applySearch(qb: TQB, alias: string, columns: string[], term: string): void;
/**
* Aplica paginação e executa a query.
* Retorna { data: [...], total: number }.
*
* @param qb — query builder
* @param offset — skip count
* @param limit — take count
* @returns objeto com data e total
*/
applyPagination(
qb: TQB,
offset: number,
limit: number
): Promise<{ data: any[]; total: number }>;
/**
* Executa a query sem paginação.
* Retorna apenas os dados, sem count.
*
* @param qb — query builder
* @returns lista de linhas
*/
getMany(qb: TQB): Promise<any[]>;
/**
* Hook de customização.
* Permite que o consumer aplique operações customizadas ao query builder.
*
* @param qb — query builder
* @param fn — função que recebe qb e pode modificá-lo
*/
customize(qb: TQB, fn: (qb: TQB) => void): void;
}Responsabilidades do Adapter
Seu adapter recebe parâmetros já normalizados e coerced:
- Coerção de valores — a biblioteca já converteu
"18"em18,"2024-01-01,2024-12-31"em[Date, Date], etc. Seu adapter recebe valores no tipo correto. - Validação de whitelist — campos não autorizados foram rejeitados antes de chegar ao seu adapter.
- Parsing de operadores — você recebe um
QueryOperatorválido, nunca uma string arbitrária.
Seu adapter precisa apenas:
- Traduzir cada operação para a sintaxe nativa do seu ORM.
- Garantir que caminhos com pontos (ex:
company.name) resultem em JOINs corretos (sua implementação é livre). - Retornar dados na forma esperada pela aplicação (ex: paginação retorna
{ data, total }).
Exemplo — Prisma Adapter (pseudo-código)
import { RestQueryAdapter } from 'nestjs-rest-query';
export class PrismaAdapter implements RestQueryAdapter<
PrismaQueryBuilder,
PrismaSource
> {
createQueryBuilder(source: PrismaSource, alias: string) {
return {
source,
alias,
whereConditions: [],
orderBy: [],
select: undefined,
include: {},
};
}
applyFilter(qb, alias, fieldPath, operator, value) {
// Traduzir para Prisma's where conditions
const where = this.translateOperator(fieldPath, operator, value);
qb.whereConditions.push(where);
}
applySort(qb, alias, fieldPath, direction) {
qb.orderBy.push({ [fieldPath]: direction === 'ASC' ? 'asc' : 'desc' });
}
applySelect(qb, alias, fields, includes) {
// Mapear para Prisma select e include
qb.select = fields;
for (const inc of includes) {
qb.include[inc] = true;
}
}
applyInclude(qb, alias, joinPath) {
qb.include[joinPath] = true;
}
applySearch(qb, alias, columns, term) {
const orConditions = columns.map((col) => ({
[col]: { contains: term, mode: 'insensitive' },
}));
qb.whereConditions.push({ OR: orConditions });
}
async applyPagination(qb, offset, limit) {
const data = await qb.source.client[qb.source.modelName].findMany({
where: qb.whereConditions.length
? { AND: qb.whereConditions }
: undefined,
orderBy: qb.orderBy,
skip: offset,
take: limit,
select: qb.select,
include: qb.include,
});
const total = await qb.source.client[qb.source.modelName].count({
where: qb.whereConditions.length
? { AND: qb.whereConditions }
: undefined,
});
return { data, total };
}
async getMany(qb) {
return qb.source.client[qb.source.modelName].findMany({
where: qb.whereConditions.length
? { AND: qb.whereConditions }
: undefined,
orderBy: qb.orderBy,
select: qb.select,
include: qb.include,
});
}
customize(qb, fn) {
fn(qb);
}
private translateOperator(
fieldPath: string,
operator: string,
value: unknown
) {
switch (operator) {
case 'eq':
return { [fieldPath]: value };
case 'ne':
return { NOT: { [fieldPath]: value } };
case 'gt':
return { [fieldPath]: { gt: value } };
// ... etc
default:
return {};
}
}
}Padrão Source
O TSource genérico é seu contrato. Defina-o como se encaixe melhor:
interface PrismaSource {
client: PrismaClient;
modelName: string;
}
interface KyselySource {
db: Database;
tableName: string;
}
interface RawSqlSource {
connection: PoolClient;
table: string;
schema: ColumnSchema[];
}Seu adapter sabe como usar o TSource; o consumer passa-o:
@Get()
list(@Query() query: QueryInput, @QueryRules() rules: RulesConfig) {
return this.qb.execute(
{
client: this.prisma,
modelName: 'User',
},
query,
rules
);
}Registrando seu Adapter
import { Module } from '@nestjs/common';
import { DynamicQueryBuilderModule } from 'nestjs-rest-query';
import { PrismaAdapter } from './adapters/prisma.adapter';
@Module({
imports: [
DynamicQueryBuilderModule.forRoot({
adapter: new PrismaAdapter(),
}),
],
})
export class AppModule {}Checklist de implementação
- Implementar todos os 7 métodos da interface
- Coerção de valor — seu adapter assume valores já tipados
- Operadores — traduzir
QueryOperatorpara a sintaxe do seu ORM - Paginação — retornar
{ data, total }com linhas e contagem - Caminhos com pontos — garantir JOINs automáticos (ou erro claro se não suportado)
- Teste manual — executar um request com filtro, sort, include, search, pagination
- Considere contribuir de volta — se for uma ORM popular (Prisma, Kysely), abra uma issue para discussão!
Contribuindo um Adapter
Se você implementar um adapter para uma ORM popular (Prisma, Kysely, etc.), considere contribuir:
- Abra uma Discussion descrevendo a ORM e seu interesse.
- Siga a estrutura de
src/infra/adapters/(see TypeORM e Drizzle como referência). - Adicione testes unitários em
tests/adapters/. - Envie um PR vinculado à discussion.
Cada adapter aprovado será mantido no core e exportado como subpath (ex: nestjs-rest-query/prisma).