json-rules-engine

repository·master·Indexed 25 days ago

https://github.com/cachecontrol/json-rules-engine

A lightweight, isomorphic (Node and Browser) rules engine that uses JSON structures to define logic. It is designed to be fast, secure (no eval()), and extensible, allowing users to define rules with conditions and events, implement asynchronous facts, and utilize an Almanac for caching and fact dependency management. Version 7.3.2.

Tokens
12.1K
Snippets
31
Records
51
Agent score
83%

What's inside json-rules-engine

  1. Understand the Almanac

    master

    The Almanac is a collection of facts gathered during an engine run cycle. As the engine computes fact values, the results are stored and cached in the almanac. If the engine detects a fact computation has been previously computed, it reuses the cached result.

    A new almanac is instantiated every time engine.run() is invoked. The almanac is available as an argument to fact evaluation methods and to the engine's success event.

  2. Define Facts in json-rules-engine

    master

    Facts are constants or functions registered with the engine that provide data for rule conditions. Facts can be simple constant values or dynamic functions that compute values at runtime.

    Dynamic fact functions receive two arguments:

    1. params: An object containing data passed to the fact during evaluation.
    2. almanac: An instance of the Almanac, allowing facts to access other facts or data within the engine context.

    Fact functions should be pure and can return either a computed value or a Promise that resolves to a value.

    // constant value facts
    let fact = new Fact('apiKey', '4feca34f9d67e99b8af2')
    
    // dynamic facts
    let fact = new Fact('account-type', (params, almanac) => {
      // ...
    })
  3. Compare facts against other facts

    master

    Instead of comparing a fact to a static value, you can compare a fact to the result of another fact by nesting the second fact inside the value property of the condition.

    The nested fact can also use its own params and path helpers.

    // Compares 'product-price' against the 'budget' fact
    let rule = new Rule({
      conditions: {
        all: [
          {
            fact: 'product-price',
            params: {
              productId: 'widget',
              path: '$.price'
            },
            operator: 'greaterThan',
            value: {
              fact: 'budget' // The comparison value is another fact
            }
          }
        ]
      }
    })
  4. Define Boolean logic in conditions

    master

    Rule conditions are evaluated using boolean operators. Every condition set must start with one of these operators at its root:

    • all: An array of conditions that must all be truthy for the rule to succeed.
    • any: An array of conditions where at least one must be truthy for the rule to succeed.
    • not: A single condition that must be falsy for the rule to succeed.

    These operators can be nested to create complex logic.

  5. Implement fact dependencies using the Almanac

    master

    To optimize performance, define 'base' facts that load data and have subsequent facts depend on them using almanac.factValue(). This allows the engine to leverage caching so that the base data is only fetched once per unique set of parameters, even if multiple facts reference it.

    Example pattern:

    1. Define a base fact (e.g., account-information) that performs an API call.
    2. Define dependent facts (e.g., is-funded-account) that call almanac.factValue('account-information', ...) to access the cached data.
    /*
     * Base fact for retrieving account data information.
     * Engine will automatically cache results by accountId
     */
    let accountInformation = new Fact('account-information', function(params, almanac) {
       return request
          .get({ url: `http://my-service/account/${params.accountId}`})
          .then(function (response) {
             return response.data
          })
    })
    
    /*
     * Calls the account-information fact with the appropriate accountId.
     * Receives a promise w/results unique to the accountId
     */
    let isFundedAccount = new Fact('is-funded-account', function(params, almanac) {
       return almanac.factValue('account-information', { accountId: params.accountId }).then(info => {
         return info.funded === true
       })
    })
    
    /*
     * Calls the account-information fact with the appropriate accountId.
     * Receives a promise w/results unique to the accountId
     */
    let accountBalance = new Fact('account-balance', function(params, almanac) {
       return almanac.factValue('account-information', { accountId: params.accountId }).then(info => {
         return info.balance
       })
    })
    
    engine.addFact(accountInformation)
    engine.addFact(isFundedAccount)
    engine.addFact(accountBalance)
    
    engine.run({ accountId: 1 })
  6. Persist and restore rules via JSON

    master

    Rules can be converted to a JSON string for storage in a database or file system using rule.toJSON(). To restore a rule, pass the JSON string into the Rule constructor.

    Note: fact methods are not part of the serialized JSON because they contain application-specific business logic and cannot be safely serialized/deserialized via eval().

    // save somewhere...
    let jsonString = rule.toJSON()
    
    // ...later:
    let rule = new Rule(jsonString)
  7. Use condition helpers: params and path

    master

    To make fact handlers more reusable and handle complex data, use params and path within a condition.

    • params: Passes an object to the fact handler. This allows a single fact handler to behave differently based on the input (e.g., loading different products by ID).
    • path: Uses JSONPath syntax to traverse the data returned by a fact handler. This prevents the need to write separate fact handlers for every nested property.

    Note: JSONPath support is provided by jsonpath-plus.

    // Fact handler returns a whole object
    engine.addFact('product-price', function (params, almanac) {
      return productLoader(params.productId)
    })
    
    // Rule uses 'params' to specify which product and 'path' to pick the price
    let rule = new Rule({
      conditions: {
        all: [
          {
            fact: 'product-price',
            path: '$.price',
            params: {
              productId: 'widget'
            },
            operator: 'greaterThan',
            value: 100
          }
        ]
      }
    })
  8. Perform rule chaining via runtime facts

    master

    Rule chaining allows the outcome of one rule to define a fact that subsequent rules can use.

    Steps to implement:

    1. Define a rule with a high priority.
    2. In its onSuccess or onFailure handler, use almanac.addFact() (formerly addRuntimeFact) to set a new fact value.
    3. Define subsequent rules with a lower priority that include the newly created fact in their conditions.

    Note: almanac.addRuntimeFact() is deprecated. Use almanac.addFact() instead.

    engine.addRule({
      conditions,
      event,
      onSuccess: function (event, almanac) {
        almanac.addFact('rule-1-passed', true) // track that the rule passed
      },
      onFailure: function (event, almanac) {
        almanac.addFact('rule-1-passed', false) // track that the rule failed
      },
      priority: 10 // a higher priority ensures this rule will be run prior to subsequent rules
    })
    
    // in a later rule:
    engine.addRule({
      conditions: {
        all: [{
          fact: 'rule-1-passed',
          operator: 'equal',
          value: true
        }]
      },
      priority: 1 // lower priority ensures this is run AFTER its predecessor
    })
  9. Reference facts in event parameters

    master

    By setting the engine option replaceFactsInEventParams: true, you can include references to facts within your event parameters. These references will be replaced with the actual value of the fact before the event is emitted. The reference format follows the same structure as comparing facts, using fact, path, and optional params.

    const engine = new Engine([], { replaceFactsInEventParams: true });
    engine.addRule({
        conditions: { /* ... */ },
        event: {
          type: "gameover",
          params: {
            initials: {
              fact: "currentHighScore",
              path: "$.initials",
              params: { foo: 'bar' }
            }
          }
        }
      })