Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

json-rules-engine

Package Overview
Dependencies
Maintainers
1
Versions
82
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

json-rules-engine - npm Package Compare versions

Comparing version 7.0.0 to 8.0.0-alpha.1

dist/index.cjs

204

CHANGELOG.md
#### 6.1.0 / 2021-06-03
* engine.removeRule() now supports removing rules by name
* Added engine.updateRule(rule)
- engine.removeRule() now supports removing rules by name
- Added engine.updateRule(rule)
#### 6.0.1 / 2021-03-09
* Updates Typescript types to include `failureEvents` in EngineResult.
- Updates Typescript types to include `failureEvents` in EngineResult.
#### 6.0.0 / 2020-12-22
* BREAKING CHANGES
* To continue using [selectn](https://github.com/wilmoore/selectn.js) syntax for condition `path`s, use the new `pathResolver` feature. Read more [here](./docs/rules.md#condition-helpers-custom-path-resolver). Add the following to the engine constructor:
```js
const pathResolver = (object, path) => {
return selectn(path)(object)
}
const engine = new Engine(rules, { pathResolver })
```
(fixes #205)
* Engine and Rule events `on('success')`, `on('failure')`, and Rule callbacks `onSuccess` and `onFailure` now honor returned promises; any event handler that returns a promise will be waited upon to resolve before engine execution continues. (fixes #235)
* Private `rule.event` property renamed. Use `rule.getEvent()` to avoid breaking changes in the future.
* The `success-events` fact used to store successful events has been converted to an internal data structure and will no longer appear in the almanac's facts. (fixes #187)
* NEW FEATURES
* Engine constructor now accepts a `pathResolver` option for resolving condition `path` properties. Read more [here](./docs/rules.md#condition-helpers-custom-path-resolver). (fixes #210)
* Engine.run() now returns three additional data structures:
* `failureEvents`, an array of all failed rules events. (fixes #192)
* `results`, an array of RuleResults for each successful rule (fixes #216)
* `failureResults`, an array of RuleResults for each failed rule
- BREAKING CHANGES
- To continue using [selectn](https://github.com/wilmoore/selectn.js) syntax for condition `path`s, use the new `pathResolver` feature. Read more [here](./docs/rules.md#condition-helpers-custom-path-resolver). Add the following to the engine constructor:
```js
const pathResolver = (object, path) => {
return selectn(path)(object);
};
const engine = new Engine(rules, { pathResolver });
```
(fixes #205)
- Engine and Rule events `on('success')`, `on('failure')`, and Rule callbacks `onSuccess` and `onFailure` now honor returned promises; any event handler that returns a promise will be waited upon to resolve before engine execution continues. (fixes #235)
- Private `rule.event` property renamed. Use `rule.getEvent()` to avoid breaking changes in the future.
- The `success-events` fact used to store successful events has been converted to an internal data structure and will no longer appear in the almanac's facts. (fixes #187)
- NEW FEATURES
- Engine constructor now accepts a `pathResolver` option for resolving condition `path` properties. Read more [here](./docs/rules.md#condition-helpers-custom-path-resolver). (fixes #210)
- Engine.run() now returns three additional data structures:
- `failureEvents`, an array of all failed rules events. (fixes #192)
- `results`, an array of RuleResults for each successful rule (fixes #216)
- `failureResults`, an array of RuleResults for each failed rule
#### 5.3.0 / 2020-12-02
* Allow facts to have a value of `undefined`
- Allow facts to have a value of `undefined`
#### 5.2.0 / 2020-11-31
* No changes; published to correct an accidental publish of untagged alpha
- No changes; published to correct an accidental publish of untagged alpha
#### 5.0.4 / 2020-09-26
* Upgrade dependencies to latest
- Upgrade dependencies to latest
#### 5.0.3 / 2020-01-26
* Upgrade jsonpath-plus dependency, to fix inconsistent scalar results (#175)
- Upgrade jsonpath-plus dependency, to fix inconsistent scalar results (#175)
#### 5.0.2 / 2020-01-18
* BUGFIX: Add missing `DEBUG` log for almanac.addRuntimeFact()
- BUGFIX: Add missing `DEBUG` log for almanac.addRuntimeFact()
#### 5.0.1 / 2020-01-18
* BUGFIX: `DEBUG` envs works with cookies disables
- BUGFIX: `DEBUG` envs works with cookies disables
#### 5.0.0 / 2019-11-29
* BREAKING CHANGES
* Rule conditions' `path` property is now interpreted using [json-path](https://goessner.net/articles/JsonPath/)
* To continue using the old syntax (provided via [selectn](https://github.com/wilmoore/selectn.js)), `npm install selectn` as a direct dependency, and `json-rules-engine` will continue to interpret legacy paths this way.
* Any path starting with `$` will be assumed to use `json-path` syntax
- BREAKING CHANGES
- Rule conditions' `path` property is now interpreted using [json-path](https://goessner.net/articles/JsonPath/)
- To continue using the old syntax (provided via [selectn](https://github.com/wilmoore/selectn.js)), `npm install selectn` as a direct dependency, and `json-rules-engine` will continue to interpret legacy paths this way.
- Any path starting with `$` will be assumed to use `json-path` syntax
#### 4.1.0 / 2019-09-27
* Export Typescript definitions (@brianphillips)
- Export Typescript definitions (@brianphillips)
#### 4.0.0 / 2019-08-22
* BREAKING CHANGES
* `engine.run()` now returns a hash of events and almanac: `{ events: [], almanac: Almanac instance }`. Previously in v3, the `run()` returned the `events` array.
* For example, `const events = await engine.run()` under v3 will need to be changed to `const { events } = await engine.run()` under v4.
- BREAKING CHANGES
- `engine.run()` now returns a hash of events and almanac: `{ events: [], almanac: Almanac instance }`. Previously in v3, the `run()` returned the `events` array.
- For example, `const events = await engine.run()` under v3 will need to be changed to `const { events } = await engine.run()` under v4.
#### 3.1.0 / 2019-07-19
* Feature: `rule.setName()` and `ruleResult.name`
- Feature: `rule.setName()` and `ruleResult.name`
#### 3.0.3 / 2019-07-15
* Fix "localStorage.debug" not working in browsers
- Fix "localStorage.debug" not working in browsers
#### 3.0.2 / 2019-05-23
* Fix "process" not defined error in browsers lacking node.js global shims
- Fix "process" not defined error in browsers lacking node.js global shims
#### 3.0.0 / 2019-05-17
* BREAKING CHANGES
* Previously all conditions with undefined facts would resolve false. With this change, undefined facts values are treated as `undefined`.
* Greatly improved performance of `allowUndefinedfacts = true` engine option
* Reduce package bundle size by ~40%
- BREAKING CHANGES
- Previously all conditions with undefined facts would resolve false. With this change, undefined facts values are treated as `undefined`.
- Greatly improved performance of `allowUndefinedfacts = true` engine option
- Reduce package bundle size by ~40%
#### 2.3.5 / 2019-04-26
* Replace debug with vanilla console.log
- Replace debug with vanilla console.log
#### 2.3.4 / 2019-04-26
* Use Array.isArray instead of instanceof to test Array parameters to address edge cases
- Use Array.isArray instead of instanceof to test Array parameters to address edge cases
#### 2.3.3 / 2019-04-23
* Fix rules cache not clearing after removeRule()
- Fix rules cache not clearing after removeRule()
#### 2.3.2 / 2018-12-28
* Upgrade all dependencies to latest
- Upgrade all dependencies to latest
#### 2.3.1 / 2018-12-03
* IE8 compatibility: replace Array.forEach with for loop (@knalbandianbrightgrove)
- IE8 compatibility: replace Array.forEach with for loop (@knalbandianbrightgrove)
#### 2.3.0 / 2018-05-03
* Engine.removeFact() - removes fact from the engine (@SaschaDeWaal)
* Engine.removeRule() - removes rule from the engine (@SaschaDeWaal)
* Engine.removeOperator() - removes operator from the engine (@SaschaDeWaal)
- Engine.removeFact() - removes fact from the engine (@SaschaDeWaal)
- Engine.removeRule() - removes rule from the engine (@SaschaDeWaal)
- Engine.removeOperator() - removes operator from the engine (@SaschaDeWaal)
#### 2.2.0 / 2018-04-19
* Performance: Constant facts now perform 18-26X better
* Performance: Removes await/async transpilation and json.stringify calls, significantly improving overall performance
- Performance: Constant facts now perform 18-26X better
- Performance: Removes await/async transpilation and json.stringify calls, significantly improving overall performance
#### 2.1.0 / 2018-02-19
* Publish dist updates for 2.0.3
- Publish dist updates for 2.0.3
#### 2.0.3 / 2018-01-29
* Add factResult and result to the JSON generated for Condition (@bjacobso)
- Add factResult and result to the JSON generated for Condition (@bjacobso)
#### 2.0.2 / 2017-07-24
* Bugfix IE8 support
- Bugfix IE8 support
#### 2.0.1 / 2017-07-05
* Bugfix rule result serialization
- Bugfix rule result serialization
#### 2.0.0 / 2017-04-21
* Publishing 2.0.0
- Publishing 2.0.0
#### 2.0.0-beta2 / 2017-04-10
* Fix fact path object checking to work with objects that have prototypes (lodash isObjectLike instead of isPlainObject)
- Fix fact path object checking to work with objects that have prototypes (lodash isObjectLike instead of isPlainObject)
#### 2.0.0-beta1 / 2017-04-09
* Add rule results
* Document fact .path ability to parse properties containing dots
* Bump dependencies
* BREAKING CHANGES
* `engine.on('failure', (rule, almanac))` is now `engine.on('failure', (event, almanac, ruleResult))`
* `engine.on(eventType, (eventParams, engine))` is now `engine.on(eventType, (eventParams, almanac, ruleResult))`
- Add rule results
- Document fact .path ability to parse properties containing dots
- Bump dependencies
- BREAKING CHANGES
- `engine.on('failure', (rule, almanac))` is now `engine.on('failure', (event, almanac, ruleResult))`
- `engine.on(eventType, (eventParams, engine))` is now `engine.on(eventType, (eventParams, almanac, ruleResult))`
#### 1.5.1 / 2017-03-19
* Bugfix almanac.factValue skipping interpreting condition "path" for cached facts
- Bugfix almanac.factValue skipping interpreting condition "path" for cached facts
#### 1.5.0 / 2017-03-12
* Add fact comparison conditions
- Add fact comparison conditions
#### 1.4.0 / 2017-01-23
* Add `allowUndefinedFacts` engine option
- Add `allowUndefinedFacts` engine option
#### 1.3.1 / 2017-01-16
* Bump object-hash dependency to latest
- Bump object-hash dependency to latest
#### 1.3.0 / 2016-10-24
* Rule event emissions
* Rule chaining
- Rule event emissions
- Rule chaining
#### 1.2.1 / 2016-10-22
* Use Array.indexOf instead of Array.includes for older node version compatibility
- Use Array.indexOf instead of Array.includes for older node version compatibility
#### 1.2.0 / 2016-09-13
* Fact path support
- Fact path support
#### 1.1.0 / 2016-09-11
* Custom operator support
- Custom operator support
#### 1.0.4 / 2016-06-18
* fix issue #6; runtime facts unique to each run()
- fix issue #6; runtime facts unique to each run()
#### 1.0.3 / 2016-06-15
* fix issue #5; dependency error babel-core/register
- fix issue #5; dependency error babel-core/register
#### 1.0.0 / 2016-05-01
* api stable; releasing 1.0
* engine.run() now returns triggered events
- api stable; releasing 1.0
- engine.run() now returns triggered events
#### 1.0.0-beta10 / 2016-04-16
* Completed the 'fact-dependecy' advanced example
* Updated addFact and addRule engine methods to return 'this' for easy chaining
- Completed the 'fact-dependecy' advanced example
- Updated addFact and addRule engine methods to return 'this' for easy chaining
#### 1.0.0-beta9 / 2016-04-11
* Completed the 'basic' example
* [BREAKING CHANGE] update engine.on('success') and engine.on('failure') to pass the current almanac instance as the second argument, rather than the engine
- Completed the 'basic' example
- [BREAKING CHANGE] update engine.on('success') and engine.on('failure') to pass the current almanac instance as the second argument, rather than the engine

@@ -1,3 +0,1348 @@

'use strict';
// src/fact.mjs
import hash from "hash-it";
var Fact = class _Fact {
/**
* Returns a new fact instance
* @param {string} id - fact unique identifer
* @param {object} options
* @param {boolean} options.cache - whether to cache the fact's value for future rules
* @param {primitive|function} valueOrMethod - constant primitive, or method to call when computing the fact's value
* @return {Fact}
*/
constructor(id, valueOrMethod, options) {
this.id = id;
const defaultOptions = { cache: true };
if (typeof options === "undefined") {
options = defaultOptions;
}
if (typeof valueOrMethod !== "function") {
this.value = valueOrMethod;
this.type = this.constructor.CONSTANT;
} else {
this.calculationMethod = valueOrMethod;
this.type = this.constructor.DYNAMIC;
}
if (!this.id) throw new Error("factId required");
this.priority = parseInt(options.priority || 1, 10);
this.options = Object.assign({}, defaultOptions, options);
this.cacheKeyMethod = this.defaultCacheKeys;
return this;
}
isConstant() {
return this.type === this.constructor.CONSTANT;
}
isDynamic() {
return this.type === this.constructor.DYNAMIC;
}
/**
* Return the fact value, based on provided parameters
* @param {object} params
* @param {Almanac} almanac
* @return {any} calculation method results
*/
calculate(params, almanac) {
if (Object.prototype.hasOwnProperty.call(this, "value")) {
return this.value;
}
return this.calculationMethod(params, almanac);
}
/**
* Return a cache key (MD5 string) based on parameters
* @param {object} obj - properties to generate a hash key from
* @return {string} MD5 string based on the hash'd object
*/
static hashFromObject(obj) {
return hash(obj);
}
/**
* Default properties to use when caching a fact
* Assumes every fact is a pure function, whose computed value will only
* change when input params are modified
* @param {string} id - fact unique identifer
* @param {object} params - parameters passed to fact calcution method
* @return {object} id + params
*/
defaultCacheKeys(id, params) {
return { params, id };
}
/**
* Generates the fact's cache key(MD5 string)
* Returns nothing if the fact's caching has been disabled
* @param {object} params - parameters that would be passed to the computation method
* @return {string} cache key
*/
getCacheKey(params) {
if (this.options.cache === true) {
const cacheProperties = this.cacheKeyMethod(this.id, params);
const hash2 = _Fact.hashFromObject(cacheProperties);
return hash2;
}
}
};
Fact.CONSTANT = "CONSTANT";
Fact.DYNAMIC = "DYNAMIC";
var fact_default = Fact;
module.exports = require('./json-rules-engine');
// src/debug.mjs
function createDebug() {
try {
if (typeof process !== "undefined" && process.env && process.env.DEBUG && process.env.DEBUG.match(/json-rules-engine/) || typeof window !== "undefined" && window.localStorage && window.localStorage.debug && window.localStorage.debug.match(/json-rules-engine/)) {
return console.debug.bind(console);
}
} catch (_error) {
}
return () => {
};
}
var debug_default = createDebug();
// src/condition.mjs
var Condition = class _Condition {
constructor(properties) {
if (!properties) throw new Error("Condition: constructor options required");
const booleanOperator = _Condition.booleanOperator(properties);
Object.assign(this, properties);
if (booleanOperator) {
const subConditions = properties[booleanOperator];
const subConditionsIsArray = Array.isArray(subConditions);
if (booleanOperator !== "not" && !subConditionsIsArray) {
throw new Error(`"${booleanOperator}" must be an array`);
}
if (booleanOperator === "not" && subConditionsIsArray) {
throw new Error(`"${booleanOperator}" cannot be an array`);
}
this.operator = booleanOperator;
this.priority = parseInt(properties.priority, 10) || 1;
if (subConditionsIsArray) {
this[booleanOperator] = subConditions.map((c) => new _Condition(c));
} else {
this[booleanOperator] = new _Condition(subConditions);
}
} else if (!Object.prototype.hasOwnProperty.call(properties, "condition")) {
if (!Object.prototype.hasOwnProperty.call(properties, "fact")) {
throw new Error('Condition: constructor "fact" property required');
}
if (!Object.prototype.hasOwnProperty.call(properties, "operator")) {
throw new Error('Condition: constructor "operator" property required');
}
if (!Object.prototype.hasOwnProperty.call(properties, "value")) {
throw new Error('Condition: constructor "value" property required');
}
if (Object.prototype.hasOwnProperty.call(properties, "priority")) {
properties.priority = parseInt(properties.priority, 10);
}
}
}
/**
* Converts the condition into a json-friendly structure
* @param {Boolean} stringify - whether to return as a json string
* @returns {string,object} json string or json-friendly object
*/
toJSON(stringify = true) {
const props = {};
if (this.priority) {
props.priority = this.priority;
}
if (this.name) {
props.name = this.name;
}
const oper = _Condition.booleanOperator(this);
if (oper) {
if (Array.isArray(this[oper])) {
props[oper] = this[oper].map((c) => c.toJSON(false));
} else {
props[oper] = this[oper].toJSON(false);
}
} else if (this.isConditionReference()) {
props.condition = this.condition;
} else {
props.operator = this.operator;
props.value = this.value;
props.fact = this.fact;
if (this.factResult !== void 0) {
props.factResult = this.factResult;
}
if (this.result !== void 0) {
props.result = this.result;
}
if (this.params) {
props.params = this.params;
}
if (this.path) {
props.path = this.path;
}
}
if (stringify) {
return JSON.stringify(props);
}
return props;
}
/**
* Takes the fact result and compares it to the condition 'value', using the operator
* LHS OPER RHS
* <fact + params + path> <operator> <value>
*
* @param {Almanac} almanac
* @param {Map} operatorMap - map of available operators, keyed by operator name
* @returns {Boolean} - evaluation result
*/
evaluate(almanac, operatorMap) {
if (!almanac) return Promise.reject(new Error("almanac required"));
if (!operatorMap) return Promise.reject(new Error("operatorMap required"));
if (this.isBooleanOperator()) {
return Promise.reject(new Error("Cannot evaluate() a boolean condition"));
}
const op = operatorMap.get(this.operator);
if (!op) {
return Promise.reject(new Error(`Unknown operator: ${this.operator}`));
}
return Promise.all([
almanac.getValue(this.value),
almanac.factValue(this.fact, this.params, this.path)
]).then(([rightHandSideValue, leftHandSideValue]) => {
const result = op.evaluate(leftHandSideValue, rightHandSideValue);
debug_default("condition::evaluate", {
leftHandSideValue,
operator: this.operator,
rightHandSideValue,
result
});
return {
result,
leftHandSideValue,
rightHandSideValue,
operator: this.operator
};
});
}
/**
* Returns the boolean operator for the condition
* If the condition is not a boolean condition, the result will be 'undefined'
* @return {string 'all', 'any', or 'not'}
*/
static booleanOperator(condition) {
if (Object.prototype.hasOwnProperty.call(condition, "any")) {
return "any";
} else if (Object.prototype.hasOwnProperty.call(condition, "all")) {
return "all";
} else if (Object.prototype.hasOwnProperty.call(condition, "not")) {
return "not";
}
}
/**
* Returns the condition's boolean operator
* Instance version of Condition.isBooleanOperator
* @returns {string,undefined} - 'any', 'all', 'not' or undefined (if not a boolean condition)
*/
booleanOperator() {
return _Condition.booleanOperator(this);
}
/**
* Whether the operator is boolean ('all', 'any', 'not')
* @returns {Boolean}
*/
isBooleanOperator() {
return _Condition.booleanOperator(this) !== void 0;
}
/**
* Whether the condition represents a reference to a condition
* @returns {Boolean}
*/
isConditionReference() {
return Object.prototype.hasOwnProperty.call(this, "condition");
}
};
// src/rule-result.mjs
import deepClone from "clone";
var RuleResult = class {
constructor(conditions, event, priority, name) {
this.conditions = deepClone(conditions);
this.event = deepClone(event);
this.priority = deepClone(priority);
this.name = deepClone(name);
this.result = null;
}
setResult(result) {
this.result = result;
}
resolveEventParams(almanac) {
if (this.event.params !== null && typeof this.event.params === "object") {
const updates = [];
for (const key in this.event.params) {
if (Object.prototype.hasOwnProperty.call(this.event.params, key)) {
updates.push(
almanac.getValue(this.event.params[key]).then((val) => this.event.params[key] = val)
);
}
}
return Promise.all(updates);
}
return Promise.resolve();
}
toJSON(stringify = true) {
const props = {
conditions: this.conditions.toJSON(false),
event: this.event,
priority: this.priority,
name: this.name,
result: this.result
};
if (stringify) {
return JSON.stringify(props);
}
return props;
}
};
// src/rule.mjs
import deepClone2 from "clone";
import EventEmitter from "eventemitter2";
var Rule = class extends EventEmitter {
/**
* returns a new Rule instance
* @param {object,string} options, or json string that can be parsed into options
* @param {integer} options.priority (>1) - higher runs sooner.
* @param {Object} options.event - event to fire when rule evaluates as successful
* @param {string} options.event.type - name of event to emit
* @param {string} options.event.params - parameters to pass to the event listener
* @param {Object} options.conditions - conditions to evaluate when processing this rule
* @param {any} options.name - identifier for a particular rule, particularly valuable in RuleResult output
* @return {Rule} instance
*/
constructor(options) {
super();
if (typeof options === "string") {
options = JSON.parse(options);
}
if (options && options.conditions) {
this.setConditions(options.conditions);
}
if (options && options.onSuccess) {
this.on("success", options.onSuccess);
}
if (options && options.onFailure) {
this.on("failure", options.onFailure);
}
if (options && (options.name || options.name === 0)) {
this.setName(options.name);
}
const priority = options && options.priority || 1;
this.setPriority(priority);
const event = options && options.event || { type: "unknown" };
this.setEvent(event);
}
/**
* Sets the priority of the rule
* @param {integer} priority (>=1) - increasing the priority causes the rule to be run prior to other rules
*/
setPriority(priority) {
priority = parseInt(priority, 10);
if (priority <= 0) throw new Error("Priority must be greater than zero");
this.priority = priority;
return this;
}
/**
* Sets the name of the rule
* @param {any} name - any truthy input and zero is allowed
*/
setName(name) {
if (!name && name !== 0) {
throw new Error('Rule "name" must be defined');
}
this.name = name;
return this;
}
/**
* Sets the conditions to run when evaluating the rule.
* @param {object} conditions - conditions, root element must be a boolean operator
*/
setConditions(conditions) {
if (!Object.prototype.hasOwnProperty.call(conditions, "all") && !Object.prototype.hasOwnProperty.call(conditions, "any") && !Object.prototype.hasOwnProperty.call(conditions, "not") && !Object.prototype.hasOwnProperty.call(conditions, "condition")) {
throw new Error(
'"conditions" root must contain a single instance of "all", "any", "not", or "condition"'
);
}
this.conditions = new Condition(conditions);
return this;
}
/**
* Sets the event to emit when the conditions evaluate truthy
* @param {object} event - event to emit
* @param {string} event.type - event name to emit on
* @param {string} event.params - parameters to emit as the argument of the event emission
*/
setEvent(event) {
if (!event) throw new Error("Rule: setEvent() requires event object");
if (!Object.prototype.hasOwnProperty.call(event, "type")) {
throw new Error(
'Rule: setEvent() requires event object with "type" property'
);
}
this.ruleEvent = {
type: event.type
};
if (event.params) this.ruleEvent.params = event.params;
return this;
}
/**
* returns the event object
* @returns {Object} event
*/
getEvent() {
return this.ruleEvent;
}
/**
* returns the priority
* @returns {Number} priority
*/
getPriority() {
return this.priority;
}
/**
* returns the event object
* @returns {Object} event
*/
getConditions() {
return this.conditions;
}
/**
* returns the engine object
* @returns {Object} engine
*/
getEngine() {
return this.engine;
}
/**
* Sets the engine to run the rules under
* @param {object} engine
* @returns {Rule}
*/
setEngine(engine) {
this.engine = engine;
return this;
}
toJSON(stringify = true) {
const props = {
conditions: this.conditions.toJSON(false),
priority: this.priority,
event: this.ruleEvent,
name: this.name
};
if (stringify) {
return JSON.stringify(props);
}
return props;
}
/**
* Priorizes an array of conditions based on "priority"
* When no explicit priority is provided on the condition itself, the condition's priority is determine by its fact
* @param {Condition[]} conditions
* @return {Condition[][]} prioritized two-dimensional array of conditions
* Each outer array element represents a single priority(integer). Inner array is
* all conditions with that priority.
*/
prioritizeConditions(conditions) {
const factSets = conditions.reduce((sets, condition) => {
let priority = condition.priority;
if (!priority) {
const fact = this.engine.getFact(condition.fact);
priority = fact && fact.priority || 1;
}
if (!sets[priority]) sets[priority] = [];
sets[priority].push(condition);
return sets;
}, {});
return Object.keys(factSets).sort((a, b) => {
return Number(a) > Number(b) ? -1 : 1;
}).map((priority) => factSets[priority]);
}
/**
* Evaluates the rule, starting with the root boolean operator and recursing down
* All evaluation is done within the context of an almanac
* @return {Promise(RuleResult)} rule evaluation result
*/
evaluate(almanac) {
const ruleResult = new RuleResult(
this.conditions,
this.ruleEvent,
this.priority,
this.name
);
const evaluateCondition = (condition) => {
if (condition.isConditionReference()) {
return realize(condition);
} else if (condition.isBooleanOperator()) {
const subConditions = condition[condition.operator];
let comparisonPromise;
if (condition.operator === "all") {
comparisonPromise = all(subConditions);
} else if (condition.operator === "any") {
comparisonPromise = any(subConditions);
} else {
comparisonPromise = not(subConditions);
}
return comparisonPromise.then((comparisonValue) => {
const passes = comparisonValue === true;
condition.result = passes;
return passes;
});
} else {
return condition.evaluate(almanac, this.engine.operators).then((evaluationResult) => {
const passes = evaluationResult.result;
condition.factResult = evaluationResult.leftHandSideValue;
condition.result = passes;
return passes;
});
}
};
const evaluateConditions = (conditions, method) => {
if (!Array.isArray(conditions)) conditions = [conditions];
return Promise.all(
conditions.map((condition) => evaluateCondition(condition))
).then((conditionResults) => {
debug_default("rule::evaluateConditions", { results: conditionResults });
return method.call(conditionResults, (result) => result === true);
});
};
const prioritizeAndRun = (conditions, operator) => {
if (conditions.length === 0) {
return Promise.resolve(true);
}
if (conditions.length === 1) {
return evaluateCondition(conditions[0]);
}
const orderedSets = this.prioritizeConditions(conditions);
let cursor = Promise.resolve(operator === "all");
for (let i = 0; i < orderedSets.length; i++) {
const set = orderedSets[i];
cursor = cursor.then((setResult) => {
return operator === "any" ? setResult || evaluateConditions(set, Array.prototype.some) : setResult && evaluateConditions(set, Array.prototype.every);
});
}
return cursor;
};
const any = (conditions) => {
return prioritizeAndRun(conditions, "any");
};
const all = (conditions) => {
return prioritizeAndRun(conditions, "all");
};
const not = (condition) => {
return prioritizeAndRun([condition], "not").then((result) => !result);
};
const realize = (conditionReference) => {
const condition = this.engine.conditions.get(
conditionReference.condition
);
if (!condition) {
if (this.engine.allowUndefinedConditions) {
conditionReference.result = false;
return Promise.resolve(false);
} else {
throw new Error(
`No condition ${conditionReference.condition} exists`
);
}
} else {
delete conditionReference.condition;
Object.assign(conditionReference, deepClone2(condition));
return evaluateCondition(conditionReference);
}
};
const processResult = (result) => {
ruleResult.setResult(result);
let processEvent = Promise.resolve();
if (this.engine.replaceFactsInEventParams) {
processEvent = ruleResult.resolveEventParams(almanac);
}
const event = result ? "success" : "failure";
return processEvent.then(
() => this.emitAsync(event, ruleResult.event, almanac, ruleResult)
).then(() => ruleResult);
};
if (ruleResult.conditions.any) {
return any(ruleResult.conditions.any).then(
(result) => processResult(result)
);
} else if (ruleResult.conditions.all) {
return all(ruleResult.conditions.all).then(
(result) => processResult(result)
);
} else if (ruleResult.conditions.not) {
return not(ruleResult.conditions.not).then(
(result) => processResult(result)
);
} else {
return realize(ruleResult.conditions).then(
(result) => processResult(result)
);
}
}
};
var rule_default = Rule;
// src/errors.mjs
var UndefinedFactError = class extends Error {
constructor(...props) {
super(...props);
this.code = "UNDEFINED_FACT";
}
};
// src/almanac.mjs
import { JSONPath } from "jsonpath-plus";
function defaultPathResolver(value, path) {
return JSONPath({ path, json: value, wrap: false });
}
var Almanac = class {
constructor(options = {}) {
this.factMap = /* @__PURE__ */ new Map();
this.factResultsCache = /* @__PURE__ */ new Map();
this.allowUndefinedFacts = Boolean(options.allowUndefinedFacts);
this.pathResolver = options.pathResolver || defaultPathResolver;
this.events = { success: [], failure: [] };
this.ruleResults = [];
}
/**
* Adds a success event
* @param {Object} event
*/
addEvent(event, outcome) {
if (!outcome) throw new Error('outcome required: "success" | "failure"]');
this.events[outcome].push(event);
}
/**
* retrieve successful events
*/
getEvents(outcome = "") {
if (outcome) return this.events[outcome];
return this.events.success.concat(this.events.failure);
}
/**
* Adds a rule result
* @param {Object} event
*/
addResult(ruleResult) {
this.ruleResults.push(ruleResult);
}
/**
* retrieve successful events
*/
getResults() {
return this.ruleResults;
}
/**
* Retrieve fact by id, raising an exception if it DNE
* @param {String} factId
* @return {Fact}
*/
_getFact(factId) {
return this.factMap.get(factId);
}
/**
* Registers fact with the almanac
* @param {[type]} fact [description]
*/
_addConstantFact(fact) {
this.factMap.set(fact.id, fact);
this._setFactValue(fact, {}, fact.value);
}
/**
* Sets the computed value of a fact
* @param {Fact} fact
* @param {Object} params - values for differentiating this fact value from others, used for cache key
* @param {Mixed} value - computed value
*/
_setFactValue(fact, params, value) {
const cacheKey = fact.getCacheKey(params);
const factValue = Promise.resolve(value);
if (cacheKey) {
this.factResultsCache.set(cacheKey, factValue);
}
return factValue;
}
/**
* Add a fact definition to the engine. Facts are called by rules as they are evaluated.
* @param {object|Fact} id - fact identifier or instance of Fact
* @param {function} definitionFunc - function to be called when computing the fact value for a given rule
* @param {Object} options - options to initialize the fact with. used when "id" is not a Fact instance
*/
addFact(id, valueOrMethod, options) {
let factId = id;
let fact;
if (id instanceof fact_default) {
factId = id.id;
fact = id;
} else {
fact = new fact_default(id, valueOrMethod, options);
}
debug_default("almanac::addFact", { id: factId });
this.factMap.set(factId, fact);
if (fact.isConstant()) {
this._setFactValue(fact, {}, fact.value);
}
return this;
}
/**
* Adds a constant fact during runtime. Can be used mid-run() to add additional information
* @deprecated use addFact
* @param {String} fact - fact identifier
* @param {Mixed} value - constant value of the fact
*/
addRuntimeFact(factId, value) {
debug_default("almanac::addRuntimeFact", { id: factId });
const fact = new fact_default(factId, value);
return this._addConstantFact(fact);
}
/**
* Returns the value of a fact, based on the given parameters. Utilizes the 'almanac' maintained
* by the engine, which cache's fact computations based on parameters provided
* @param {string} factId - fact identifier
* @param {Object} params - parameters to feed into the fact. By default, these will also be used to compute the cache key
* @param {String} path - object
* @return {Promise} a promise which will resolve with the fact computation.
*/
factValue(factId, params = {}, path = "") {
let factValuePromise;
const fact = this._getFact(factId);
if (fact === void 0) {
if (this.allowUndefinedFacts) {
return Promise.resolve(void 0);
} else {
return Promise.reject(
new UndefinedFactError(`Undefined fact: ${factId}`)
);
}
}
if (fact.isConstant()) {
factValuePromise = Promise.resolve(fact.calculate(params, this));
} else {
const cacheKey = fact.getCacheKey(params);
const cacheVal = cacheKey && this.factResultsCache.get(cacheKey);
if (cacheVal) {
factValuePromise = Promise.resolve(cacheVal);
debug_default("almanac::factValue cache hit for fact", { id: factId });
} else {
debug_default("almanac::factValue cache miss, calculating", { id: factId });
factValuePromise = this._setFactValue(
fact,
params,
fact.calculate(params, this)
);
}
}
if (path) {
debug_default("condition::evaluate extracting object", { property: path });
return factValuePromise.then((factValue) => {
if (factValue != null && typeof factValue === "object") {
const pathValue = this.pathResolver(factValue, path);
debug_default("condition::evaluate extracting object", {
property: path,
received: pathValue
});
return pathValue;
} else {
debug_default(
"condition::evaluate could not compute object path of non-object",
{ path, factValue, type: typeof factValue }
);
return factValue;
}
});
}
return factValuePromise;
}
/**
* Interprets value as either a primitive, or if a fact, retrieves the fact value
*/
getValue(value) {
if (value != null && typeof value === "object" && Object.prototype.hasOwnProperty.call(value, "fact")) {
return this.factValue(value.fact, value.params, value.path);
}
return Promise.resolve(value);
}
};
// src/engine.mjs
import EventEmitter2 from "eventemitter2";
// src/operator.mjs
var Operator = class {
/**
* Constructor
* @param {string} name - operator identifier
* @param {function(factValue, jsonValue)} callback - operator evaluation method
* @param {function} [factValueValidator] - optional validator for asserting the data type of the fact
* @returns {Operator} - instance
*/
constructor(name, cb, factValueValidator) {
this.name = String(name);
if (!name) throw new Error("Missing operator name");
if (typeof cb !== "function") throw new Error("Missing operator callback");
this.cb = cb;
this.factValueValidator = factValueValidator;
if (!this.factValueValidator) this.factValueValidator = () => true;
}
/**
* Takes the fact result and compares it to the condition 'value', using the callback
* @param {mixed} factValue - fact result
* @param {mixed} jsonValue - "value" property of the condition
* @returns {Boolean} - whether the values pass the operator test
*/
evaluate(factValue, jsonValue) {
return this.factValueValidator(factValue) && this.cb(factValue, jsonValue);
}
};
// src/engine-default-operators.mjs
var Operators = [];
Operators.push(new Operator("equal", (a, b) => a === b));
Operators.push(new Operator("notEqual", (a, b) => a !== b));
Operators.push(new Operator("in", (a, b) => b.indexOf(a) > -1));
Operators.push(new Operator("notIn", (a, b) => b.indexOf(a) === -1));
Operators.push(
new Operator("contains", (a, b) => a.indexOf(b) > -1, Array.isArray)
);
Operators.push(
new Operator("doesNotContain", (a, b) => a.indexOf(b) === -1, Array.isArray)
);
function numberValidator(factValue) {
return Number.parseFloat(factValue).toString() !== "NaN";
}
Operators.push(new Operator("lessThan", (a, b) => a < b, numberValidator));
Operators.push(
new Operator("lessThanInclusive", (a, b) => a <= b, numberValidator)
);
Operators.push(new Operator("greaterThan", (a, b) => a > b, numberValidator));
Operators.push(
new Operator("greaterThanInclusive", (a, b) => a >= b, numberValidator)
);
var engine_default_operators_default = Operators;
// src/operator-decorator.mjs
var OperatorDecorator = class {
/**
* Constructor
* @param {string} name - decorator identifier
* @param {function(factValue, jsonValue, next)} callback - callback that takes the next operator as a parameter
* @param {function} [factValueValidator] - optional validator for asserting the data type of the fact
* @returns {OperatorDecorator} - instance
*/
constructor(name, cb, factValueValidator) {
this.name = String(name);
if (!name) throw new Error("Missing decorator name");
if (typeof cb !== "function") throw new Error("Missing decorator callback");
this.cb = cb;
this.factValueValidator = factValueValidator;
if (!this.factValueValidator) this.factValueValidator = () => true;
}
/**
* Takes the fact result and compares it to the condition 'value', using the callback
* @param {Operator} operator - fact result
* @returns {Operator} - whether the values pass the operator test
*/
decorate(operator) {
const next = operator.evaluate.bind(operator);
return new Operator(
`${this.name}:${operator.name}`,
(factValue, jsonValue) => {
return this.cb(factValue, jsonValue, next);
},
this.factValueValidator
);
}
};
// src/engine-default-operator-decorators.mjs
var OperatorDecorators = [];
OperatorDecorators.push(
new OperatorDecorator(
"someFact",
(factValue, jsonValue, next) => factValue.some((fv) => next(fv, jsonValue)),
Array.isArray
)
);
OperatorDecorators.push(
new OperatorDecorator(
"someValue",
(factValue, jsonValue, next) => jsonValue.some((jv) => next(factValue, jv))
)
);
OperatorDecorators.push(
new OperatorDecorator(
"everyFact",
(factValue, jsonValue, next) => factValue.every((fv) => next(fv, jsonValue)),
Array.isArray
)
);
OperatorDecorators.push(
new OperatorDecorator(
"everyValue",
(factValue, jsonValue, next) => jsonValue.every((jv) => next(factValue, jv))
)
);
OperatorDecorators.push(
new OperatorDecorator(
"swap",
(factValue, jsonValue, next) => next(jsonValue, factValue)
)
);
OperatorDecorators.push(
new OperatorDecorator(
"not",
(factValue, jsonValue, next) => !next(factValue, jsonValue)
)
);
var engine_default_operator_decorators_default = OperatorDecorators;
// src/operator-map.mjs
var OperatorMap = class {
constructor() {
this.operators = /* @__PURE__ */ new Map();
this.decorators = /* @__PURE__ */ new Map();
}
/**
* Add a custom operator definition
* @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc
* @param {function(factValue, jsonValue)} callback - the method to execute when the operator is encountered.
*/
addOperator(operatorOrName, cb) {
let operator;
if (operatorOrName instanceof Operator) {
operator = operatorOrName;
} else {
operator = new Operator(operatorOrName, cb);
}
debug_default("operatorMap::addOperator", { name: operator.name });
this.operators.set(operator.name, operator);
}
/**
* Remove a custom operator definition
* @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc
* @param {function(factValue, jsonValue)} callback - the method to execute when the operator is encountered.
*/
removeOperator(operatorOrName) {
let operatorName;
if (operatorOrName instanceof Operator) {
operatorName = operatorOrName.name;
} else {
operatorName = operatorOrName;
}
const suffix = ":" + operatorName;
const operatorNames = Array.from(this.operators.keys());
for (let i = 0; i < operatorNames.length; i++) {
if (operatorNames[i].endsWith(suffix)) {
this.operators.delete(operatorNames[i]);
}
}
return this.operators.delete(operatorName);
}
/**
* Add a custom operator decorator
* @param {string} decoratorOrName - decorator identifier within the condition; i.e. instead of 'everyFact', 'someValue', etc
* @param {function(factValue, jsonValue, next)} callback - the method to execute when the decorator is encountered.
*/
addOperatorDecorator(decoratorOrName, cb) {
let decorator;
if (decoratorOrName instanceof OperatorDecorator) {
decorator = decoratorOrName;
} else {
decorator = new OperatorDecorator(decoratorOrName, cb);
}
debug_default("operatorMap::addOperatorDecorator", { name: decorator.name });
this.decorators.set(decorator.name, decorator);
}
/**
* Remove a custom operator decorator
* @param {string} decoratorOrName - decorator identifier within the condition; i.e. instead of 'everyFact', 'someValue', etc
*/
removeOperatorDecorator(decoratorOrName) {
let decoratorName;
if (decoratorOrName instanceof OperatorDecorator) {
decoratorName = decoratorOrName.name;
} else {
decoratorName = decoratorOrName;
}
const prefix = decoratorName + ":";
const operatorNames = Array.from(this.operators.keys());
for (let i = 0; i < operatorNames.length; i++) {
if (operatorNames[i].includes(prefix)) {
this.operators.delete(operatorNames[i]);
}
}
return this.decorators.delete(decoratorName);
}
/**
* Get the Operator, or null applies decorators as needed
* @param {string} name - the name of the operator including any decorators
* @returns an operator or null
*/
get(name) {
const decorators = [];
let opName = name;
while (!this.operators.has(opName)) {
const firstDecoratorIndex = opName.indexOf(":");
if (firstDecoratorIndex > 0) {
const decoratorName = opName.slice(0, firstDecoratorIndex);
const decorator = this.decorators.get(decoratorName);
if (!decorator) {
debug_default("operatorMap::get invalid decorator", { name: decoratorName });
return null;
}
decorators.unshift(decorator);
opName = opName.slice(firstDecoratorIndex + 1);
} else {
debug_default("operatorMap::get no operator", { name: opName });
return null;
}
}
let op = this.operators.get(opName);
for (let i = 0; i < decorators.length; i++) {
op = decorators[i].decorate(op);
this.operators.set(op.name, op);
}
return op;
}
};
// src/engine.mjs
var READY = "READY";
var RUNNING = "RUNNING";
var FINISHED = "FINISHED";
var Engine = class extends EventEmitter2 {
/**
* Returns a new Engine instance
* @param {Rule[]} rules - array of rules to initialize with
*/
constructor(rules = [], options = {}) {
super();
this.rules = [];
this.allowUndefinedFacts = options.allowUndefinedFacts || false;
this.allowUndefinedConditions = options.allowUndefinedConditions || false;
this.replaceFactsInEventParams = options.replaceFactsInEventParams || false;
this.pathResolver = options.pathResolver;
this.operators = new OperatorMap();
this.facts = /* @__PURE__ */ new Map();
this.conditions = /* @__PURE__ */ new Map();
this.status = READY;
rules.map((r) => this.addRule(r));
engine_default_operators_default.map((o) => this.addOperator(o));
engine_default_operator_decorators_default.map((d) => this.addOperatorDecorator(d));
}
/**
* Add a rule definition to the engine
* @param {object|Rule} properties - rule definition. can be JSON representation, or instance of Rule
* @param {integer} properties.priority (>1) - higher runs sooner.
* @param {Object} properties.event - event to fire when rule evaluates as successful
* @param {string} properties.event.type - name of event to emit
* @param {string} properties.event.params - parameters to pass to the event listener
* @param {Object} properties.conditions - conditions to evaluate when processing this rule
*/
addRule(properties) {
if (!properties) throw new Error("Engine: addRule() requires options");
let rule;
if (properties instanceof rule_default) {
rule = properties;
} else {
if (!Object.prototype.hasOwnProperty.call(properties, "event"))
throw new Error('Engine: addRule() argument requires "event" property');
if (!Object.prototype.hasOwnProperty.call(properties, "conditions"))
throw new Error(
'Engine: addRule() argument requires "conditions" property'
);
rule = new rule_default(properties);
}
rule.setEngine(this);
this.rules.push(rule);
this.prioritizedRules = null;
return this;
}
/**
* update a rule in the engine
* @param {object|Rule} rule - rule definition. Must be a instance of Rule
*/
updateRule(rule) {
const ruleIndex = this.rules.findIndex(
(ruleInEngine) => ruleInEngine.name === rule.name
);
if (ruleIndex > -1) {
this.rules.splice(ruleIndex, 1);
this.addRule(rule);
this.prioritizedRules = null;
} else {
throw new Error("Engine: updateRule() rule not found");
}
}
/**
* Remove a rule from the engine
* @param {object|Rule|string} rule - rule definition. Must be a instance of Rule
*/
removeRule(rule) {
let ruleRemoved = false;
if (!(rule instanceof rule_default)) {
const filteredRules = this.rules.filter(
(ruleInEngine) => ruleInEngine.name !== rule
);
ruleRemoved = filteredRules.length !== this.rules.length;
this.rules = filteredRules;
} else {
const index = this.rules.indexOf(rule);
if (index > -1) {
ruleRemoved = Boolean(this.rules.splice(index, 1).length);
}
}
if (ruleRemoved) {
this.prioritizedRules = null;
}
return ruleRemoved;
}
/**
* sets a condition that can be referenced by the given name.
* If a condition with the given name has already been set this will replace it.
* @param {string} name - the name of the condition to be referenced by rules.
* @param {object} conditions - the conditions to use when the condition is referenced.
*/
setCondition(name, conditions) {
if (!name) throw new Error("Engine: setCondition() requires name");
if (!conditions)
throw new Error("Engine: setCondition() requires conditions");
if (!Object.prototype.hasOwnProperty.call(conditions, "all") && !Object.prototype.hasOwnProperty.call(conditions, "any") && !Object.prototype.hasOwnProperty.call(conditions, "not") && !Object.prototype.hasOwnProperty.call(conditions, "condition")) {
throw new Error(
'"conditions" root must contain a single instance of "all", "any", "not", or "condition"'
);
}
this.conditions.set(name, new Condition(conditions));
return this;
}
/**
* Removes a condition that has previously been added to this engine
* @param {string} name - the name of the condition to remove.
* @returns true if the condition existed, otherwise false
*/
removeCondition(name) {
return this.conditions.delete(name);
}
/**
* Add a custom operator definition
* @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc
* @param {function(factValue, jsonValue)} callback - the method to execute when the operator is encountered.
*/
addOperator(operatorOrName, cb) {
this.operators.addOperator(operatorOrName, cb);
}
/**
* Remove a custom operator definition
* @param {string} operatorOrName - operator identifier within the condition; i.e. instead of 'equals', 'greaterThan', etc
*/
removeOperator(operatorOrName) {
return this.operators.removeOperator(operatorOrName);
}
/**
* Add a custom operator decorator
* @param {string} decoratorOrName - decorator identifier within the condition; i.e. instead of 'someFact', 'everyValue', etc
* @param {function(factValue, jsonValue, next)} callback - the method to execute when the decorator is encountered.
*/
addOperatorDecorator(decoratorOrName, cb) {
this.operators.addOperatorDecorator(decoratorOrName, cb);
}
/**
* Remove a custom operator decorator
* @param {string} decoratorOrName - decorator identifier within the condition; i.e. instead of 'someFact', 'everyValue', etc
*/
removeOperatorDecorator(decoratorOrName) {
return this.operators.removeOperatorDecorator(decoratorOrName);
}
/**
* Add a fact definition to the engine. Facts are called by rules as they are evaluated.
* @param {object|Fact} id - fact identifier or instance of Fact
* @param {function} definitionFunc - function to be called when computing the fact value for a given rule
* @param {Object} options - options to initialize the fact with. used when "id" is not a Fact instance
*/
addFact(id, valueOrMethod, options) {
let factId = id;
let fact;
if (id instanceof fact_default) {
factId = id.id;
fact = id;
} else {
fact = new fact_default(id, valueOrMethod, options);
}
debug_default("engine::addFact", { id: factId });
this.facts.set(factId, fact);
return this;
}
/**
* Remove a fact definition to the engine. Facts are called by rules as they are evaluated.
* @param {object|Fact} id - fact identifier or instance of Fact
*/
removeFact(factOrId) {
let factId;
if (!(factOrId instanceof fact_default)) {
factId = factOrId;
} else {
factId = factOrId.id;
}
return this.facts.delete(factId);
}
/**
* Iterates over the engine rules, organizing them by highest -> lowest priority
* @return {Rule[][]} two dimensional array of Rules.
* Each outer array element represents a single priority(integer). Inner array is
* all rules with that priority.
*/
prioritizeRules() {
if (!this.prioritizedRules) {
const ruleSets = this.rules.reduce((sets, rule) => {
const priority = rule.priority;
if (!sets[priority]) sets[priority] = [];
sets[priority].push(rule);
return sets;
}, {});
this.prioritizedRules = Object.keys(ruleSets).sort((a, b) => {
return Number(a) > Number(b) ? -1 : 1;
}).map((priority) => ruleSets[priority]);
}
return this.prioritizedRules;
}
/**
* Stops the rules engine from running the next priority set of Rules. All remaining rules will be resolved as undefined,
* and no further events emitted. Since rules of the same priority are evaluated in parallel(not series), other rules of
* the same priority may still emit events, even though the engine is in a "finished" state.
* @return {Engine}
*/
stop() {
this.status = FINISHED;
return this;
}
/**
* Returns a fact by fact-id
* @param {string} factId - fact identifier
* @return {Fact} fact instance, or undefined if no such fact exists
*/
getFact(factId) {
return this.facts.get(factId);
}
/**
* Runs an array of rules
* @param {Rule[]} array of rules to be evaluated
* @return {Promise} resolves when all rules in the array have been evaluated
*/
evaluateRules(ruleArray, almanac) {
return Promise.all(
ruleArray.map((rule) => {
if (this.status !== RUNNING) {
debug_default("engine::run, skipping remaining rules", {
status: this.status
});
return Promise.resolve();
}
return rule.evaluate(almanac).then((ruleResult) => {
debug_default("engine::run", { ruleResult: ruleResult.result });
almanac.addResult(ruleResult);
if (ruleResult.result) {
almanac.addEvent(ruleResult.event, "success");
return this.emitAsync(
"success",
ruleResult.event,
almanac,
ruleResult
).then(
() => this.emitAsync(
ruleResult.event.type,
ruleResult.event.params,
almanac,
ruleResult
)
);
} else {
almanac.addEvent(ruleResult.event, "failure");
return this.emitAsync(
"failure",
ruleResult.event,
almanac,
ruleResult
);
}
});
})
);
}
/**
* Runs the rules engine
* @param {Object} runtimeFacts - fact values known at runtime
* @param {Object} runOptions - run options
* @return {Promise} resolves when the engine has completed running
*/
run(runtimeFacts = {}, runOptions = {}) {
debug_default("engine::run started");
this.status = RUNNING;
const almanac = runOptions.almanac || new Almanac({
allowUndefinedFacts: this.allowUndefinedFacts,
pathResolver: this.pathResolver
});
this.facts.forEach((fact) => {
almanac.addFact(fact);
});
for (const factId in runtimeFacts) {
let fact;
if (runtimeFacts[factId] instanceof fact_default) {
fact = runtimeFacts[factId];
} else {
fact = new fact_default(factId, runtimeFacts[factId]);
}
almanac.addFact(fact);
debug_default("engine::run initialized runtime fact", {
id: fact.id,
value: fact.value,
type: typeof fact.value
});
}
const orderedSets = this.prioritizeRules();
let cursor = Promise.resolve();
return new Promise((resolve, reject) => {
orderedSets.map((set) => {
cursor = cursor.then(() => {
return this.evaluateRules(set, almanac);
}).catch(reject);
return cursor;
});
cursor.then(() => {
this.status = FINISHED;
debug_default("engine::run completed");
const ruleResults = almanac.getResults();
const { results, failureResults } = ruleResults.reduce(
(hash2, ruleResult) => {
const group = ruleResult.result ? "results" : "failureResults";
hash2[group].push(ruleResult);
return hash2;
},
{ results: [], failureResults: [] }
);
resolve({
almanac,
results,
failureResults,
events: almanac.getEvents("success"),
failureEvents: almanac.getEvents("failure")
});
}).catch(reject);
});
}
};
var engine_default = Engine;
// src/index.mjs
function src_default(rules, options) {
return new engine_default(rules, options);
}
export {
Almanac,
engine_default as Engine,
fact_default as Fact,
Operator,
OperatorDecorator,
rule_default as Rule,
src_default as default
};
//# sourceMappingURL=index.js.map
{
"name": "json-rules-engine",
"version": "7.0.0",
"version": "8.0.0-alpha.1",
"description": "Rules Engine expressed in simple json",
"main": "dist/index.js",
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "types/index.d.ts",
"type": "module",
"engines": {

@@ -11,9 +13,7 @@ "node": ">=18.0.0"

"scripts": {
"test": "mocha && npm run lint --silent && npm run test:types",
"test:types": "tsd",
"lint": "standard --verbose --env mocha | snazzy || true",
"lint:fix": "standard --fix --env mocha",
"prepublishOnly": "npm run build",
"build": "babel --stage 1 -d dist/ src/",
"watch": "babel --watch --stage 1 -d dist/ src",
"test": "vitest --typecheck",
"lint": "eslint",
"format": "prettier -w .",
"build": "tsup",
"watch": "tsup --watch",
"examples": "./test/support/example_runner.sh"

@@ -30,33 +30,5 @@ },

],
"standard": {
"parser": "babel-eslint",
"ignore": [
"/dist",
"/examples/node_modules"
],
"globals": [
"context",
"xcontext",
"describe",
"xdescribe",
"it",
"xit",
"before",
"beforeEach",
"expect",
"factories"
]
"publishConfig": {
"tag": "next"
},
"mocha": {
"require": [
"babel-core/register",
"babel-polyfill"
],
"file": "./test/support/bootstrap.js",
"checkLeaks": true,
"recursive": true,
"globals": [
"expect"
]
},
"author": "Cache Hamm <cache.hamm@gmail.com>",

@@ -72,22 +44,13 @@ "contributors": [

"devDependencies": {
"babel-cli": "6.26.0",
"babel-core": "6.26.3",
"babel-eslint": "10.1.0",
"babel-loader": "8.2.2",
"babel-polyfill": "6.26.0",
"babel-preset-es2015": "~6.24.1",
"babel-preset-stage-0": "~6.24.1",
"babel-register": "6.26.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"colors": "~1.4.0",
"dirty-chai": "2.0.1",
"@eslint/js": "^9.13.0",
"eslint": "^9.13.0",
"globals": "^15.11.0",
"lodash": "4.17.21",
"mocha": "^8.4.0",
"perfy": "^1.1.5",
"sinon": "^11.1.1",
"sinon-chai": "^3.7.0",
"snazzy": "^9.0.0",
"standard": "^16.0.3",
"tsd": "^0.17.0"
"prettier": "^3.3.3",
"tsd": "^0.17.0",
"tsup": "^8.3.0",
"typescript": "^5.6.3",
"typescript-eslint": "^8.11.0",
"vitest": "^2.1.3"
},

@@ -94,0 +57,0 @@ "dependencies": {

@@ -11,27 +11,27 @@ ![json-rules-engine](http://i.imgur.com/MAzq7l2.png)

* [Synopsis](#synopsis)
* [Features](#features)
* [Installation](#installation)
* [Docs](#docs)
* [Examples](#examples)
* [Basic Example](#basic-example)
* [Advanced Example](#advanced-example)
* [Debugging](#debugging)
* [Node](#node)
* [Browser](#browser)
* [Related Projects](#related-projects)
* [License](#license)
- [Synopsis](#synopsis)
- [Features](#features)
- [Installation](#installation)
- [Docs](#docs)
- [Examples](#examples)
- [Basic Example](#basic-example)
- [Advanced Example](#advanced-example)
- [Debugging](#debugging)
- [Node](#node)
- [Browser](#browser)
- [Related Projects](#related-projects)
- [License](#license)
## Synopsis
```json-rules-engine``` is a powerful, lightweight rules engine. Rules are composed of simple json structures, making them human readable and easy to persist.
`json-rules-engine` is a powerful, lightweight rules engine. Rules are composed of simple json structures, making them human readable and easy to persist.
## Features
* Rules expressed in simple, easy to read JSON
* Full support for ```ALL``` and ```ANY``` boolean operators, including recursive nesting
* Fast by default, faster with configuration; priority levels and cache settings for fine tuning performance
* Secure; no use of eval()
* Isomorphic; runs in node and browser
* Lightweight & extendable; 17kb gzipped w/few dependencies
- Rules expressed in simple, easy to read JSON
- Full support for `ALL` and `ANY` boolean operators, including recursive nesting
- Fast by default, faster with configuration; priority levels and cache settings for fine tuning performance
- Secure; no use of eval()
- Isomorphic; runs in node and browser
- Lightweight & extendable; 17kb gzipped w/few dependencies

@@ -60,9 +60,8 @@ ## Installation

```js
const { Engine } = require('json-rules-engine')
const { Engine } = require("json-rules-engine");
/**
* Setup a new engine
*/
let engine = new Engine()
let engine = new Engine();

@@ -73,31 +72,41 @@ // define a rule for detecting the player has exceeded foul limits. Foul out any player who:

conditions: {
any: [{
all: [{
fact: 'gameDuration',
operator: 'equal',
value: 40
}, {
fact: 'personalFoulCount',
operator: 'greaterThanInclusive',
value: 5
}]
}, {
all: [{
fact: 'gameDuration',
operator: 'equal',
value: 48
}, {
fact: 'personalFoulCount',
operator: 'greaterThanInclusive',
value: 6
}]
}]
any: [
{
all: [
{
fact: "gameDuration",
operator: "equal",
value: 40,
},
{
fact: "personalFoulCount",
operator: "greaterThanInclusive",
value: 5,
},
],
},
{
all: [
{
fact: "gameDuration",
operator: "equal",
value: 48,
},
{
fact: "personalFoulCount",
operator: "greaterThanInclusive",
value: 6,
},
],
},
],
},
event: { // define the event to fire when the conditions evaluate truthy
type: 'fouledOut',
event: {
// define the event to fire when the conditions evaluate truthy
type: "fouledOut",
params: {
message: 'Player has fouled out!'
}
}
})
message: "Player has fouled out!",
},
},
});

@@ -110,11 +119,9 @@ /**

personalFoulCount: 6,
gameDuration: 40
}
gameDuration: 40,
};
// Run the engine to evaluate
engine
.run(facts)
.then(({ events }) => {
events.map(event => console.log(event.params.message))
})
engine.run(facts).then(({ events }) => {
events.map((event) => console.log(event.params.message));
});

@@ -134,3 +141,3 @@ /*

This demonstrates an engine which uses asynchronous fact data.
This demonstrates an engine which uses asynchronous fact data.
Fact information is loaded via API call during runtime, and the results are cached and recycled for all 3 conditions.

@@ -140,6 +147,6 @@ It also demonstates use of the condition _path_ feature to reference properties of objects returned by facts.

```js
const { Engine } = require('json-rules-engine')
const { Engine } = require("json-rules-engine");
// example client for making asynchronous requests to an api, database, etc
import apiClient from './account-api-client'
import apiClient from "./account-api-client";

@@ -149,3 +156,3 @@ /**

*/
let engine = new Engine()
let engine = new Engine();

@@ -160,27 +167,31 @@ /**

conditions: {
all: [{
fact: 'account-information',
operator: 'equal',
value: 'microsoft',
path: '$.company' // access the 'company' property of "account-information"
}, {
fact: 'account-information',
operator: 'in',
value: ['active', 'paid-leave'], // 'status' can be active or paid-leave
path: '$.status' // access the 'status' property of "account-information"
}, {
fact: 'account-information',
operator: 'contains', // the 'ptoDaysTaken' property (an array) must contain '2016-12-25'
value: '2016-12-25',
path: '$.ptoDaysTaken' // access the 'ptoDaysTaken' property of "account-information"
}]
all: [
{
fact: "account-information",
operator: "equal",
value: "microsoft",
path: "$.company", // access the 'company' property of "account-information"
},
{
fact: "account-information",
operator: "in",
value: ["active", "paid-leave"], // 'status' can be active or paid-leave
path: "$.status", // access the 'status' property of "account-information"
},
{
fact: "account-information",
operator: "contains", // the 'ptoDaysTaken' property (an array) must contain '2016-12-25'
value: "2016-12-25",
path: "$.ptoDaysTaken", // access the 'ptoDaysTaken' property of "account-information"
},
],
},
event: {
type: 'microsoft-christmas-pto',
type: "microsoft-christmas-pto",
params: {
message: 'current microsoft employee taking christmas day off'
}
}
}
engine.addRule(microsoftRule)
message: "current microsoft employee taking christmas day off",
},
},
};
engine.addRule(microsoftRule);

@@ -193,17 +204,16 @@ /**

*/
engine.addFact('account-information', function (params, almanac) {
console.log('loading account information...')
return almanac.factValue('accountId')
.then((accountId) => {
return apiClient.getAccountInformation(accountId)
})
})
engine.addFact("account-information", function (params, almanac) {
console.log("loading account information...");
return almanac.factValue("accountId").then((accountId) => {
return apiClient.getAccountInformation(accountId);
});
});
// define fact(s) known at runtime
let facts = { accountId: 'lincoln' }
engine
.run(facts)
.then(({ events }) => {
console.log(facts.accountId + ' is a ' + events.map(event => event.params.message))
})
let facts = { accountId: "lincoln" };
engine.run(facts).then(({ events }) => {
console.log(
facts.accountId + " is a " + events.map((event) => event.params.message),
);
});

@@ -231,5 +241,6 @@ /*

### Browser
```js
// set debug flag in local storage & refresh page to see console output
localStorage.debug = 'json-rules-engine'
localStorage.debug = "json-rules-engine";
```

@@ -243,4 +254,4 @@

## License
## License
[ISC](./LICENSE)

@@ -0,1 +1,3 @@

/* eslint-disable @typescript-eslint/no-explicit-any */
export interface AlmanacOptions {

@@ -25,3 +27,3 @@ allowUndefinedFacts?: boolean;

rules: Array<RuleProperties>,
options?: EngineOptions
options?: EngineOptions,
): Engine;

@@ -42,3 +44,3 @@

operatorName: string,
callback: OperatorEvaluator<A, B>
callback: OperatorEvaluator<A, B>,
): void;

@@ -48,3 +50,6 @@ removeOperator(operator: Operator | string): boolean;

addOperatorDecorator(decorator: OperatorDecorator): void;
addOperatorDecorator<A, B, NextA, NextB>(decoratorName: string, callback: OperatorDecoratorEvaluator<A, B, NextA, NextB>): void;
addOperatorDecorator<A, B, NextA, NextB>(
decoratorName: string,
callback: OperatorDecoratorEvaluator<A, B, NextA, NextB>,
): void;
removeOperatorDecorator(decorator: OperatorDecorator | string): boolean;

@@ -56,3 +61,3 @@

valueCallback: DynamicFactCallback<T> | T,
options?: FactOptions
options?: FactOptions,
): this;

@@ -64,3 +69,6 @@ removeFact(factOrId: string | Fact): boolean;

run(facts?: Record<string, any>, runOptions?: RunOptions): Promise<EngineResult>;
run(
facts?: Record<string, any>,
runOptions?: RunOptions,
): Promise<EngineResult>;
stop(): this;

@@ -78,3 +86,3 @@ }

evaluator: OperatorEvaluator<A, B>,
validator?: (factValue: A) => boolean
validator?: (factValue: A) => boolean,
);

@@ -84,6 +92,15 @@ }

export interface OperatorDecoratorEvaluator<A, B, NextA, NextB> {
(factValue: A, compareToValue: B, next: OperatorEvaluator<NextA, NextB>): boolean
(
factValue: A,
compareToValue: B,
next: OperatorEvaluator<NextA, NextB>,
): boolean;
}
export class OperatorDecorator<A = unknown, B = unknown, NextA = unknown, NextB = unknown> {
export class OperatorDecorator<
A = unknown,
B = unknown,
NextA = unknown,
NextB = unknown,
> {
public name: string;

@@ -93,4 +110,4 @@ constructor(

evaluator: OperatorDecoratorEvaluator<A, B, NextA, NextB>,
validator?: (factValue: A) => boolean
)
validator?: (factValue: A) => boolean,
);
}

@@ -103,3 +120,3 @@

params?: Record<string, any>,
path?: string
path?: string,
): Promise<T>;

@@ -110,3 +127,3 @@ addFact<T>(fact: Fact<T>): this;

valueCallback: DynamicFactCallback<T> | T,
options?: FactOptions
options?: FactOptions,
): this;

@@ -123,3 +140,3 @@ addRuntimeFact(factId: string, value: any): void;

params: Record<string, any>,
almanac: Almanac
almanac: Almanac,
) => T;

@@ -137,3 +154,3 @@

value: T | DynamicFactCallback<T>,
options?: FactOptions
options?: FactOptions,
);

@@ -152,3 +169,3 @@ }

almanac: Almanac,
ruleResult: RuleResult
ruleResult: RuleResult,
) => void;

@@ -188,3 +205,3 @@

toJSON<T extends boolean>(
stringify: T
stringify: T,
): T extends true ? string : RuleSerializable;

@@ -191,0 +208,0 @@ }

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc