Compact, safe expression DSL for filtering JSON-like data.
npm install filter-expression> Compact, safe expression DSL for filtering JSON-like data.



Compile a string expression once and evaluate it against many records — no eval.
``bash`
npm i filter-expression
`ts
import { compile } from 'filter-expression';
const expr = compile("user.age >= 18 and country == 'US'");
const ok = expr.evaluate({ user: { age: 22 }, country: 'US' }); // true
`
- compile(source: string, options?: CompileOptions): CompiledExpressionCompiledExpression.evaluate(record: unknown): boolean
- CompileOptions
- caseInsensitive?: boolean
- — lowercases string comparisons and regexmaxDepth?: number
- — guard against runaway recursion (default 128)disallowRegex?: boolean
- — make regex(...) always return false
- Safe by default: no eval; guarded property access; regex opt-outin/nin
- Portable DSL: store, transmit, and reuse filters across boundaries
- Practical operators: logical, comparisons, , and regex
- Predictable null/undefined and typed comparison semantics
- Fast evaluate-many workflow with pre-parsed AST
| Function | Description |
|---|---|
| compile(source, options?) | Parses and compiles an expression into a reusable evaluator. |
| Option | Type | Default | Notes |
|---|---|---|---|
| caseInsensitive | boolean | false | Lowercases strings and adds i to regex if missing. |maxDepth
| | number | 128 | Evaluation depth guard. |disallowRegex
| | boolean | false | Forces regex(...) to return false. |
- Literals: strings 'abc', numbers 123, booleans true/false, nulluser.address.city
- Identifiers: nested property access and
- Logical: , or, not==
- Comparisons: , !=, >, >=, <, <=in (...)
- Membership: , nin (...)regex(valueExpr, 'pattern', 'flags?')
- Regex:
Precedence: not > comparisons/in/regex > and > or. Use parentheses to group.
`ts`
compile("price >= 10 and price <= 20");
compile("country in ('US','CA') and not user.banned");
compile("regex(email, '@example\\.com$', 'i')");
compile("name == 'Alice' or name == 'Bob'");
`ts
import { compile } from 'filter-expression';
// 1) Numbers and strings
compile('price > 9.99 and currency == "USD"').evaluate({ price: 10, currency: 'USD' }); // true
compile('price <= 5').evaluate({ price: 3 }); // true
compile("name == 'Alice'").evaluate({ name: 'alice' }); // false (case-sensitive by default)
// 2) Case-insensitive mode
compile("name == 'alice'", { caseInsensitive: true }).evaluate({ name: 'Alice' }); // true
compile('country < "b"', { caseInsensitive: true }).evaluate({ country: 'AZ' }); // true (lexicographic)
// 3) Logical precedence and grouping
// not > (comparisons/in/regex) > and > or
compile('true and false or true').evaluate({}); // true => (true and false) or true
compile('not (false or true) and true').evaluate({}); // false
// 4) Membership (IN / NIN)
compile("status in ('new','paid','shipped')").evaluate({ status: 'paid' }); // true
compile('code nin (200, 201, 204)').evaluate({ code: 404 }); // true
compile('num in (1, 2, 3)').evaluate({ num: 2 }); // true
// 5) Regex
compile("regex(email, '@example\\.com$')").evaluate({ email: 'user@example.com' }); // true
compile("regex(msg, '^hello', 'i')").evaluate({ msg: 'Hello world' }); // true
// With caseInsensitive option, an 'i' flag is added implicitly for comparisons and regex
compile("regex(text, 'world')", { caseInsensitive: true }).evaluate({ text: 'Hello World' }); // true
// 6) Null / undefined semantics
// Missing paths resolve to undefined
compile('user.age == null').evaluate({}); // true (both sides nullish)
compile('user.age != null').evaluate({ user: {} }); // false
compile("user.name == 'Alice'").evaluate({}); // false (undefined == 'Alice')
// 7) Numeric coercion where reasonable
// If both sides can be parsed as numbers, numeric comparison is used
compile('age >= "18"').evaluate({ age: 20 }); // true
compile('"10" < 2').evaluate({}); // false (10 < 2 is false)
// 8) Options guards
compile("regex(text, '.*')", { disallowRegex: true }).evaluate({ text: 'any' }); // false (regex disabled)
compile('not not not true', { maxDepth: 10 }).evaluate({}); // works (depth guard for pathological cases)
// 9) Nested property access and safety
compile('user.address.city == "NYC"').evaluate({ user: { address: { city: 'NYC' } } }); // true
// Access to forbidden keys is blocked and treated as undefined
compile('obj.__proto__ == null').evaluate({ obj: {} }); // true
`
- No eval/Function — expressions are tokenized, parsed to an AST, and interpreted.__proto__
- Property access is guarded; forbidden keys , prototype, constructor are blocked.undefined
- Missing paths resolve to , comparisons against null/undefined are predictable.disallowRegex: true
- Optional: disallow regex via .
- Syntax errors include the offending token position.
- Evaluation may throw if maxDepth is exceeded.
- Expressions are compiled once into an AST; evaluation is fast.
- in (...) uses a Set internally for O(1) membership checks.
Example run (Node.js, local laptop), using the built dist with warmup and multiple runs:
`bash`
npm run build
N=100000 WARMUP=1 RUNS=5 node --expose-gc benchmarks/run.mjs
Sample output:
``
compile-once evaluate-many (num range): median 5.63 ms (17,746,884 ops/sec) [min 5.55 ms, max 6.47 ms]
compile-once evaluate-many (age & active): median 7.09 ms (14,107,938 ops/sec) [min 6.75 ms, max 8.01 ms]
compile-once evaluate-many (IN set): median 16.44 ms (6,084,576 ops/sec) [min 16.14 ms, max 18.45 ms]
compile-once evaluate-many (regex i): median 6.97 ms (14,350,976 ops/sec) [min 6.90 ms, max 8.88 ms]
parse+evaluate each time (num range): median 107.60 ms (929,382 ops/sec) [min 106.34 ms, max 108.75 ms]
Notes:
- The primary production pattern is compile-once / evaluate-many.
- You can tune the dataset size and run settings via environment variables: N, WARMUP, RUNS.node --expose-gc
- Use to allow optional GC between runs for more stable medians.
- Node.js ≥ 18
- ESM and CJS via exports map; types in distsideEffects: false
- Tree-shakeable ()
`bash`
git clone
cd filter-expression
npm i
npm run check && npm test && npm run build
Please run tests and lints before submitting PRs.
See GitHub releases or future CHANGELOG.md.
- Strict mode enabled (strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes)..d.ts` declarations.
- ESM and CJS builds with
MIT