filter-expression
v1.0.0
Published
Compact, safe expression DSL for filtering JSON-like data.
Maintainers
Readme
filter-expression (TypeScript)
Compact, safe expression DSL for filtering JSON-like data.
Compile a string expression once and evaluate it against many records — no eval.
Install
npm i filter-expressionQuick start
import { compile } from 'filter-expression';
const expr = compile("user.age >= 18 and country == 'US'");
const ok = expr.evaluate({ user: { age: 22 }, country: 'US' }); // trueAPI
compile(source: string, options?: CompileOptions): CompiledExpressionCompiledExpression.evaluate(record: unknown): boolean
CompileOptionscaseInsensitive?: boolean— lowercases string comparisons and regexmaxDepth?: number— guard against runaway recursion (default 128)disallowRegex?: boolean— makeregex(...)always return false
Features
- Safe by default: no
eval; guarded property access; regex opt-out - Portable DSL: store, transmit, and reuse filters across boundaries
- Practical operators: logical, comparisons,
in/nin, andregex - Predictable null/undefined and typed comparison semantics
- Fast evaluate-many workflow with pre-parsed AST
API reference (compact)
| 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. |
Language
- Literals: strings
'abc', numbers123, booleanstrue/false,null - Identifiers: nested property access
user.address.city - Logical:
and,or,not - Comparisons:
==,!=,>,>=,<,<= - Membership:
in (...),nin (...) - Regex:
regex(valueExpr, 'pattern', 'flags?')
Precedence: not > comparisons/in/regex > and > or. Use parentheses to group.
Examples
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'");Full usage examples
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: '[email protected]' }); // 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: {} }); // trueSafety
- No
eval/Function— expressions are tokenized, parsed to an AST, and interpreted. - Property access is guarded; forbidden keys
__proto__,prototype,constructorare blocked. - Missing paths resolve to
undefined, comparisons againstnull/undefinedare predictable. - Optional: disallow regex via
disallowRegex: true.
Errors
- Syntax errors include the offending token position.
- Evaluation may throw if
maxDepthis exceeded.
Performance
- Expressions are compiled once into an AST; evaluation is fast.
in (...)uses a Set internally for O(1) membership checks.
Benchmarks
Example run (Node.js, local laptop), using the built dist with warmup and multiple runs:
npm run build
N=100000 WARMUP=1 RUNS=5 node --expose-gc benchmarks/run.mjsSample 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. - Use
node --expose-gcto allow optional GC between runs for more stable medians.
Compatibility
- Node.js ≥ 18
- ESM and CJS via
exportsmap; types indist - Tree-shakeable (
sideEffects: false)
Contributing
git clone <this-repo>
cd filter-expression
npm i
npm run check && npm test && npm run buildPlease run tests and lints before submitting PRs.
Changelog
See GitHub releases or future CHANGELOG.md.
TypeScript
- Strict mode enabled (
strict,noUncheckedIndexedAccess,exactOptionalPropertyTypes). - ESM and CJS builds with
.d.tsdeclarations.
License
MIT
