Re-write of underscore-query using ES modules, TypeScript, and Ramda in an (extreme) point-free style.
This module exports createPredicate, which takes a mongo-like query and returns a predicate function. Use it with Array.prototype.filter, Ramda's R.filter, R.find, and so on.
The module parses a wide variety of mongo queries — see the tests for examples. For operator behaviour, see the underscore-query README.
npm install query-predicateThe package is ESM-only ("type": "module"). Import it with:
import createPredicate from 'query-predicate'import createPredicate, { type QueryInput } from 'query-predicate'
const query: QueryInput = {
status: 'active',
score: { $gte: 10 },
tags: { $elemMatch: { label: 'sale' } },
}
const matches = createPredicate(query)
const results = items.filter(matches)createPredicate also exposes sort/filter helpers:
import createPredicate from 'query-predicate'
const { filterAndSort, filterNoSort } = createPredicate.sortFunctions
const filtered = filterNoSort([{ status: 'active' }], collection)To inspect the parsed query AST without running it:
import { parseQuery, type ParsedNode } from 'query-predicate'
const ast: ParsedNode[] = parseQuery({ foo: { $gt: 1 } })From v2.1 the package is written in TypeScript and published as compiled ESM in dist/.
| Before (v2.0) | After (v2.1) |
|---|---|
CommonJS (require) |
ESM (import) |
src/*.js shipped directly |
src/*.ts compiled to dist/ |
| No type definitions | .d.ts via package.json "types" |
| Default export only | Default export + named type/value exports |
Build locally
npm run build # tsc → dist/
npm test # build + mocha (107 tests)Typing strategy
- The public API (
index.ts,types.ts,memoize.ts) is fully type-checked. - Internal Ramda-heavy modules (
parser,operators,run-query,sort) use// @ts-nocheck. They are point-free pipelines that do not play nicely with strict Ramda typings; behaviour is covered by the existing test suite.
If you are consuming the package, you only need the exported types below — you do not depend on those internal implementation details.
All types below are re-exported from the package entry point:
import type {
QueryInput,
Predicate,
// ...
} from 'query-predicate'These describe what you can pass to createPredicate and parseQuery.
| Type | Purpose |
|---|---|
QueryInput |
Top-level query: field map, compound query, swapped operator form, or array of queries |
QueryObject |
Object keyed by field names and/or $and / $or / $not / $nor |
FieldQuery |
Value for a single field: scalar, RegExp, function, or operator object |
FieldOperatorQuery |
Operator conditions on one field, e.g. { $gt: 5, $lt: 10 } |
RegexFieldQuery |
Regex with options: { $regex: "pat", $options: "i" } |
SwappedOperatorQuery |
Operator-first form: { $equal: { status: "active" } } |
QueryScalar |
string | number | boolean | null | undefined |
QueryOperator |
Union of all field operators ($equal, $gt, $in, $elemMatch, …) |
CompoundOperator |
$and | $or | $not | $nor |
OperatorValueMap |
Maps each operator to its expected value shape (e.g. $between: [min, max]) |
QueryInput examples:
// implicit $equal
{ status: 'active' }
// explicit operators
{ score: { $gte: 10, $lt: 100 } }
// compound
{ $and: [{ a: 1 }, { b: 2 }] }
// elemMatch
{ items: { $elemMatch: { qty: { $gt: 0 } } } }
// swapped operator
{ $equal: { status: 'active' } }| Type | Purpose |
|---|---|
Predicate<T> |
(data: T) => boolean — return type of createPredicate |
ParsedNode |
Union of parsed AST node types |
ParsedQuery |
Leaf node: { _type: 'query', key, op, val } |
ParsedCompound |
Compound node: { _type: 'compound', op, queries } |
ParsedElemMatch |
$elemMatch node: { _type: 'elemMatch', key, queries } |
| Export | Description |
|---|---|
default (createPredicate) |
(query: QueryInput) => Predicate |
parseQuery |
(query: QueryInput) => ParsedNode[] |
createPredicate.sortFunctions |
filterAndSort, filterNoSort, createSortMap, etc. |
Generic document typing:
interface Article {
title: string
score: number
tags: { label: string }[]
}
const query: QueryInput = { score: { $gte: 10 } }
const matches = createPredicate<Article>(query)
articles.filter(matches) // Article[]This is an experiment in how much of a non-trivial program can be written in a point-free manner — partly to explore the Ramda API, partly as a challenge.
Currently there are only a few explicit function declarations:
- 3 are needed to allow recursion (a y-combinator rewrite would make those harder to reason about)
- 1 is needed to throw errors
When used well, the style is expressive: R.pluck("key") states intent more clearly than a manual for loop. Some operators (e.g. $mod) become much more verbose in point-free form — see src/operators.ts for an example.