UNPKG

@arunkumar_h/rule-engine

Version:

A lightweight and extensible rule engine built with TypeScript and Node.js. Define complex business rules and evaluate conditions easily using a simple JSON structure.

242 lines (240 loc) 7.17 kB
// src/helper.ts import { search } from "jmespath"; function typeGuardCondition(condition) { return typeof condition === "object" && ("and" in condition || "or" in condition); } function memoize(cache, fn, resolver = (...args) => JSON.stringify(args)) { return function(...args) { const key = resolver(...args); if (cache.has(key)) return cache.get(key); const result = fn(...args); cache.set(key, result); return result; }; } function includes(collection, value) { if (typeof collection === "string") { return collection.includes(value); } if (Array.isArray(collection)) { return collection.indexOf(value) !== -1; } if (typeof collection === "object" && collection !== null) { return Object.values(collection).includes(value); } return false; } function endsWith(str, target, position) { if (typeof str !== "string" || typeof target !== "string") return false; const len = position !== void 0 ? Math.min(position, str.length) : str.length; return str.slice(0, len).endsWith(target); } function startsWith(str, target, position) { if (typeof str !== "string" || typeof target !== "string") return false; const len = position !== void 0 ? Math.min(position, str.length) : 0; return str.slice(len, str.length).startsWith(target); } function get(fact, path) { return search(fact, path); } var defaultOperators = { "%like%": (a, b) => includes(a, b), "%like": (a, b) => endsWith(a, b), "like%": (a, b) => startsWith(a, b), "===": (a, b) => a === b, "==": (a, b) => a == b, "!==": (a, b) => a !== b, "!=": (a, b) => a != b, ">": (a, b) => a > b, ">=": (a, b) => a >= b, "<": (a, b) => a < b, "<=": (a, b) => a <= b, in: (a, b) => includes(b, a), "!in": (a, b) => !includes(b, a), includes: (a, b) => includes(a, b) }; // src/index.ts import { LRUCache } from "lru-cache"; var Engine = class { constructor(cacheOptions = {}) { this.namedRules = /* @__PURE__ */ new Map(); this.namedConditions = /* @__PURE__ */ new Map(); this.namedOperators = new Map( Object.entries(defaultOperators) ); this.cache = null; this.cacheOption = { max: 500, ttl: 1e3 * 60 * 5, allowStale: false, updateAgeOnGet: false, updateAgeOnHas: false // maxSize: 5000, // for use with tracking overall storage size // sizeCalculation: (value, key) => { // return 1; // }, // for use when you need to clean up something when objects // are evicted from the cache // dispose: (value, key, reason) => { // freeFromMemoryOrWhatever(value); // }, // for use when you need to know that an item is being inserted // note that this does NOT allow you to prevent the insertion, // it just allows you to know about it. // onInsert: (value: any, key: any, reason: any) => {}, // async method to use for cache.fetch(), for // stale-while-revalidate type of behavior // fetchMethod: async (key, staleValue, { options, signal, context }) => {}, }; this.cache = new LRUCache({ ...this.cacheOption, ...cacheOptions }); } get rule() { return Object.fromEntries(this.namedRules); } get condition() { return Object.fromEntries(this.namedConditions); } get operator() { return Object.fromEntries(this.namedOperators); } add(key, data) { if ("condition" in data) { if (this.namedRules.has(key)) { throw new Error(`Rule ${key} already exists`); } this.namedRules.set(key, data); } else if ("and" in data || "or" in data) { if (this.namedConditions.has(key)) { throw new Error(`Condition ${key} already exists`); } this.namedConditions.set(key, data); } else if (typeof data === "function") { if (this.namedOperators.has(key)) { throw new Error(`Operator ${key} already exists`); } this.namedOperators.set(key, data); } else { throw new Error(`Invalid data type: ${typeof data}`); } } addLoop(list) { for (const key in list) { if (Object.prototype.hasOwnProperty.call(list, key)) { this.add(key, list[key]); } } } async executeOperation(fact, { path, operator, value }) { const actual = get(fact, path); const fn = this.namedOperators.get(operator); if (!fn) { throw new Error(`Operator "${operator}" not found`); } return fn(actual, value); } async executeConditionOperation(fact, cond) { if (typeof cond === "string" || "and" in cond || "or" in cond) { return this.evaluateRule(fact, cond); } else if ("operator" in cond) { return this.executeOperation(fact, cond); } } async evaluateRule(fact, condition) { let namedCondition; if (typeof condition === "string") { namedCondition = this.namedConditions.get(condition); } else { namedCondition = condition; } if (!namedCondition) { throw new Error(`Condition "${condition}" not found`); } if (typeGuardCondition(namedCondition)) { if ("and" in namedCondition) { return (await Promise.all( namedCondition.and.map( async (cond) => this.executeConditionOperation(fact, cond) ) )).every((result) => result); } else { return (await Promise.all( namedCondition.or.map( async (cond) => this.executeConditionOperation(fact, cond) ) )).some((result) => result); } } throw new Error(`Condition "${JSON.stringify(condition)}" is not valid`); } async memoize(resolver) { const self = this; return async function(...args) { const key = resolver(...args); if (self.cache?.has(key)) { return self.cache.get(key); } const result = await self.evaluateRule(args[0], args[1]); if (self.cache) { self.cache.set(key, result); } return result; }; } async cachedEvaluateRule(ruleName, rule) { const cacheMethod = rule.cache ?? true; if (cacheMethod === false) { return this.evaluateRule; } return this.memoize( (fact) => `${ruleName}-${JSON.stringify(fact)}` ); } addRule(list) { this.addLoop(list); return this; } addCondition(list) { this.addLoop(list); return this; } addOperator(list) { this.addLoop(list); return this; } async run(fact, ruleName) { const rule = this.namedRules.get(ruleName); if (!rule) { throw new Error(`Rule "${ruleName}" not found`); } const resultCallback = await this.cachedEvaluateRule(ruleName, rule); const result = await Reflect.apply(resultCallback, this, [ fact, rule.condition ]); if (result) { if (typeof rule.onSuccess === "function") { return rule.onSuccess(fact, ruleName); } else { return rule.onSuccess; } } if (typeof rule.onFail === "function") { return rule.onFail(fact, ruleName); } else { return rule.onFail; } } }; export { Engine, endsWith, get, includes, memoize, startsWith, typeGuardCondition };