UNPKG

json-expression-eval

Version:

json serializable rule engine / boolean expression evaluator

279 lines (207 loc) 7.8 kB
<p align="center"> <h1 align="center">json-expression-eval</h1> <p align="center"> <strong>A fully typed, JSON-serializable boolean expression evaluator and rule engine for Node.js</strong> </p> </p> <p align="center"> <a href="https://www.npmjs.com/package/json-expression-eval"><img src="https://img.shields.io/npm/v/json-expression-eval.svg?style=flat-square" alt="npm version"></a> <a href="https://www.npmjs.com/package/json-expression-eval"><img src="https://img.shields.io/node/v-lts/json-expression-eval?style=flat-square" alt="node version"></a> <a href="https://github.com/regevbr/json-expression-eval/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/regevbr/json-expression-eval/ci.yml?branch=master&style=flat-square" alt="build status"></a> <a href="https://qlty.sh/gh/regevbr/projects/json-expression-eval"><img src="https://qlty.sh/gh/regevbr/projects/json-expression-eval/coverage.svg" alt="coverage"></a> <a href="https://qlty.sh/gh/regevbr/projects/json-expression-eval"><img src="https://qlty.sh/gh/regevbr/projects/json-expression-eval/maintainability.svg" alt="maintainability"></a> <a href="https://snyk.io/test/github/regevbr/json-expression-eval?targetFile=package.json"><img src="https://snyk.io/test/github/regevbr/json-expression-eval/badge.svg?targetFile=package.json" alt="vulnerabilities"></a> </p> --- ## Why json-expression-eval? Need to store complex business rules in a database? Want to evaluate dynamic conditions without `eval()`? This library lets you: - **Serialize expressions as JSON** - Store rules in databases, send them over APIs - **100% type-safe** - Full TypeScript support with compile-time validation - **Extensible** - Add custom functions for domain-specific logic - **Debug easily** - `evaluateWithReason` tells you exactly *why* an expression matched --- ## Table of Contents - [Installation](#-installation) - [Quick Start](#-quick-start) - [Expression Syntax](#-expression-syntax) - [Operators](#-operators) - [Custom Functions](#-custom-functions) - [Rule Engine](#-rule-engine) - [API Reference](#-api-reference) --- ## Installation ```sh # npm npm install json-expression-eval # yarn yarn add json-expression-eval # pnpm pnpm add json-expression-eval ``` --- ## Quick Start ```typescript import { evaluate } from 'json-expression-eval'; // Define your context (the data to evaluate against) const context = { user: { age: 25, role: 'admin' }, subscription: { plan: 'pro', daysLeft: 15 } }; // Define your expression (can be stored as JSON!) const expression = { and: [ { 'user.age': { gte: 18 } }, { 'user.role': 'admin' }, { 'subscription.daysLeft': { gt: 0 } } ] }; // Evaluate const result = await evaluate(expression, context, {}); console.log(result); // true ``` --- ## Expression Syntax Expressions are plain JavaScript objects that can be serialized to JSON. They support: ### Logical Operators | Operator | Description | Example | |----------|-------------|---------| | `and` | All conditions must be true | `{ and: [expr1, expr2] }` | | `or` | At least one condition must be true | `{ or: [expr1, expr2] }` | | `not` | Negates the condition | `{ not: expr }` | ### Comparison Operators | Operator | Description | Example | |----------|-------------|---------| | `eq` | Equal (`===`) | `{ age: { eq: 25 } }` | | `neq` | Not equal (`!==`) | `{ status: { neq: 'inactive' } }` | | `gt` | Greater than | `{ price: { gt: 100 } }` | | `gte` | Greater than or equal | `{ age: { gte: 18 } }` | | `lt` | Less than | `{ quantity: { lt: 10 } }` | | `lte` | Less than or equal | `{ score: { lte: 100 } }` | | `inq` | In array | `{ color: { inq: ['red', 'blue'] } }` | | `nin` | Not in array | `{ status: { nin: ['banned', 'suspended'] } }` | | `between` | Between two values (inclusive) | `{ age: { between: [18, 65] as const } }` | | `regexp` | Matches regex | `{ email: { regexp: '^.*@gmail\\.com$' } }` | | `regexpi` | Matches regex (case-insensitive) | `{ name: { regexpi: '^john' } }` | | `exists` | Check if value exists (not null/undefined) | `{ middleName: { exists: true } }` | ### Shorthand Syntax For simple equality checks, you can omit the operator: ```typescript // These are equivalent: { userId: { eq: 'abc123' } } { userId: 'abc123' } ``` ### Nested Properties Access nested properties using dot notation: ```typescript { 'user.address.city': 'New York' } { 'order.items.0.price': { gt: 50 } } ``` --- ## Operators ### Reference Values Compare a property against another property in the context: ```typescript { 'cart.total': { lte: { ref: 'user.balance' } // cart.total <= user.balance } } ``` ### Math Operations Perform calculations in comparisons: ```typescript { 'order.total': { lte: { op: '*', // Multiply lhs: { ref: 'user.credit' }, // Left-hand side rhs: 0.9 // Right-hand side (10% discount) } } } ``` **Available operations:** `+`, `-`, `*`, `/`, `%`, `pow` --- ## Custom Functions Add domain-specific logic with custom functions: ```typescript import { evaluate, Expression, EvaluatorFuncRunOptions } from 'json-expression-eval'; // Define your function table type type MyFunctions = { isPremiumUser: ( minPurchases: number, ctx: { purchases: number }, opts: EvaluatorFuncRunOptions<{}> ) => Promise<boolean>; }; // Implement the functions const functions: MyFunctions = { isPremiumUser: async (minPurchases, ctx) => { return ctx.purchases >= minPurchases; } }; // Use in expressions const expression: Expression<MyContext, MyFunctions> = { and: [ { isPremiumUser: 10 }, // Custom function call { 'subscription.active': true } ] }; await evaluate(expression, context, functions); ``` --- ## Rule Engine For more complex scenarios, use the rule engine to evaluate multiple rules and get detailed results: ```typescript import { RulesEngine, Rule } from 'json-expression-eval'; type MyPayload = { discountPercent: number }; const rules: Rule<MyPayload, ...>[] = [ { condition: { and: [ { 'user.tier': 'gold' }, { 'cart.total': { gte: 100 } } ] }, consequence: { message: 'Gold member discount applied!', custom: { discountPercent: 20 } } }, { condition: { 'cart.total': { gte: 50 } }, consequence: { message: 'Bulk discount applied!', custom: { discountPercent: 10 } } } ]; const engine = new RulesEngine(functions, ruleFunctions); const results = await engine.evaluateAll(rules, context, {}); // Returns all matching rules with their consequences ``` --- ## API Reference ### Core Functions | Function | Description | |----------|-------------| | `evaluate(expr, ctx, funcs, opts?)` | Evaluate expression, returns `boolean` | | `evaluateWithReason(expr, ctx, funcs, opts?)` | Evaluate and return the minimal matching sub-expression | | `validate(expr, ctx, funcs, opts?)` | Validate expression structure against a context | ### Classes | Class | Description | |-------|-------------| | `ExpressionHandler` | Reusable expression evaluator instance | | `RulesEngine` | Rule engine for evaluating multiple rules | ### `evaluateWithReason` Behavior Returns `{ result: boolean, reason: Expression }` where `reason` is the minimal sub-expression that determined the result: - **`or` = true**: Returns `{or: [firstTrueExpr]}` - **`or` = false**: Returns `{or: [allExprs]}` (all failed) - **`and` = false**: Returns `{and: [firstFalseExpr]}` - **`and` = true**: Returns `{and: [allExprs]}` (all passed) --- ## Examples For more examples, see the [`/src/examples`](./src/examples) and [`/src/test`](./src/test) directories. --- ## License MIT - See [LICENSE](./LICENSE) for details.