AdaptersWriting your own Adapter
Writing your own Adapter
Custom support for Prisma, Kysely, raw SQL, or any other ORM.
nestjs-rest-query is ORM-agnostic. If you want to support Prisma, Kysely, raw SQL, or any other database/ORM, you can implement the RestQueryAdapter interface.
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 — operator (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;
}Adapter responsibilities
Your adapter receives already normalized and coerced parameters:
- Value coercion - the library has already converted
"18"into18,"2024-01-01,2024-12-31"into[Date, Date], etc. Your adapter receives values in the correct type. - Whitelist validation - unauthorized fields were rejected before reaching your adapter.
- Operator parsing - you receive a valid
QueryOperator, never an arbitrary string.
Your adapter only needs to:
- Translate each operation into your ORM's native syntax.
- Ensure dotted paths (for example,
company.name) produce correct JOINs (the implementation is up to you). - Return data in the shape expected by the application (for example, pagination returns
{ data, total }).
Example - Prisma Adapter (pseudo-code)
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 {};
}
}
}Source pattern
The generic TSource is your contract. Define it however it fits best:
interface PrismaSource {
client: PrismaClient;
modelName: string;
}
interface KyselySource {
db: Database;
tableName: string;
}
interface RawSqlSource {
connection: PoolClient;
table: string;
schema: ColumnSchema[];
}Your adapter knows how to use TSource; the consumer passes it:
@Get()
list(@Query() query: QueryInput, @QueryRules() rules: RulesConfig) {
return this.qb.execute(
{
client: this.prisma,
modelName: 'User',
},
query,
rules
);
}Registering your 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 {}Implementation checklist
- Implement all 7 interface methods
- Value coercion - your adapter assumes already typed values
- Operators - translate
QueryOperatorto your ORM's syntax - Pagination - return
{ data, total }with rows and count - Dotted paths - ensure automatic JOINs (or a clear error if unsupported)
- Manual test - run a request with filter, sort, include, search, and pagination
- Consider contributing back - if it is a popular ORM (Prisma, Kysely), open an issue for discussion!
Contributing an adapter
If you implement an adapter for a popular ORM (Prisma, Kysely, etc.), consider contributing:
- Open a Discussion describing the ORM and your interest.
- Follow the structure in
src/infra/adapters/(see TypeORM and Drizzle for reference). - Add unit tests in
tests/adapters/. - Send a PR linked to the discussion.
Each approved adapter will be maintained in the core and exported as a subpath (for example, nestjs-rest-query/prisma).