{"id":14483590,"url":"https://github.com/akmjenkins/json-schema-rules-engine","last_synced_at":"2025-04-12T00:33:53.856Z","repository":{"id":42494067,"uuid":"402612335","full_name":"akmjenkins/json-schema-rules-engine","owner":"akmjenkins","description":"A highly configurable and dynamic rules engine based on JSON Schema","archived":false,"fork":false,"pushed_at":"2023-07-19T04:01:48.000Z","size":851,"stargazers_count":45,"open_issues_count":5,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-18T09:04:59.368Z","etag":null,"topics":["business-rules","engine","json","json-rules-engine","json-schema","json-schema-validator","rule-engine","rules","rules-engine","rules-processor"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/akmjenkins.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-03T01:37:00.000Z","updated_at":"2024-09-10T05:44:13.000Z","dependencies_parsed_at":"2024-09-03T00:06:22.003Z","dependency_job_id":"fc44e527-a993-4db6-a623-c17e32450598","html_url":"https://github.com/akmjenkins/json-schema-rules-engine","commit_stats":{"total_commits":78,"total_committers":2,"mean_commits":39.0,"dds":0.08974358974358976,"last_synced_commit":"e516eaa739bdd12a412bf0842306269753871784"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akmjenkins%2Fjson-schema-rules-engine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akmjenkins%2Fjson-schema-rules-engine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akmjenkins%2Fjson-schema-rules-engine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/akmjenkins%2Fjson-schema-rules-engine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/akmjenkins","download_url":"https://codeload.github.com/akmjenkins/json-schema-rules-engine/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248501696,"owners_count":21114676,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["business-rules","engine","json","json-rules-engine","json-schema","json-schema-validator","rule-engine","rules","rules-engine","rules-processor"],"created_at":"2024-09-03T00:01:53.532Z","updated_at":"2025-04-12T00:33:53.833Z","avatar_url":"https://github.com/akmjenkins.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# JSON Schema Rules Engine\n\n[![npm version](https://img.shields.io/npm/v/json-schema-rules-engine)](https://npmjs.org/package/json-schema-rules-engine)\n[![codecov](https://codecov.io/gh/akmjenkins/json-schema-rules-engine/branch/main/graph/badge.svg)](https://codecov.io/gh/akmjenkins/json-schema-rules-engine)\n![Build Status](https://github.com/akmjenkins/json-schema-rules-engine/actions/workflows/test.yaml/badge.svg)\n[![Bundle Phobia](https://badgen.net/bundlephobia/minzip/json-schema-rules-engine)](https://bundlephobia.com/result?p=json-schema-rules-engine)\n\nA highly configurable rules engine based on [JSON Schema](https://json-schema.org/). Inspired by the popular [JSON rules engine](https://github.com/CacheControl/json-rules-engine).\n\n_NBD: It actually doesn't **have** to use JSON Schema, but it's suggested_\n\n## Preface\n\nLots of rules engines use custom predicates, or predicates available from other libraries. [json-rules-engine](https://github.com/CacheControl/json-rules-engine) uses custom [Operators](https://github.com/CacheControl/json-rules-engine/blob/master/src/engine-default-operators.js) and [json-rules-engine-simplified](https://github.com/RxNT/json-rules-engine-simplified) uses the [predicate](https://github.com/landau/predicate) library. One thing that seems to have gotten missed is that **a json schema _IS_ a predicate** - a subject will either validate against a JSON schema, or it won't. Therefore, the only thing you need to write rules is a schema validator, no other dependencies needed. The other benefit of this is that if you need to use a new operator, your dependency on this library doesn't change. You either get that logic for free when the JSON Schema specification updates, or you add that operator to your [validator](#validator), but not to this rules engine itself.\n\nThis library doesn't do a whole lot - it just has an opinionated syntax to make rules human readable - which is why it's less than 2kb minzipped. You just need to bring your own validator (may we suggest [Ajv](https://github.com/ajv-validator/ajv)?) and write your rules.\n\n## Why?\n\nThree reasons:\n\n1. A JSON schema **is a predicate**\n2. Tools for JSON schema are everywhere and support is wide\n3. No dependency on second or third-party packages for logical operators. You get whatever is in the JSON schema specification, or whatever you decide to support in your [validator](#validator).\n\n## Features\n\n- Highly configurable - use any type of schema to express your logic (we strongly suggest JSON Schema)\n- Configurable interpolation to make highly reusable rules/actions\n- Zero-dependency, extremely lightweight (under 2kb minzipped)\n- Runs everywhere\n- Nested conditions allow for controlling rule evaluation order\n- [Memoization makes it fast](#memoization)\n- No thrown errors - errors are emitted, never thrown\n\n## Installation\n\n```bash\nnpm install json-schema-rules-engine\n# or\nyarn add json-schema-rules-engine\n```\n\nor, use it directly in the browser\n\n```html\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/json-schema-rules-engine\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  const engine = jsonSchemaRulesEngine(validator, {\n    facts,\n    actions,\n    rules,\n  });\n\u003c/script\u003e\n```\n\n## Basic Example\n\n```js\nimport Ajv from 'ajv';\nimport createRulesEngine from 'json-schema-rules-engine';\n\nconst facts = {\n  weather: async ({ query, appId, units }) =\u003e {\n    const url = `https://api.openweathermap.org/data/2.5/weather/?q=${q}\u0026units=${units}\u0026appid=${appId}`;\n    return (await fetch(url)).json();\n  },\n};\n\nconst rules = {\n  dailyTemp: {\n    when: [\n      {\n        weather: {\n          params: {\n            query: '{{city}}',\n            appId: '{{apiKey}}',\n            units: '{{units}}',\n          },\n          path: 'main.temp',\n          is: {\n            type: 'number',\n            minimum: '{{hotTemp}}',\n          },\n        },\n      },\n    ],\n    then: {\n      actions: [\n        {\n          type: 'log',\n          params: { message: 'Quite hot out today!' },\n        },\n      ],\n    },\n    otherwise: {\n      actions: [\n        {\n          type: 'log',\n          params: { message: 'Brrr, bundle up!' },\n        },\n      ],\n    },\n  },\n};\n\nconst actions = {\n  log: console.log,\n};\n\n// validate using a JSON schema via AJV\nconst ajv = new Ajv();\nconst validator = async (subject, schema) =\u003e {\n  const validate = await ajv.compile(schema);\n  const result = validate(subject);\n  return { result };\n};\n\nconst engine = createRulesEngine(validator, { facts, rules, actions });\n\nengine.run({\n  hotTemp: 20,\n  city: 'Halifax',\n  apiKey: 'XXXX',\n  units: 'metric',\n});\n\n// check the console\n```\n\n## Concepts\n\n- [Validator](#validator)\n- [Context](#context)\n- [Facts](#facts)\n- [Actions](#actions)\n- [Rules](#rules)\n  - [Nesting](#nesting-rules)\n  - [FactMap](#factmap)\n  - [Evaluator](#evaluator)\n- [Resolver](#resolver)\n- [Interpolation](#interpolation)\n  - [Results Context](#results-context)\n- [Events](#events)\n\n## Validator\n\nThe validator is what makes `json-schema-rules-engine` so powerful. The validator is passed the resolved fact value and the schema (the value of the `is` property of an [`evaluator`](#evaluator)) and asynchronously returns a `ValidatorResult`:\n\n```ts\ntype ValidatorResult = {\n  result: boolean;\n};\n```\n\nIf you want to use `json-schema-rules-engine` as was originally envisioned - to allow encoding of boolean logic by means of JSON Schema - then this is a great validator to use:\n\n```js\nimport Ajv from 'Ajv';\nconst ajv = new Ajv();\nconst validator = async (subject, schema) =\u003e {\n  const validate = await ajv.compile(schema);\n  const result = validate(subject);\n  return { result };\n};\n\nconst engine = createRulesEngine(validator);\n```\n\nYou can see by abstracting the JSON Schema part away from the core rules engine (by means of the `validator`) this engine can actually use **anything** to evaluate a property against. The validator is why `json-schema-rules-engine` is so small and so powerful.\n\n### Context\n\n`context` is the name of the object the rules engine evaluates during `run`. It can be used for interpolation or even as a source of facts\n\n```js\nconst context = {\n  hotTemp: 20,\n  city: 'Halifax',\n  apiKey: 'XXXX',\n  units: 'metric',\n};\n\nengine.run(context);\n```\n\n### Facts\n\nThere are two types of facts - static and functional. Functional facts come from the facts given to the rule engine when it is created (or via [setFacts](`setFacts`)). They are unary functions that return a value, synchronously or asynchronously. Check out this example weather fact that calls an the [openweather api](https://openweathermap.org/api) and returns the JSON response.\n\n```js\nconst weather = async ({ query, appId, units }) =\u003e {\n  const url = `https://api.openweathermap.org/data/2.5/weather/?q=${q}\u0026units=${units}\u0026appid=${appId}`;\n  return (await fetch(url)).json();\n};\n```\n\nStatic facts are simply the values of the context object\n\n#### Memoization\n\nIt's important to note that all functional facts are memoized during an individual run of the rule engine - **but not between runs** - based on **shallow equality** of their argument. This is to ensure that the same functional fact can be evaluated in multiple rules without that fact being called more than once (useful for aysnchronous facts to prevent multiple API calls).\n\nThis means that functions that accept an argument that contains values that are objects or arrays **are not memoized by default**. But this can be configured using something like [lodash's isEqual](https://lodash.com/docs/4.17.15#isEqual)\n\n```js\nimport _ from 'lodash';\n\nconst engine = createRulesEngine(validator, { memoizer: _.isEqual });\n```\n\nIf you want any of your facts to be memoized **between** runs, feel free to use our memoization helpers before setting the facts\n\n```js\nimport _ from 'lodash';\nimport { memo, memoRecord } from 'json-schema-rules-engine/memo';\n\n// memoize a single function\nconst memoizedFunction = memo((...args) =\u003e {\n  /* ... */\n});\n\n// deep equal memoize\nconst deeplyMemoizedFunction = memo((...args) =\u003e {\n  /* ... */\n}, _.isEqual);\n\n// memoize an object whos values are functions\nconst memoizedFacts = memoRecord({\n  weather: async (...args) =\u003e {\n    /* ... */\n  },\n});\n\nconst deeplyMemoizedFacts = memoRecord(\n  {\n    weather: async (...args) =\u003e {\n      /* ... */\n    },\n  },\n  _.isEqual,\n);\n\nengine.setFacts(memoizedFacts);\n```\n\nIf, for some reason, you do not want facts to be memoized during a run, then you can just pass a stub memoizer:\n\n```js\nconst engine = createRulesEngine(validator, { memoizer: () =\u003e false });\n```\n\n### Actions\n\nActions, just like facts, are unary functions. They can be sync or async and can do anything. They are executed as an outcome of a rule.\n\n```js\nconst saveAuditRecord = async ({ eventType, data }) =\u003e {\n  await db.insert('INSERT INTO audit_log (event, data) VALUES(?,?)', [\n    eventType,\n    data,\n  ]);\n};\n\nconst engine = createRulesEngine(validator, { actions: saveAuditRecord });\n```\n\n### Rules\n\nRules are written as **when**, **then**, **otherwise**. A when clause consists of an array of [`FactMap`s](#factmap), or an object whose values are [`FactMap`s](#factmap). If any of the `FactMap`s in the object or array evaluate to true, the properties of the `then` clause of the rule are evaluated. If not, the `otherwise` clause is evaluated.\n\n```js\nconst myRule = {\n  when: [\n    {\n      age: {\n        is: {\n          type: 'number',\n          minimum: 30,\n        },\n      },\n      name: {\n        is: {\n          type: 'string',\n          pattern: '^J',\n        },\n      },\n    },\n  ],\n  then: {\n    actions: [\n      {\n        type: 'log',\n        params: {\n          message: 'Hi {{name}}!',\n        },\n      },\n    ],\n  },\n};\n\nconst engine = createRulesEngine(validator, { rules: { myRule } });\nengine.run({ age: 31, name: 'Fred' }); // no action is fired\nengine.run({ age: 32, name: 'Joe' }); // fires the log action with { message: 'Hi Joe!' }\n```\n\n#### Nesting Rules\n\nThe `then` or `otherwise` property can consist of either `actions`, but it can also contain a nested rule. All functional facts in all [FactMaps](#factmaps) are evaluated simultaneously. By nesting `when`'s, you can cause facts to be executed serially.\n\n```js\nconst myRule = {\n  when: [\n    {\n      weather: {\n        params: {\n          query: '{{city}}',\n          appId: '{{apiKey}}',\n          units: '{{units}}',\n        },\n        path: 'main.temp',\n        is: {\n          type: 'number',\n          minimum: 30\n        }\n      },\n    },\n  ],\n  then: {\n    when: [\n      {\n        forecast: {\n          params: {\n            appId: '{{apiKey}}',\n            coord: '{{results[0].weather.value.coord}}' // interpolate a value returned from the first fact\n          },\n          path: 'daily',\n          is: {\n            type: 'array',\n            contains: {\n              type: 'object',\n              properties: {\n                temp: {\n                  type: 'object',\n                  properties: {\n                    max: {\n                      type: 'number',\n                      minimum: 20\n                    }\n                  }\n                }\n              }\n            },\n            minContains: 4\n          }\n        }\n      },\n      then: {\n        actions: {\n          type: 'log',\n          params: {\n            message: 'Nice week of weather coming up',\n          }\n        }\n      }\n    ],\n    actions: [\n      {\n        type: 'log',\n        params: {\n          message: 'Warm one today',\n        },\n      },\n    ],\n  },\n};\n```\n\n#### FactMap\n\nA FactMap is a plain object whose keys are facts (static or functional) and values are [`Evaluator`'s](#evaluator).\n\n#### Evaluator\n\nAn evaluator is an object that specifies a JSON Schema to evaluate a fact against. If the fact is a functional fact, the evaluator can specify params to pass to the fact as an argument. A `path` can also be specified to more easily evaluate a nested property contained within the fact.\n\nThe following weather fact evaluator passes parameters to the function and specifies a schema to check the value at `main.temp` against:\n\n```js\nconst myFactMap = {\n  weather: {\n    params: {\n      query: '{{city}}',\n      appId: '{{apiKey}}',\n      units: '{{units}}',\n    },\n    path: 'main.temp',\n    is: {\n      type: 'number',\n      minimum: '{{hotTemp}}',\n    },\n  },\n};\n```\n\n### Resolver\n\nBy default, `json-schema-rules-engine` uses dot notation - like [property-expr](https://github.com/jquense/expr) or [lodash's get](https://lodash.com/docs/4.17.15#get) - to retrieve an inner value from an object or array via `path`. This can be changed by the `resolver` option. For example, if you wanted to use [json pointer](https://www.npmjs.com/package/jsonpointer), you could do it like this:\n\n```js\nimport { get } from 'jsonpointer';\n\nconst engine = createRulesEngine(validator, { resolver: get });\n\nengine.setRules({\n  myRule: {\n    weather: {\n      params: {\n        query: '{{/city}}',\n        appId: '{{/apiKey}}',\n        units: '{{/units}}',\n      },\n      path: '/main/temp',\n      is: {\n        type: 'number',\n        minimum: '{{/hotTemp}}',\n      },\n    },\n  },\n});\n```\n\n**NOTE:** the `resolver` is also used to retrieve values for [`interpolation`](#interpolation). If using `jsonpointer` notation, this means that interpolations must be prefixed with a `/`.\n\n### Interpolation\n\nInterpolation is configurable by passing the `pattern` option. By default, it uses the [handlebars](https://handlebarsjs.com/)-style pattern of `{{variable}}`.\n\nAnything passed in via the context object given to `engine.run` is available to be interpolated _anywhere_ in a rule.\n\nIn addition to `context`, actions have a special property called [`results`](#results-context) that can be used for interpolation in `then` and `otherwise` clauses.\n\n#### Results Context\n\nThe (top level) `when` clause of a rule can interpolate things from `context`. But the `then` and `otherwise` have a special property available to them called `results` that you can interpolate. This is where defining FactMap as arrays or objects also comes into play. Consider the following rule:\n\n```js\nconst rules = {\n  dailyTemp: {\n    when: [\n      {\n        weather: {\n          params: {\n            query: '{{city}}',\n            appId: '{{apiKey}}',\n            units: '{{units}}',\n          },\n          path: 'main.temp',\n          is: {\n            type: 'number',\n            minimum: '{{hotTemp}}',\n          },\n        },\n      },\n    ],\n    then: {\n      actions: [\n        {\n          type: 'log',\n          params: {\n            message:\n              'Quite hot out today - going to be {{results[0].weather.resolved}}!',\n          },\n        },\n      ],\n    },\n    otherwise: {\n      actions: [\n        {\n          type: 'log',\n          params: {\n            message:\n              'Brrr, bundle up - only going to be {{resilts[0].weather.resolved}}',\n          },\n        },\n      ],\n    },\n  },\n};\n```\n\nIf we were to name the FactMap using an object instead of an array, we could use the key of the FactMap for the interpolation:\n\n```js\nconst rules = {\n  dailyTemp: {\n    when: {\n      myWeatherCondition: {\n        weather: {\n          params: {\n            query: '{{city}}',\n            appId: '{{apiKey}}',\n            units: '{{units}}',\n          },\n          path: 'main.temp',\n          is: {\n            type: 'number',\n            minimum: '{{hotTemp}}',\n          },\n        },\n      },\n    },\n    then: {\n      actions: [\n        {\n          type: 'log',\n          params: {\n            message:\n              'Quite hot out today - going to be {{results.myWeatherCondition.weather.resolved}}!',\n          },\n        },\n      ],\n    },\n  },\n};\n```\n\nTwo things to note:\n\n1. The `results` variable is local to the rule that it's operating in. Different rules have different results.\n2. There are two properties on the fact name (`weather` in the above case):\n   - `value` - the value returned from the function (or the value from context if using a static fact)\n   - `resolved` - the value being evaluated. If there is no `path`, value and `resolved` are the same\n\n### Events\n\nThe rules engine is also an event emitter. There are 4 types of events you can listen to\n\n- [start](#start)\n- [complete](#complete)\n- [debug](#debug)\n- [error](#error)\n\n### start\n\nEmitted as soon as you call `run` on the engine\n\n```js\nengine.on('start', ({ context, facts, rules, actions }) =\u003e {\n  /* ... */\n});\n```\n\n### complete\n\nEmitted when all rules have been evaluated AND all actions have been executed\n\n```js\nengine.on('complete', ({ context, results }) =\u003e {\n  /* ... */\n});\n```\n\n### debug\n\nUseful to monitor the internal execution and evaluation of facts and actions\n\n```js\nengine.on('debug', ({ type, ...rest }) =\u003e {\n  /* ... */\n});\n```\n\n### error\n\nAny errors thrown during fact execution/evaluation or action execution are emitted via `error`\n\n```js\nengine.on('error', ({ type, ...rest }) =\u003e {\n  /* ... */\n});\n```\n\nThe errors that can be emitted are:\n\n- `FactExecutionError` - errors thrown during the execution of functional facts\n- `FactEvaluationError` - errors thrown during the evaluation of facts/results from facts\n- `ActionExecutionError` - errors thrown during the execution of actions\n\n## API/Types\n\n- **`createRulesEngine(validator: Validator, options?: Options): RulesEngine`**\n\n```ts\ntype Options = {\n  facts?: Record\u003cstring,Fact\u003e;\n  rules?: Record\u003cstring,Rule\u003e;\n  actions?: Record\u003cstring,Action\u003e;\n  pattern?: RegExp; // for interpolation\n  memoizer?: \u003cT\u003e(a: T, b: T) =\u003e boolean;\n  resolver?: (subject: Record\u003cstring,any\u003e, path: string) =\u003e any\n};\n\ninterface RulesEngine {\n  setRules(rulesPatch: Patch\u003cRules\u003e): void;\n  setFacts(factsPatch: Patch\u003cFacts\u003e): void;\n  setActions(actionsPatch: Patch\u003cActions\u003e): void;\n  on('debug', subscriber: DebugSubscriber): Unsubscribe\n  on('error', subscriber: ErrorSubscriber): Unsubscribe\n  on('start', subscriber: StartSubscriber): Unsubscribe\n  on('complete', subscriber: CompleteSubscriber): Unsubscribe\n  run(context: Record\u003cstring, any\u003e): Promise\u003cEngineResults\u003e;\n}\n\ntype Unsubscribe = () =\u003e void;\n\ntype PatchFunction\u003cT\u003e = (o: T) =\u003e T;\ntype Patch\u003cT\u003e = PatchFunction\u003cT\u003e | Partial\u003cT\u003e;\n```\n\n## License\n\n[MIT](./LICENSE)\n\n## Contributing\n\nHelp wanted! I'd like to create really great advanced types around the content of the facts, actions, and context given to the engine. Reach out [@akmjenkins](https://twitter.com/akmjenkins) or [akmjenkins@gmail.com](mailto:akmjenkins@gmail.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakmjenkins%2Fjson-schema-rules-engine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fakmjenkins%2Fjson-schema-rules-engine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fakmjenkins%2Fjson-schema-rules-engine/lists"}