@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
JavaScript
;
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
});