@usex/rule-engine

repository·master·Indexed 18 days ago

https://github.com/ali-master/rule-engine

A high-performance, JSON-based rule engine for TypeScript applications, compatible with Node.js, browsers, and edge environments. The monorepo includes a core engine and @usex/rule-engine-builder, a visual rule constructor for React with support for Next.js and Vite. It features components like TreeRuleBuilder and RuleEvaluator, integrates with state management libraries such as Zustand and Redux Toolkit, and supports REST, GraphQL, tRPC, and Firebase backends.

Tokens
87.8K
Snippets
224
Records
287
Agent score
63%

What's inside @usex/rule-engine

  1. Key Features of @usex/rule-engine

    master

    The @usex/rule-engine provides several advanced capabilities for rule processing:

    • JSONPath Support: Access nested properties within your data using $.path.to.field syntax.
    • 126+ Operators: A wide range of operators including Comparison, String, Numeric, Array, Date/Time, Type, Existence, Boolean, Pattern, and Persian-specific operators.
    • Type Safety: Full TypeScript support utilizing generics for robust rule definitions.
    • Builder Pattern: A fluent API available via @usex/rule-engine-builder for programmatic rule construction.
    • Async Support: All evaluation processes are Promise-based.
    • Advanced Processing: Supports batch processing, rule introspection (analyzing rules for possible inputs), and data mutations before evaluation.
    • Environment Agnostic: Works seamlessly in both Node.js and browser environments with zero dependencies in the core package.
  2. Compare @usex/rule-engine with other rule engines

    master

    Use the comparison documentation to evaluate @usex/rule-engine against alternatives like json-rules-engine, node-rules, nools, and drools.

    Key advantages of @usex/rule-engine include:

    • Visual Builder: Built-in React components for rule creation.
    • Rich Operator Set: Over 121 built-in operators (including extensive Date/Time, Array, String, and Type validation).
    • TypeScript Native: Full type safety with generics.
    • JSONPath Support: Native support for self-referencing and complex data paths.
    • Browser Ready: Zero dependencies and small bundle size (42KB).
    • Developer Experience: Built-in history/undo, real-time testing, and debug mode.
  3. How RuleEvaluator evaluation works

    master

    The RuleEvaluator follows a specific lifecycle to provide feedback:

    1. Engine Integration: It uses the @usex/rule-engine package to perform the actual rule evaluation.
    2. Subscription: It subscribes to the enhanced rule store to detect rule updates.
    3. Evaluation Trigger: In live mode, it automatically re-evaluates whenever rules change. In manual mode, it waits for user input.
    4. Recursive Breakdown: The component evaluates conditions recursively, providing a hierarchical view that includes:
      • Condition type (e.g., AND, OR, NONE).
      • Number of sub-conditions.
      • Individual constraint results.
      • Actual vs. expected values.
      • Custom error messages.
    5. Visual Feedback: Results are communicated via green (pass) and red (fail) indicators with smooth Framer Motion animations.
  4. Common Rule Patterns in @usex/rule-engine

    master

    Use these patterns to implement common logic within your JSON rules:

    1. Dynamic Field References (JSONPath)

    Reference other fields in your data using $.path.to.field syntax in both field and value.

    {
      conditions: {
        and: [{ field: '$.current.price', operator: 'less-than', value: '$.maximum.price' }]
      }
    }

    2. Complex Nested Conditions

    Convert nested if/else or complex boolean logic into nested and/or blocks.

    // (A && B) || (C && D) || E
    {
      conditions: {
        or: [
          { and: [conditionA, conditionB] },
          { and: [conditionC, conditionD] },
          conditionE
        ]
      }
    }

    3. Array Operations

    Use built-in operators for array content validation.

    {
      conditions: {
        and: [
          { field: 'tags', operator: 'contains-any', value: ['urgent', 'critical'] },
          { field: 'assignees', operator: 'not-empty', value: true }
        ]
      }
    }

    4. Date Comparisons

    Use specialized date operators for time-based logic.

    {
      conditions: {
        and: [
          { field: 'subscription.expiresAt', operator: 'date-after-now', value: true },
          { field: 'lastPayment', operator: 'date-between', value: ['2025-01-01', '2025-12-31'] }
        ]
      }
    }

    5. Validation Rules

    Include a message field in your conditions to handle validation feedback.

    {
      conditions: {
        and: [
          { field: 'username', operator: 'matches', value: '^[a-z]{3,20}$', message: 'Invalid username' }
        ]
      }
    }
  5. How rules, conditions, and constraints work together

    master

    The engine operates on three hierarchical levels:

    1. Rules: The top-level object. It contains conditions (the logic to check) and an optional default value to return if no conditions match.
    2. Conditions: Logical building blocks that allow grouping. They use boolean logic keys like and (all must match), or (any must match), or none (none must match). A condition can contain either a Constraint or another nested Condition.
    3. Constraints: The smallest unit of evaluation. A constraint specifies a field (using JSONPath), an operator (the comparison logic), and a value (the target for comparison).
  6. Understand the Rule Evaluation Pipeline

    master

    When a rule is evaluated, it passes through a multi-stage pipeline to ensure accuracy and performance:

    1. Input Reception: Receives the rule and the criteria data.
    2. Mutation Phase: Applies registered mutations to transform the criteria data.
    3. Validation Phase: Validates the rule structure, checks operator validity, and verifies field paths.
    4. Resolution Phase: Resolves JSONPath expressions and extracts field values (including nested objects).
    5. Evaluation Phase: Applies constraints and evaluates logical conditions to aggregate results.
    6. Result Composition: Returns the final EvaluationResult.
  7. How TypeScript Generics work with RuleEngine

    master

    The RuleEngine is designed with TypeScript generics to ensure type safety for both the input data and the rule results.

    1. Rule<TResult>: Defines a rule where the result and default fields must match the shape of TResult.
    2. Rule<TResult, TInput>: Defines a rule with specific types for both the output (TResult) and the input data (TInput) used during evaluation.
    3. RuleEngine.evaluate<TResult, TInput>(rule, data): When evaluating, passing these generics ensures that the returned result.value is correctly typed.
    interface DiscountResult {
      discount: number;
      code: string;
      description: string;
    }
    
    // Generic rule with full type inference
    const discountRule: Rule<DiscountResult> = {
      conditions: [
        {
          and: [
            { field: "$.user.tier", operator: "equals", value: "premium" },
            { field: "$.order.total", operator: "greater-than", value: 100 }
          ],
          result: {
            discount: 0.20,
            code: "PREMIUM20",
            description: "20% premium discount"
          }
        }
      ],
      default: { discount: 0, code: "", description: "No discount" }
    };
    
    // Evaluation with full type inference
    const result = await RuleEngine.evaluate<DiscountResult>(discountRule, orderData);
    // result.value is automatically typed as DiscountResult
  8. Use overloaded methods for automatic type inference

    master

    The Rule Engine uses TypeScript method overloads to automatically infer return types based on whether you are processing a single object or an array. This reduces the need for manual type guards.

    Evaluator Class

    • evaluate(): Returns EvaluationResult<T> for a single object, or Array<EvaluationResult<T>> for arrays.

    RuleEngine Class

    • evaluate(): Async evaluation with type inference.
    • checkIsPassed(): Returns boolean for a single item, or boolean | boolean[] for arrays.
    • getEvaluateResult(): Returns T for a single item, or T[] for arrays.
    • evaluateMany(): (Renamed from evaluateMultiple) Handles both single and array criteria.

    Note: All instance methods are also available as static methods with matching overloads.

    ObjectDiscovery Class

    • resolveProperty(), updateProperty(), and resolveTextPathExpressions(): All support both single objects and arrays.
  9. Extend the Rule Engine with Plugins

    master

    The rule engine supports a plugin system via the RuleEnginePlugin interface. Plugins can hook into the engine's lifecycle and extend its core capabilities by providing new operators, mutations, or validators.

    Lifecycle Hooks:

    • onInit(engine: RuleEngine): Called when the plugin is registered.
    • beforeEvaluate(rule: RuleType, criteria: any): Executed before a rule is evaluated.
    • afterEvaluate(result: EvaluationResult): Executed after evaluation completes.

    Extension Points:

    • operators: A record of OperatorDefinition objects.
    • mutations: A record of MutationFunction objects.
    • validators: A record of ValidatorFunction objects.
    interface RuleEnginePlugin {
      name: string;
      version: string;
      
      // Lifecycle hooks
      onInit?(engine: RuleEngine): void;
      beforeEvaluate?(rule: RuleType, criteria: any): void;
      afterEvaluate?(result: EvaluationResult): void;
      
      // Extension points
      operators?: Record<string, OperatorDefinition>;
      mutations?: Record<string, MutationFunction>;
      validators?: Record<string, ValidatorFunction>;
    }
  10. Understand the Rule Engine System Architecture

    master

    The Rule Engine is organized into a layered architecture designed to support different application types:

    1. Application Layer: Where your code lives (React Apps, Node.js Apps, or API Services).
    2. Rule Engine SDK Layer: The middleware layer providing core functionality:
      • UI Builder (@usex/builder): For visual rule construction in React applications.
      • Rule Engine (@usex/core): The programmatic engine for Node.js or API services.
      • Validators & Utils: Shared logic for rule integrity.

    This separation allows you to use the same rule logic in both your frontend (for user interaction) and your backend (for enforcement).

  11. Understand Rule and Condition structures

    master

    Rules are composed of conditions and an optional default value.

    Rule Interface

    interface Rule {
      conditions: Condition | Condition[];  // What to evaluate
      default?: any;                       // Fallback if no match
    }

    Logical Operators (Conditions)

    Conditions group Constraint objects using logical operators:

    • and: All constraints in the array must match.
    • or: Any constraint in the array must match.
    • none: None of the constraints in the array must match.

    Constraint Interface

    Constraints are the basic building blocks of a rule:

    interface Constraint {
      field: string;      // The field to check (supports JSONPath)
      operator: string;   // The comparison operator
      value?: any;        // The value to compare against
      message?: string;   // Optional validation message
    }
  12. Understand RuleEngine data structures

    master

    Rules in @usex/rule-engine are composed of three main layers:

    1. RuleType: The top-level container holding conditions (an array or single condition) and an optional default value.
    2. Condition: A logical grouping that uses and, or, or none to wrap an array of Constraint or nested Condition objects. It also holds the result to return if the condition is met.
    3. Constraint: The smallest unit of evaluation, consisting of a field, an operator, a value, and an optional message.
    interface RuleType<R> {
      conditions: Condition<R> | Condition<R>[];
      default?: R;
    }
    
    interface Condition<R> {
      or?: Array<Constraint | Condition<R>>;
      and?: Array<Constraint | Condition<R>>;
      none?: Array<Constraint | Condition<R>>;
      result?: R;
    }
    
    interface Constraint {
      field: string;
      operator: string;
      value: any;
      message?: string;
    }