json-rules-engine
Advanced tools
Comparing version 7.0.0 to 8.0.0-alpha.1
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 |
1349
dist/index.js
@@ -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": { |
205
README.md
@@ -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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
287117
11
3038
247
Yes
14
1
6
1