kuery
v2.0.0
Published
MongoDB-style in-memory query engine — zero dependencies, TypeScript, ESM+CJS
Downloads
9,305
Maintainers
Keywords
Readme
Kuery
MongoDB-style in-memory query engine — zero dependencies, TypeScript, ESM+CJS
Purpose
Kuery is a zero-dependency query engine for filtering in-memory JavaScript collections using MongoDB-compatible query syntax. It compiles queries to optimized native closures, supports full TypeScript with type-safe dot-path queries, and ships dual ESM+CJS.
Installation
npm i kueryQuick Start
import Kuery from 'kuery';
const users = [
{ id: 1, name: 'Alice', age: 25, role: 'admin', address: { city: 'NYC' } },
{ id: 2, name: 'Bob', age: 17, role: 'user', address: { city: 'LA' } },
{ id: 3, name: 'Carol', age: 30, role: 'mod', address: { city: 'NYC' } },
];
const admins = new Kuery({ role: 'admin' }).find(users);
// [{ id: 1, name: 'Alice', ... }]
const page = new Kuery({ age: { $gte: 18 } })
.sort({ name: 1 })
.skip(0)
.limit(10)
.find(users);
// [{ id: 1, name: 'Alice', ... }, { id: 3, name: 'Carol', ... }]
// CJS (backward-compatible with v1)
const Kuery = require('kuery');
new Kuery({ id: 1 }).findOne(collection);Architecture
Kuery processes queries through a three-stage pipeline:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ compile │────▶│ AST │────▶│ execute │
│ (query → │ │ (ExprNode │ │ (native │
│ ExprNode) │ │ tree) │ │ closures) │
└─────────────┘ └─────────────┘ └─────────────┘Compile — A MongoDB-style query object is parsed into an
ExprNodeAST (abstract syntax tree). This is a pure data structure that can be inspected, serialized, or transformed.AST — The intermediate representation uses three node kinds:
literal— constant valuespath— dot-path field referencesop— operator applications with child nodes
Execute — The AST is compiled into native JavaScript closures (
FilterFn) that run without interpretation overhead. Path resolution handles array traversal, and operators are dispatched via direct function calls.
This design enables:
- Pre-compilation: compile once, filter millions of documents
- AST inspection: transform queries, extract diagnostics, build alternative backends
- Custom operators: extend the engine without modifying core code
API Reference
Kuery class
| Method | Description |
|--------|-------------|
| new Kuery(query, options?) | Create instance with a filter query |
| .skip(n) | Skip first n matched documents |
| .limit(n) | Limit results to n documents |
| .sort(spec) | Sort by keys (1 = asc, -1 = desc, supports dot-paths) |
| .find(collection) | Return all matching documents |
| .findOne(collection) | Return exactly one match or throw KueryError |
| .test(document) | Return true if document matches the query |
Standalone functions
| Export | Signature | Description |
|--------|-----------|-------------|
| compileFilter | (query, options?) => FilterFn | Compile query to reusable native filter |
| compile | (query) => ExprNode | Compile query to inspectable AST |
| evaluate | (ast, scope) => unknown | Evaluate an AST against a document |
| find | (collection, query, options?) => T[] | Standalone collection find |
| findOne | (collection, query) => T \| undefined | Return first match (no uniqueness assertion) |
Note:
Kuery.findOne()asserts exactly one result (throws on 0 or 2+). The standalonefindOne()fromkuery/collectionreturns the first match orundefined.
Error handling
import { KueryError } from 'kuery';
try {
new Kuery({ id: 99 }).findOne(users);
} catch (e) {
if (e instanceof KueryError) {
console.log(e.code); // 'KUERY_FIND_ONE'
}
}Supported Operators
| Category | Operators |
|----------|-----------|
| Comparison | $eq, $ne, $gt, $gte, $lt, $lte |
| Inclusion | $in, $nin |
| Logical | $and, $or, $not, $nor |
| Element | $exists |
| Array | $elemMatch, $all, $size |
| String | $regex (with $options) |
TypeScript — Type-Safe Queries
Kuery ships TypedQuery<T> which provides autocomplete on dot-paths and validates operator values against field types:
import { Kuery, type TypedQuery } from 'kuery';
interface User {
name: string;
age: number;
role: 'admin' | 'user' | 'mod';
active: boolean;
address: {
city: string;
zip: string;
};
tags: string[];
}
// Autocomplete on field paths, type-checked operator values
const query: TypedQuery<User> = {
age: { $gte: 18 }, // ✓ number operator on number field
role: { $in: ['admin', 'mod'] }, // ✓ array of valid role values
'address.city': { $eq: 'Stockholm' }, // ✓ dot-path with string equality
active: true, // ✓ implicit $eq
};
const results = new Kuery(query).find(users); // results: User[]Extending TypedQuery with custom operators
If you register custom operators, you can make them type-safe via module augmentation:
// my-operators.ts
declare module 'kuery' {
interface CustomFieldOps<V> {
/** Match documents where a field starts with a prefix (string fields only). */
$startsWith?: V extends string ? string : never;
/** Graph traversal operator (string ID fields only). */
$inGraph?: V extends string ? { relation: string; rootId: string } : never;
}
}
// Now TypedQuery understands your custom operators:
const query: TypedQuery<User> = {
name: { $startsWith: 'Al' }, // ✓ type-safe custom operator
};Performance
v2 compiles queries to native closures instead of piping through lodash/fp. Benchmarks on a MacBook (100k documents):
| Query | v1 (lodash/fp) | v2 cold | v2 hot (pre-compiled) | Speedup |
|-------|---------------|---------|----------------------|---------|
| { status: 'active' } | 2.37ms | 0.81ms | 0.64ms | 3.7× |
| { status: 'active', score: { $gte: 50 } } | 3.08ms | 1.63ms | 1.39ms | 2.2× |
| { $or: [{ status: 'active' }, { role: 'admin' }] } | 3.53ms | 1.86ms | 1.70ms | 2.1× |
| { 'address.city': 'NYC' } | 2.28ms | 1.09ms | 1.03ms | 2.2× |
| { role: { $in: ['admin', 'mod', 'editor'] } } | 1.36ms | 1.05ms | 0.86ms | 1.6× |
| { name: { $regex: '^User_1' } } | 1.84ms | 1.27ms | 1.26ms | 1.5× |
Cold = new Kuery(query).find(collection) (compile + execute each call)
Hot = compileFilter(query) once, then collection.filter(fn) repeatedly
Pre-compiling filters is recommended for hot paths (event handlers, stream processing, repeated evaluations).
Advanced Usage
Pre-compiled filters
import { compileFilter } from 'kuery/filter';
const isEligible = compileFilter<User>({ age: { $gte: 18 }, active: true });
// Reuse across millions of evaluations
stream.filter(isEligible);
events.filter(isEligible);Custom operators
import { Kuery, OperatorRegistry } from 'kuery';
const registry = new OperatorRegistry();
registry.register(
{ name: '$between', arity: 2 },
(args) => {
const [value, [min, max]] = args;
return typeof value === 'number' && value >= min && value <= max;
}
);
const q = new Kuery({ score: { $between: [10, 50] } }, { registry });Failure tracing / diagnostics
import { compile } from 'kuery/compile';
import { evaluateWithTrace } from 'kuery/trace';
const ast = compile({ age: { $gte: 18 }, role: 'admin' });
const { result, traces } = evaluateWithTrace(ast, { age: 15, role: 'user' });
// traces: [{ path: 'age', operator: '$gte', expected: 18, actual: 15 }, ...]Sub-path imports
For tree-shaking or when you only need specific functionality:
import { compileFilter } from 'kuery/filter';
import { compile } from 'kuery/compile';
import { evaluate } from 'kuery/evaluate';
import { OperatorRegistry } from 'kuery/operators';
import { KueryError } from 'kuery/errors';
import { find, findOne } from 'kuery/collection';Migrating from v1
Breaking Changes
$existschecks key presence, not truthiness- v1:
{ field: { $exists: true } }matched if!!doc.fieldwas truthy - v2: matches if the key exists at all (even if value is
null,0,"",false) - Migration: use
{ field: { $ne: null } }if you need truthiness semantics
- v1:
Array fields now match element-wise (MongoDB-compatible)
- v1:
{ tags: { $in: ['a'] } }on{ tags: ['a','b'] }→ no match - v2: matches if any array element satisfies the condition
- v1:
Falsy values in dot-path traversal (bugfix)
- v1:
{ 'items.score': 0 }silently failed forscore: 0inside arrays - v2: correctly matches
- v1:
Error type:
KueryErrorreplaces genericErrorfindOnethrowsKueryErrorwith a.codepropertyinstanceof Errorstill true; existing catch handlers work
lodash removed — zero runtime dependencies
Non-Breaking Additions
- TypeScript types with
TypedQuery<T>and dot-path autocomplete - ESM + CJS dual format
- New operators:
$nor,$all,$size - Custom operator registry with type-safe augmentation
- Pre-compiled filter functions (1.5–3.7× faster)
- Prototype pollution protection
- Failure trace diagnostics
License
ISC
