UNPKG

rule-engine-js

Version:

A high-performance, secure rule engine with dynamic field comparison support

3 lines (2 loc) 17.7 kB
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).RuleEngineJS={})}(this,function(t){"use strict";class e{constructor(t={}){this.allowPrototypeAccess=t.allowPrototypeAccess||!1,this.enableCache=!1!==t.enableCache,this.maxCacheSize=t.maxCacheSize||1e3,this.cache=this.enableCache?new Map:null,this.NOT_FOUND=Symbol("PATH_NOT_FOUND"),this.PROTOTYPE_PROPS=new Set(["__proto__","constructor","prototype"])}resolve(t,e,r=void 0){if(!this.t(t)||!this.i(e))return r;if(this.cache){const r=this.h(t,e);if(this.cache.has(r))return this.cache.get(r)}try{const s=this.o(t,e),i=s===this.NOT_FOUND?r:s;return this.u(t,e,i),i}catch(t){return r}}resolveValue(t,e,r=void 0){if("string"!=typeof e)return e;const s=this.resolve(t,e,this.NOT_FOUND);return s!==this.NOT_FOUND?s:void 0!==r?r:e}resolveValueOrLiteral(t,e){return this.resolveValue(t,e,e)}resolveValueOrDefault(t,e,r){return this.resolveValue(t,e,r)}clearCache(){this.cache&&this.cache.clear()}getCacheStats(){return this.cache?{size:this.cache.size,maxSize:this.maxCacheSize,hitRate:this.l()}:null}t(t){return null!==t&&"object"==typeof t&&!Array.isArray(t)}i(t){return"string"==typeof t&&t.length>0&&!t.startsWith(".")&&!t.endsWith(".")}o(t,e){const r=e.split(".");let s=t;for(const t of r){if(null==s)return this.NOT_FOUND;if(!this.allowPrototypeAccess&&this.PROTOTYPE_PROPS.has(t))return this.NOT_FOUND;if("object"==typeof s&&!Object.prototype.hasOwnProperty.call(s,t))return this.NOT_FOUND;if("function"==typeof s[t]&&!this.allowPrototypeAccess)return this.NOT_FOUND;s=s[t]}return s}h(t,e){return`${this.p(t)}:${e}`}p(t){if(t.N)return String(t.N);if(t.id)return String(t.id);const e=Object.keys(t).sort();return`${e.join(",")}:${e.map(e=>typeof t[e]).join(",")}:${e.length}`}u(t,e,r){if(!this.cache)return;if(this.cache.size>=this.maxCacheSize){const t=this.cache.keys().next().value;this.cache.delete(t)}const s=this.h(t,e);this.cache.set(s,r)}l(){return 0}}class r extends Error{constructor(t,e=null,s={},i=null){super(t),this.name="RuleEngineError",this.operator=e,this.context=s,this.originalError=i,this.timestamp=(new Date).toISOString(),Error.captureStackTrace&&Error.captureStackTrace(this,r)}toJSON(){return{name:this.name,message:this.message,operator:this.operator,context:this.context,timestamp:this.timestamp}}}class s extends r{constructor(t,e,r={},s=null){super(t,e,r,s),this.name="OperatorError"}}const i={EQ:"eq",NEQ:"neq",GT:"gt",GTE:"gte",LT:"lt",LTE:"lte",IN:"in",NOT_IN:"notIn",AND:"and",OR:"or",NOT:"not",CONTAINS:"contains",STARTS_WITH:"startsWith",ENDS_WITH:"endsWith",REGEX:"regex",BETWEEN:"between",IS_NULL:"isNull",IS_NOT_NULL:"isNotNull"},n={maxDepth:10,maxOperators:100,maxCacheSize:1e3,enableCache:!0,enableDebug:!1,strict:!0,allowPrototypeAccess:!1};class h{constructor(t={}){this.config={...n,...t},this.pathResolver=new e(this.config),this.operators=new Map,this.metrics={evaluations:0,cacheHits:0,errors:0,totalTime:0,avgTime:0},this.expressionCache=this.config.enableCache?new Map:null}evaluateExpr(t,e,r=0){const s=performance.now();this.metrics.evaluations++;try{this.T(t,r);const i=this.m(t,e);if(i)return this.O(s,!0),i;const n=this.S(t,e,r);return this._(t,e,n),this.O(s,!1),n}catch(r){return this.metrics.errors++,this.O(s,!1),this.$(r,t,e)}}registerOperator(t,e,s={}){if(!s.allowOverwrite&&this.operators.has(t))throw new r(`Operator '${t}' already exists`,t);if("function"!=typeof e)throw new r("Operator handler must be a function",t);this.operators.set(t,e)}getOperators(){return Array.from(this.operators.keys())}getMetrics(){return{...this.metrics}}clearCache(){this.expressionCache&&this.expressionCache.clear(),this.pathResolver.clearCache()}getConfig(){return{...this.config}}getCacheStats(){return{expression:this.expressionCache?{size:this.expressionCache.size,maxSize:this.config.maxCacheSize}:null,path:this.pathResolver.getCacheStats()}}T(t,e){if(e>this.config.maxDepth)throw new r(`Rule exceeds maximum depth of ${this.config.maxDepth}`,null,{depth:e,maxDepth:this.config.maxDepth});if(!t||"object"!=typeof t||Array.isArray(t))throw new r("Rule must be a non-null object",null,{rule:t});if(0===Object.keys(t).length)throw new r("Rule must contain at least one operator",null,{rule:t});const s=this.A(t);if(s>this.config.maxOperators)throw new r(`Rule exceeds maximum operators of ${this.config.maxOperators}`,null,{operatorCount:s,maxOperators:this.config.maxOperators})}A(t,e=0){if(!t||"object"!=typeof t)return e;for(const[r,s]of Object.entries(t))if(this.operators.has(r)&&(e++,Array.isArray(s)))for(const t of s)e=this.A(t,e);return e}m(t,e){if(!this.expressionCache)return null;const r=this.R(t,e);return this.expressionCache.get(r)||null}_(t,e,r){if(!this.expressionCache||!r.success)return;const s=this.R(t,e);if(this.expressionCache.size>=this.config.maxCacheSize){const t=this.expressionCache.keys().next().value;this.expressionCache.delete(t)}this.expressionCache.set(s,r)}S(t,e,s){const i=Object.keys(t);for(const n of i){const i=t[n],h=this.operators.get(n);if(!h)throw new r(`Unknown operator: ${n}`,n,{args:i});if(!Array.isArray(i))throw new r(`Invalid arguments for operator ${n}`,n,{args:i,type:typeof i});try{if(!h(i,e,this.evaluateExpr.bind(this),s))return{success:!1,operator:n,details:{args:i,context:e}}}catch(t){throw new r(`Error in operator ${n}: ${t.message}`,n,{args:i,context:e},t)}}return{success:!0}}R(t,e){try{return`expr:${JSON.stringify(t)}:ctx:${this.p(e)}`}catch(t){return`fallback:${Date.now()}:${Math.random()}`}}p(t){if(t&&"object"==typeof t){if(t.N)return String(t.N);if(t.id)return String(t.id);const e=Object.keys(t).sort();return`keys:${e.join(",")}:count:${e.length}:hash:${this.I(t)}`}return"default"}I(t,e=0,r=new WeakSet){if(e>4)return"deep";if(null==t)return"null";if("object"!=typeof t){const e=String(t);return e.length>50?e.substring(0,47)+"...":e}if(r.has(t))return"circular";r.add(t);try{if(Array.isArray(t)){const s=t.slice(0,5).map(t=>this.I(t,e+1,r)),i=t.length>5?`+${t.length-5}more`:"";return`[${s.join(",")}${i}]`}const s=Object.keys(t).sort().slice(0,10).map(s=>`${s}:${this.I(t[s],e+1,r)}`),i=Object.keys(t).length>10?"+more":"";return`{${s.join("|")}${i}}`}finally{r.delete(t)}}$(t,e,s){const i=t instanceof r?t:new r("Expression evaluation failed",null,{expr:e,context:s},t);return this.config.enableDebug,{success:!1,operator:i.operator,error:i.message,details:i.context,timestamp:i.timestamp}}O(t,e){const r=performance.now()-t;this.metrics.totalTime+=r,this.metrics.avgTime=this.metrics.totalTime/this.metrics.evaluations,e&&this.metrics.cacheHits++}}class o{static coerceToNumber(t,e=!1){if(e)return"number"!=typeof t||isNaN(t)?null:t;if(null==t||""===t)return null;const r=parseFloat(t);return isNaN(r)?null:r}static coerceToString(t,e=!1){return e?"string"==typeof t?t:null:null==t?null:String(t)}static coerceToBoolean(t,e=!1){return e?"boolean"==typeof t?t:null:Boolean(t)}static isEqual(t,e,r=!1){return r?t===e:t==e}static isArray(t){return Array.isArray(t)}static isObject(t){return null!==t&&"object"==typeof t&&!Array.isArray(t)}static isString(t){return"string"==typeof t}static isNumber(t){return"number"==typeof t&&!isNaN(t)}static isBoolean(t){return"boolean"==typeof t}static isNull(t){return null==t}}class a{constructor(t,e){this.pathResolver=t,this.config=e}validateArgs(t,e,r){if(!Array.isArray(t))throw new s(`${r} operator requires array arguments`,r,{args:t,type:typeof t});if(void 0!==e)if(Array.isArray(e)){const[i,n]=e;if(t.length<i||t.length>n)throw new s(`${r} operator requires ${i}-${n} arguments, got ${t.length}`,r,{args:t,expectedRange:e,actualLength:t.length})}else if(t.length!==e)throw new s(`${r} operator requires ${e} arguments, got ${t.length}`,r,{args:t,expectedLength:e,actualLength:t.length})}resolveOperands(t,e,r,i="literal",n=void 0){if("literal"===i)return{left:this.pathResolver.resolveValueOrLiteral(t,e),right:this.pathResolver.resolveValueOrLiteral(t,r)};if("default"===i)return{left:this.pathResolver.resolveValueOrDefault(t,e,n),right:this.pathResolver.resolveValueOrDefault(t,r,n)};throw new s("Invalid resolution strategy",null,{strategy:i})}isStrictMode(t={}){return"boolean"==typeof t.strict?t.strict:!1!==this.config.strict}coerceToNumbers(t,e,r,i){const n=o.coerceToNumber(t,r),h=o.coerceToNumber(e,r);if(null===n||null===h)throw new s(`${i} operator requires numeric operands`,i,{left:t,right:e,leftType:typeof t,rightType:typeof e,strict:r,leftCoerced:n,rightCoerced:h});return{left:n,right:h}}}class u extends a{register(t){const{EQ:e,NEQ:r}=i;t.registerOperator(e,this.createEqualityOperator(!0).bind(this)),t.registerOperator(r,this.createEqualityOperator(!1).bind(this))}createEqualityOperator(t){return(e,r)=>{this.validateArgs(e,[2,3],t?"EQ":"NEQ");const[s,i,n={}]=e,h=this.isStrictMode(n),{left:a,right:u}=this.resolveOperands(r,s,i,"literal"),l=o.isEqual(a,u,h);return t?l:!l}}}class l extends a{register(t){const{GT:e,GTE:r,LT:s,LTE:n}=i;t.registerOperator(e,this.createNumericOperator("GT").bind(this)),t.registerOperator(r,this.createNumericOperator("GTE").bind(this)),t.registerOperator(s,this.createNumericOperator("LT").bind(this)),t.registerOperator(n,this.createNumericOperator("LTE").bind(this))}createNumericOperator(t){return(e,r)=>{this.validateArgs(e,[2,3],t);const[s,i,n={}]=e,h=this.isStrictMode(n),{left:o,right:a}=this.resolveOperands(r,s,i,"literal"),{left:u,right:l}=this.coerceToNumbers(o,a,h,t);switch(t){case"GT":return u>l;case"GTE":return u>=l;case"LT":return u<l;case"LTE":return u<=l;default:throw new Error(`Unknown numeric operator: ${t}`)}}}}class c extends a{register(t){const{AND:e,OR:r,NOT:s}=i;t.registerOperator(e,this.createAndOperator().bind(this)),t.registerOperator(r,this.createOrOperator().bind(this)),t.registerOperator(s,this.createNotOperator().bind(this))}createAndOperator(){return(t,e,r,i)=>{if(!Array.isArray(t)||0===t.length)throw new s("AND operator requires at least one argument","AND",{args:t});for(const s of t)if(!r(s,e,i+1).success)return!1;return!0}}createOrOperator(){return(t,e,r,i)=>{if(!Array.isArray(t)||0===t.length)throw new s("OR operator requires at least one argument","OR",{args:t});for(const s of t)if(r(s,e,i+1).success)return!0;return!1}}createNotOperator(){return(t,e,r,s)=>{this.validateArgs(t,1,"NOT");const[i]=t;return!r(i,e,s+1).success}}}class f extends a{register(t){const{CONTAINS:e,STARTS_WITH:r,ENDS_WITH:s}=i;t.registerOperator(e,this.createStringOperator("CONTAINS").bind(this)),t.registerOperator(r,this.createStringOperator("STARTS_WITH").bind(this)),t.registerOperator(s,this.createStringOperator("ENDS_WITH").bind(this))}createStringOperator(t){return(e,r)=>{this.validateArgs(e,[2,3],t);const[s,i,n={}]=e,h=this.isStrictMode(n),{left:o,right:a}=this.resolveOperands(r,s,i,"literal"),{left:u,right:l}=this.coerceToStrings(o,a,h,t);switch(t){case"CONTAINS":return u.includes(l);case"STARTS_WITH":return u.startsWith(l);case"ENDS_WITH":return u.endsWith(l);default:throw new Error(`Unknown string operator: ${t}`)}}}coerceToStrings(t,e,r,i){const n=o.coerceToString(t,r),h=o.coerceToString(e,r);if(null===n||null===h)throw new s(`${i} operator requires string operands`,i,{left:t,right:e,leftType:typeof t,rightType:typeof e,strict:r});return{left:n,right:h}}}class p extends a{constructor(t,e){super(t,e),this.regexCache=new Map,this.maxCacheSize=e.maxCacheSize||1e3}register(t){const{REGEX:e}=i;t.registerOperator(e,this.createRegexOperator().bind(this))}createRegexOperator(){return(t,e)=>{if(!Array.isArray(t)||t.length<2||t.length>3)throw new s("REGEX operator requires 2 or 3 arguments","REGEX",{args:t,actualLength:t.length});const[r,i,n={}]=t,{left:h,right:a}=this.resolveOperands(e,r,i,"literal"),u=this.isStrictMode(n),l=o.coerceToString(h,u),c=o.coerceToString(a,u);if(null===l||null===c)throw new s("REGEX operator requires valid text and pattern","REGEX",{text:h,pattern:a});try{const t=n.flags||"";return this.getCompiledRegex(c,t).test(l)}catch(t){throw new s(`Invalid regex pattern: ${c}`,"REGEX",{pattern:c,text:l,flags:n.flags},t)}}}getCompiledRegex(t,e=""){const r=`${t}:::${e}`;if(this.regexCache.has(r))return this.regexCache.get(r);const s=new RegExp(t,e);if(this.regexCache.size>=this.maxCacheSize){const t=this.regexCache.keys().next().value;this.regexCache.delete(t)}return this.regexCache.set(r,s),s}clearCache(){this.regexCache.clear()}getCacheStats(){return{size:this.regexCache.size,maxSize:this.maxCacheSize}}}class g extends a{register(t){const{IN:e,NOT_IN:r}=i;t.registerOperator(e,this.createArrayOperator("IN").bind(this)),t.registerOperator(r,this.createArrayOperator("NOT_IN").bind(this))}createArrayOperator(t){return(e,r)=>{this.validateArgs(e,[2,3],t);const[i,n,h={}]=e,a=this.isStrictMode(h),{left:u,right:l}=this.resolveOperands(r,i,n,"literal");if(!Array.isArray(l))throw new s(`${t} operator requires array as right operand`,t,{left:u,right:l,rightType:typeof l,originalRight:n});const c=l.some(t=>o.isEqual(t,u,a));return"IN"===t?c:!c}}}class w extends a{register(t){const{BETWEEN:e,IS_NULL:r,IS_NOT_NULL:s}=i;t.registerOperator(e,this.createBetweenOperator().bind(this)),t.registerOperator(r,this.createNullCheckOperator("IS_NULL").bind(this)),t.registerOperator(s,this.createNullCheckOperator("IS_NOT_NULL").bind(this))}createBetweenOperator(){return(t,e)=>{this.validateArgs(t,2,"BETWEEN");const[r,i,n={}]=t,h=this.isStrictMode(n),a=this.pathResolver.resolveValueOrLiteral(e,r),u=this.pathResolver.resolveValueOrLiteral(e,i);if(!Array.isArray(u)||2!==u.length)throw new s("BETWEEN operator requires array of 2 values","BETWEEN",{range:u,originalRange:i,rangeType:typeof u});const[l,c]=u,f=this.pathResolver.resolveValueOrLiteral(e,l),p=this.pathResolver.resolveValueOrLiteral(e,c),g=o.coerceToNumber(a,h),w=o.coerceToNumber(f,h),E=o.coerceToNumber(p,h);if(null===g||null===w||null===E)throw new s("BETWEEN operator requires numeric operands","BETWEEN",{value:a,min:f,max:p,strict:h,valueCoerced:g,minCoerced:w,maxCoerced:E});return g>=w&&g<=E}}createNullCheckOperator(t){return(e,r)=>{this.validateArgs(e,1,t);const[s]=e;if("string"!=typeof s){const e=null==s;return"IS_NULL"===t?e:!e}const i=this.pathResolver.resolve(r,s,this.pathResolver.NOT_FOUND),n=i===this.pathResolver.NOT_FOUND||null==i;return"IS_NULL"===t?n:!n}}}class E{constructor(){this.ops=i,this.L(),this.C()}eq(t,e,r){return arguments.length>2?{[this.ops.EQ]:[t,e,r||{}]}:{[this.ops.EQ]:[t,e]}}neq(t,e,r){return arguments.length>2?{[this.ops.NEQ]:[t,e,r||{}]}:{[this.ops.NEQ]:[t,e]}}gt(t,e,r){return arguments.length>2?{[this.ops.GT]:[t,e,r||{}]}:{[this.ops.GT]:[t,e]}}gte(t,e,r){return arguments.length>2?{[this.ops.GTE]:[t,e,r||{}]}:{[this.ops.GTE]:[t,e]}}lt(t,e,r){return arguments.length>2?{[this.ops.LT]:[t,e,r||{}]}:{[this.ops.LT]:[t,e]}}lte(t,e,r){return arguments.length>2?{[this.ops.LTE]:[t,e,r||{}]}:{[this.ops.LTE]:[t,e]}}and(...t){return{[this.ops.AND]:t}}or(...t){return{[this.ops.OR]:t}}not(t){return{[this.ops.NOT]:[t]}}contains(t,e,r){return arguments.length>2?{[this.ops.CONTAINS]:[t,e,r||{}]}:{[this.ops.CONTAINS]:[t,e]}}startsWith(t,e,r){return arguments.length>2?{[this.ops.STARTS_WITH]:[t,e,r||{}]}:{[this.ops.STARTS_WITH]:[t,e]}}endsWith(t,e,r){return arguments.length>2?{[this.ops.ENDS_WITH]:[t,e,r||{}]}:{[this.ops.ENDS_WITH]:[t,e]}}regex(t,e,r){return arguments.length>2?{[this.ops.REGEX]:[t,e,r||{}]}:{[this.ops.REGEX]:[t,e]}}in(t,e,r){return arguments.length>2?{[this.ops.IN]:[t,e,r||{}]}:{[this.ops.IN]:[t,e]}}notIn(t,e,r){return arguments.length>2?{[this.ops.NOT_IN]:[t,e,r||{}]}:{[this.ops.NOT_IN]:[t,e]}}between(t,e,r){return arguments.length>2?{[this.ops.BETWEEN]:[t,e,r||{}]}:{[this.ops.BETWEEN]:[t,e]}}isNull(t){return{[this.ops.IS_NULL]:[t]}}isNotNull(t){return{[this.ops.IS_NOT_NULL]:[t]}}isTrue(t){return this.eq(t,!0)}isFalse(t){return this.eq(t,!1)}isEmpty(t){return this.eq(t,"")}isNotEmpty(t){return this.neq(t,"")}exists(t){return this.and(this.isNotNull(t),this.neq(t,""),this.neq(t,!1))}L(){this.field={equals:(t,e,r)=>this.eq(t,e,r),greaterThan:(t,e,r)=>this.gt(t,e,r),greaterThanOrEqual:(t,e,r)=>this.gte(t,e,r),lessThan:(t,e,r)=>this.lt(t,e,r),lessThanOrEqual:(t,e,r)=>this.lte(t,e,r)}}C(){this.validation={email:t=>this.regex(t,"^[\\w\\.-]+@[\\w\\.-]+\\.[a-zA-Z]{2,}$"),required:t=>this.and(this.isNotNull(t),this.isNotEmpty(t)),minAge:(t,e)=>this.gte(t,e),maxAge:(t,e)=>this.lte(t,e),ageRange:(t,e,r)=>this.between(t,[e,r]),oneOf:(t,e)=>this.in(t,e),minLength:(t,e)=>this.gte(`${t}.length`,e),maxLength:(t,e)=>this.lte(`${t}.length`,e),lengthRange:(t,e,r)=>this.between(`${t}.length`,[e,r]),exactLength:(t,e)=>this.eq(`${t}.length`,e)}}}t.OPERATOR_NAMES=i,t.OperatorError=s,t.PathResolver=e,t.RuleEngine=h,t.RuleEngineError=r,t.RuleHelpers=E,t.TypeUtils=o,t.ValidationError=class extends r{constructor(t,e={}){super(t,null,e),this.name="ValidationError"}},t.createRuleEngine=function(t={}){const e=new h(t);return function(t,e,r){new u(e,r).register(t),new l(e,r).register(t),new c(e,r).register(t),new f(e,r).register(t),new p(e,r).register(t),new g(e,r).register(t),new w(e,r).register(t)}(e,e.pathResolver,e.config),{evaluateExpr:e.evaluateExpr.bind(e),registerOperator:e.registerOperator.bind(e),getOperators:e.getOperators.bind(e),getMetrics:e.getMetrics.bind(e),getConfig:e.getConfig.bind(e),getCacheStats:e.getCacheStats.bind(e),clearCache:e.clearCache.bind(e),resolvePath:(t,r,s)=>e.pathResolver.resolve(t,r,s),resolveValue:(t,r,s)=>e.pathResolver.resolveValueOrLiteral(t,r,s),OPERATOR_NAMES:i,v:{pathResolver:e.pathResolver,engine:e}}},t.createRuleHelpers=function(){return new E}}); //# sourceMappingURL=index.min.js.map