https://github.com/lukethacoder/query-builder-ts
๐ฏ TypeScript Query Builder implementation
https://github.com/lukethacoder/query-builder-ts
query-builder
Last synced: about 1 month ago
JSON representation
๐ฏ TypeScript Query Builder implementation
- Host: GitHub
- URL: https://github.com/lukethacoder/query-builder-ts
- Owner: lukethacoder
- License: mit
- Created: 2026-06-22T12:19:39.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-06-22T15:09:50.000Z (about 1 month ago)
- Last Synced: 2026-06-22T15:10:44.265Z (about 1 month ago)
- Topics: query-builder
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/@lukethacoder/query-builder
- Size: 56.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# @lukethacoder/query-builder
A TypeScript implementation of the [QueryBuilder Spec](https://github.com/lukethacoder/query-builder-spec) โ a language-neutral standard for representing, addressing, manipulating, validating, and serializing structured filter queries.
## Installation
```sh
pnpm add @lukethacoder/query-builder
```
## Overview
A query is a tree. The root is a **group**; groups contain **rules** (single conditions) and nested groups. This library gives you:
- **Types** โ full TypeScript types for the entire data model
- **Manipulation** โ immutable operations to build and edit query trees (`add`, `remove`, `update`, `move`, `insert`, `group`)
- **Validation** โ a default validator plus utilities for custom validators
- **Serialization** โ export to JSON, SQL, parameterized SQL, MongoDB query objects, and JsonLogic
## Quick start
```ts
import { add, formatSql, remove, update } from "@lukethacoder/query-builder";
import type { RuleGroup } from "@lukethacoder/query-builder";
// Start with an empty query
const empty: RuleGroup = { combinator: "and", rules: [] };
// Add rules
let query = add(empty, { field: "firstName", operator: "=", value: "Steve" }, []);
query = add(query, { field: "lastName", operator: "=", value: "Vai" }, []);
// Export to SQL
console.log(formatSql(query));
// โ (firstName = 'Steve' and lastName = 'Vai')
```
## Data model
### Rule
A single condition: a field, an operator, and a value.
```ts
interface Rule {
id?: string;
field: string;
operator: string;
value: unknown;
valueSource?: "value" | "field";
disabled?: boolean;
}
```
### RuleGroup (standard)
Joins all children with a single combinator.
```ts
interface RuleGroup {
id?: string;
combinator: string; // e.g. "and" | "or"
rules: Array;
not?: boolean;
disabled?: boolean;
}
```
### RuleGroupIC (independent combinators)
Allows mixed combinators within one group by interleaving combinator strings between children.
```ts
interface RuleGroupIC {
id?: string;
rules: Array; // strings are combinators
not?: boolean;
disabled?: boolean;
}
```
## Manipulation
All operations are **immutable** โ they return a new query and never mutate the input.
Operations accept either a `Path` (integer array) or a node `id` string as the location argument.
### `add(query, ruleOrGroup, parentPath, options?)`
Appends a rule or group to the end of the target group's children.
```ts
import { add } from "@lukethacoder/query-builder";
query = add(query, { field: "age", operator: ">", value: 18 }, []);
```
### `remove(query, path)`
Removes the node at the given path.
```ts
import { remove } from "@lukethacoder/query-builder";
query = remove(query, [0]); // remove first child of root
```
### `update(query, property, value, path, options?)`
Sets a single property on the node at the given path.
```ts
import { update } from "@lukethacoder/query-builder";
query = update(query, "operator", ">=", [0]);
query = update(query, "combinator", "or", []); // update root combinator
```
Changing `field` automatically resets `operator`, `valueSource`, and `value` by default (`resetOnFieldChange: true`). Pass `options` to override.
### `move(query, fromPath, toPath, options?)`
Moves a node to another position. `toPath` can be a `Path`, `"up"`, or `"down"`.
```ts
import { move } from "@lukethacoder/query-builder";
query = move(query, [0], [2]);
query = move(query, [1], "up");
query = move(query, [0], [1], { clone: true }); // copy instead of move
```
### `insert(query, ruleOrGroup, path, options?)`
Inserts a node at an exact position (always regenerates IDs on the inserted node).
```ts
import { insert } from "@lukethacoder/query-builder";
query = insert(query, { field: "city", operator: "=", value: "London" }, [1]);
```
### `group(query, sourcePath, targetPath, options?)`
Wraps the nodes at `sourcePath` and `targetPath` together inside a new nested group at `targetPath`.
```ts
import { group } from "@lukethacoder/query-builder";
query = group(query, [1], [0]);
```
### IC (independent combinator) queries
Convert between standard and IC forms:
```ts
import { convertToIC, convertFromIC } from "@lukethacoder/query-builder";
const ic = convertToIC(standardQuery);
const standard = convertFromIC(icQuery);
```
## Validation
### Default validator
Validates group structure: flags empty groups, invalid combinators, and malformed IC arrays.
```ts
import { defaultValidator } from "@lukethacoder/query-builder";
const result = defaultValidator(query);
// โ Record
```
### Custom validators
```ts
import type { QueryValidator, RuleValidator } from "@lukethacoder/query-builder";
// Rule-level: attached to a field definition
const ageValidator: RuleValidator = (rule) => {
return Number(rule.value) >= 0
? true
: { valid: false, reasons: ["age must be non-negative"] };
};
// Query-level: passed to serializers or your own pipeline
const queryValidator: QueryValidator = (query) => {
// return boolean or ValidationMap keyed by node id
};
```
## Serialization
All serializers accept a `validator` option โ invalid rules/groups are dropped from non-JSON output.
### JSON
```ts
import { formatJson, formatJsonWithoutIds, parseJson } from "@lukethacoder/query-builder";
const json = formatJson(query); // pretty JSON, includes ids
const stable = formatJsonWithoutIds(query); // single-line, strips ids and paths
const parsed = parseJson(json); // round-trip back to a query tree
```
### SQL
```ts
import { formatSql } from "@lukethacoder/query-builder";
formatSql(query);
// โ (firstName = 'Steve' and lastName = 'Vai')
// Dialect presets
formatSql(query, { preset: "postgresql" });
formatSql(query, { preset: "mssql" });
formatSql(query, { preset: "mysql" });
formatSql(query, { preset: "sqlite" });
```
### Parameterized SQL
```ts
import { formatParameterized, formatParameterizedNamed } from "@lukethacoder/query-builder";
const { sql, params } = formatParameterized(query);
// โ { sql: "(firstName = ? and lastName = ?)", params: ["Steve", "Vai"] }
const named = formatParameterizedNamed(query);
// โ { sql: "(firstName = :firstName_1 and lastName = :lastName_1)", params: { firstName_1: "Steve", lastName_1: "Vai" } }
// PostgreSQL numbered params
formatParameterized(query, { preset: "postgresql" });
// โ { sql: "(firstName = $1 and lastName = $2)", params: ["Steve", "Vai"] }
```
### MongoDB query
```ts
import { formatMongodbQuery } from "@lukethacoder/query-builder";
formatMongodbQuery(query);
// โ { "$and": [{ "firstName": "Steve" }, { "lastName": "Vai" }] }
```
### JsonLogic
```ts
import { formatJsonLogic } from "@lukethacoder/query-builder";
formatJsonLogic(query);
// โ { "and": [{ "==": [{ "var": "firstName" }, "Steve"] }, { "==": [{ "var": "lastName" }, "Vai"] }] }
```
## Default operators
| Identifier | Label | Arity |
|---|---|---|
| `=` | `=` | binary |
| `!=` | `!=` | binary |
| `<` | `<` | binary |
| `>` | `>` | binary |
| `<=` | `<=` | binary |
| `>=` | `>=` | binary |
| `contains` | `contains` | binary |
| `beginsWith` | `begins with` | binary |
| `endsWith` | `ends with` | binary |
| `doesNotContain` | `does not contain` | binary |
| `doesNotBeginWith` | `does not begin with` | binary |
| `doesNotEndWith` | `does not end with` | binary |
| `null` | `is null` | unary |
| `notNull` | `is not null` | unary |
| `in` | `in` | multi-value |
| `notIn` | `not in` | multi-value |
| `between` | `between` | ternary |
| `notBetween` | `not between` | ternary |
## Path addressing
Paths are integer arrays describing a node's location in the tree. The root is `[]`; `[0]` is the first child of the root; `[2, 1]` is the second child of the third child.
In IC queries, children occupy **even** indices (0, 2, 4 โฆ) and combinator strings occupy odd indices.
```ts
import { findPath, getParentPath, isAncestor } from "@lukethacoder/query-builder";
const node = findPath([0, 1], query);
const parent = getParentPath([0, 1]); // โ [0]
const ancestor = isAncestor([0], [0, 1, 2]); // โ true
```
## Spec conformance
This library implements **core + baseline export conformance** as defined by the QueryBuilder Spec:
- **Core** โ data model (ch. 01), configuration model (ch. 02), defaults (ch. 03), paths (ch. 04), query manipulation (ch. 05), validation (ch. 06), canonical JSON (ยง7.2)
- **Baseline exports** โ `json`, `json_without_ids`, `sql`, `parameterized`, `parameterized_named`, `mongodb_query`, `jsonlogic`
Optional extended formats (CEL, SPEL, Elasticsearch, ORM adapters, import parsers) are not yet implemented.
## License
MIT