npm.io
0.0.1-1247-beta.1 • Published 3d ago

@qrvey/query-builder

Licence
MIT
Version
0.0.1-1247-beta.1
Deps
0
Size
545 kB
Vulns
0
Weekly
0

@qrvey/query-builder

Build one query, run it on any engine.

license types modules dependencies

@qrvey/query-builder turns a normalized, engine-agnostic query definition into an engine-specific query — SQL for PostgreSQL and ClickHouse, search DSL for Elasticsearch and OpenSearch — plus consistent output metadata.

It is a pure transform: no I/O, no engine clients, no product coupling. You give it an input and a target engine; it returns a query object. A consumer (for example @qrvey/enginex) executes the result.

import { QueryBuilder } from '@qrvey/query-builder';

const { query, values } = new QueryBuilder(
  {
    source: { name: 'orders' },
    select: [{ field: 'id', type: 'string' }, { field: 'amount', type: 'number' }],
    filter: { field: 'status', type: 'string', operator: 'eq', value: 'active' },
    orderBy: [{ field: 'created', type: 'date', direction: 'desc' }],
    limit: 20,
  },
  { engine: 'postgresql' },
).transpile();

// query  → SELECT "id", "amount" FROM "orders" WHERE "status" = $1 ORDER BY "created" DESC LIMIT 20
// values → ['active']

Table of contents


Why

Applications that must talk to several data stores usually end up with one query layer per engine and duplicated business logic. query-builder gives you a single, typed query definition that compiles to each engine's native query, so callers write the query once and only choose an engine at execution time.

  • One input, many engines — the same QueryInput compiles to SQL or search DSL.
  • Safe by construction — values are always parameterized; identifiers and LIKE/wildcard metacharacters are escaped.
  • Typed — the input and output contracts are fully typed.
  • Extensible — adding an engine means adding a dialect, not changing the contract.

Install

npm install @qrvey/query-builder
# or
yarn add @qrvey/query-builder

Ships CommonJS, ESM and type declarations. No runtime dependencies.


Quick start

Read a query
import { QueryBuilder } from '@qrvey/query-builder';

const builder = new QueryBuilder(
  {
    source: { name: 'orders' },
    groupBy: [{ field: 'country', type: 'string' }],
    aggregations: [
      { function: 'sum', field: 'amount', type: 'number' },
      { function: 'count' }, // COUNT(*) / total hits
    ],
    having: {
      aggregation: { function: 'sum', field: 'amount', type: 'number' },
      operator: 'gt',
      value: 1000,
    },
  },
  { engine: 'clickhouse' },
);

const result = builder.transpile();
Write a mutation
import { MutationBuilder } from '@qrvey/query-builder';

const result = new MutationBuilder(
  {
    operation: 'update',
    source: { name: 'orders' },
    set: [{ field: 'status', type: 'string', value: 'archived' }],
    filter: { field: 'amount', type: 'number', operator: 'lt', value: 10 },
  },
  { engine: 'postgresql' },
).transpile();
Choose the engine per call

The engine can be set on the constructor or per call:

const builder = new QueryBuilder(input);
builder.transpile({ engine: 'postgresql' });
builder.transpile({ engine: 'elasticsearch' });

Supported engines

Engine Family Identifiers Parameters
postgresql SQL "col" positional $1, $2, …
clickhouse SQL `col` named {pN:Type}
elasticsearch Search DSL dotted field names request body
opensearch Search DSL dotted field names request body

Features

  • Projections — select typed fields, or aggregate.
  • Filters — nested and/or/not trees with a closed operator set.
  • Aggregationscount, sum, avg, min, max (count without a field = COUNT(*) / total hits).
  • Grouping — group by fields, including calendar date grouping (hour … year).
  • Having — filter over aggregation results.
  • Ordering — by field or by aggregation.
  • Pagination — offset (skip) or cursor (search_after) with output metadata.
  • Nested fields — read JSON/object sub-fields via path.
  • Mutationsinsert (single/batch), upsert, update (with granular increment/append/removeField), delete.
  • Safety — parameterized values, escaped identifiers and LIKE/wildcard metacharacters, and a guard against unfiltered mutations.

Guides

Guide What it covers
Queries Building read queries: projections, filters, operators, aggregations, grouping, having, ordering, pagination, nested fields.
Mutations insert, upsert, update (+ granular actions), delete, and safety rules.
API reference Every exported class, method, type and error code.
Integration How an executor (e.g. @qrvey/enginex) interprets and runs each result per engine.
Architecture Contributor guide: internals, and how to add an engine or validation rule.

Result shapes

.transpile() returns a discriminated union — check kind:

// SQL engines (postgresql, clickhouse)
{
  engine, kind: 'sql',
  query,        // parameterized statement — run this
  values,       // positional params
  namedValues,  // named params (ClickHouse)
  plainQuery,   // literals inlined — logging/debug only, never execute
  metadata,     // output field descriptors + pagination
}

// Search engines (elasticsearch, opensearch)
{
  engine, kind: 'search-dsl',
  index,        // target index
  body,         // request body (_source/query/aggs/sort/size/…)
  metadata,
}

See the Integration guide for how to execute each shape, and the API reference for the full type definitions.


Validation & errors

const { valid, errors } = new QueryBuilder(input, { engine: 'postgresql' }).validate();
  • .validate() returns { valid, errors } — errors as data, no throw.
  • .plan() / .transpile() throw the first QueryBuilderError on invalid input.

Every error is a QueryBuilderError with a stable code and a path pointing at the offending input location (e.g. $.groupBy[0].limit). See the error codes.


Scope

Supported: one source; typed fields (string | number | boolean | date); nested and/or/not filters; the operator set above; count/sum/avg/min/max aggregations; group by (incl. date grouping) and per-group limits; having; order by field/aggregation; offset/cursor pagination (default limit 25, max limit/skip 10_000, max 50 selected fields); nested field paths; and portable insert/upsert/update/delete mutations.

Out of scope: joins; multiple sources; raw SQL/DSL passthrough; input aliases; calculated columns; distinct; full-text search; regex; median/percentiles; and normalization of ES/OS aggregation responses.


Architecture

The library separates a stable public input contract from engine-specific compilation:

QueryInput ─▶ validate shape ─▶ normalize ─▶ QueryPlan (AST) ─▶ validate plan ─▶ compile ─▶ result

The QueryPlan is the internal logical model (intent, not syntax); compilers consume a validated plan, never the raw input. SQL engines share SqlCompiler; search engines share SearchDslCompiler. The rule of thumb is compiler = shared flow, dialect = engine syntax, so adding an engine means implementing a dialect — not changing the contract.

See the API reference for the full public surface.


Changelog

See CHANGELOG.md.


License

MIT

Keywords