nestjs-rest-querynestjs-rest-query

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" into 18, "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:

  1. Translate each operation into your ORM's native syntax.
  2. Ensure dotted paths (for example, company.name) produce correct JOINs (the implementation is up to you).
  3. 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 QueryOperator to 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:

  1. Open a Discussion describing the ORM and your interest.
  2. Follow the structure in src/infra/adapters/ (see TypeORM and Drizzle for reference).
  3. Add unit tests in tests/adapters/.
  4. 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).

Resources

Edit this page on GitHub

On this page