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.

275 lines (271 loc) 8.42 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { Engine: () => Engine, endsWith: () => endsWith, get: () => get, includes: () => includes, memoize: () => memoize, startsWith: () => startsWith, typeGuardCondition: () => typeGuardCondition }); module.exports = __toCommonJS(index_exports); // src/helper.ts var import_jmespath = require("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 (0, import_jmespath.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 var import_lru_cache = require("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 import_lru_cache.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; } } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Engine, endsWith, get, includes, memoize, startsWith, typeGuardCondition });