@inixiative/json-rules
Version:
TypeScript-first JSON rules engine with intuitive syntax and detailed error messages
2 lines • 7.67 kB
JavaScript
import {get,isEmpty,some,isObject}from'lodash';import m from'dayjs';import A from'dayjs/plugin/utc';import B from'dayjs/plugin/timezone';import D from'dayjs/plugin/isSameOrBefore';import I from'dayjs/plugin/isSameOrAfter';var b=(f=>(f.equals="equals",f.notEquals="notEquals",f.lessThan="lessThan",f.lessThanEquals="lessThanEquals",f.greaterThan="greaterThan",f.greaterThanEquals="greaterThanEquals",f.contains="contains",f.notContains="notContains",f.in="in",f.notIn="notIn",f.matches="matches",f.notMatches="notMatches",f.between="between",f.notBetween="notBetween",f.isEmpty="isEmpty",f.notEmpty="notEmpty",f.exists="exists",f.notExists="notExists",f.startsWith="startsWith",f.endsWith="endsWith",f))(b||{}),E=(n=>(n.all="all",n.any="any",n.none="none",n.atLeast="atLeast",n.atMost="atMost",n.exactly="exactly",n.empty="empty",n.notEmpty="notEmpty",n))(E||{}),$=(n=>(n.before="before",n.after="after",n.onOrBefore="onOrBefore",n.onOrAfter="onOrAfter",n.between="between",n.notBetween="notBetween",n.dayIn="dayIn",n.dayNotIn="dayNotIn",n))($||{});m.extend(A);m.extend(B);m.extend(D);m.extend(I);var q=(e,o,u)=>{let s=get(o,e.field);if(!s)throw new Error(`${e.field} is null or undefined`);let r=m(s);if(!r.isValid())throw new Error(`${e.field} is not a valid date: ${s}`);let l=i=>e.error||`${e.field} ${i}`,a=j(e,o,u,r,s),t=a[0],n=a[1];switch(e.dateOperator){case "before":return r.isBefore(t)||l(`must be before ${t.format()}`);case "after":return r.isAfter(t)||l(`must be after ${t.format()}`);case "onOrBefore":return r.isSameOrBefore(t)||l(`must be on or before ${t.format()}`);case "onOrAfter":return r.isSameOrAfter(t)||l(`must be on or after ${t.format()}`);case "between":return r.isSameOrAfter(t)&&r.isSameOrBefore(n)||l(`must be between ${t.format()} and ${n.format()}`);case "notBetween":return r.isBefore(t)||r.isAfter(n)||l(`must not be between ${t.format()} and ${n.format()}`);case "dayIn":if(!Array.isArray(e.value))throw new Error("dayIn operator requires an array of day names");let i=r.format("dddd").toLowerCase(),h=e.value.map(c=>c.toLowerCase());return h.includes(i)||l(`must be on ${h.join(" or ")}`);case "dayNotIn":if(!Array.isArray(e.value))throw new Error("dayNotIn operator requires an array of day names");let x=r.format("dddd").toLowerCase(),g=e.value.map(c=>c.toLowerCase());return !g.includes(x)||l(`must not be on ${g.join(" or ")}`);default:throw new Error("Unknown date operator")}},j=(e,o,u,s,r)=>{if(["between","notBetween"].includes(e.dateOperator)){if(!Array.isArray(e.value)||e.value.length!==2)throw new Error(`${e.dateOperator} operator requires an array of two dates`);let t=p(e.value[0],r),n=p(e.value[1],r);if(!t.isValid())throw new Error(`Invalid start date: ${e.value[0]}`);if(!n.isValid())throw new Error(`Invalid end date: ${e.value[1]}`);return [t,n]}if(["before","after","onOrBefore","onOrAfter"].includes(e.dateOperator)){let t;if(e.value!==void 0)t=e.value;else if(e.path)e.path.startsWith("$.")?t=get(o,e.path.substring(2)):t=get(u,e.path);else throw new Error("No value or path specified for date comparison");let n=p(t,r);if(!n.isValid())throw new Error(`Invalid comparison date: ${t}`);return [n,void 0]}return [m(),void 0]},p=(e,o)=>{let u=String(e);if(u.includes("Z")||u.includes("T")&&(u.includes("+")||u.match(/T.*-\d{2}:/)))return m(e);let r=String(o),l=0;if(r.includes("+")||r.includes("T")&&r.match(/T.*-\d{2}:/)){let t=r.match(/([+-])(\d{2}):(\d{2})/);t&&(l=(t[1]==="+"?1:-1)*(parseInt(t[2])*60+parseInt(t[3])));}else r.includes("Z")||(l=0);return u.match(/^\d{4}-\d{2}-\d{2}$/)?m(e+"T00:00:00").subtract(l,"minute"):m(e).subtract(l,"minute")};var T=(e,o,u)=>{let s=get(o,e.field),l=!["isEmpty","notEmpty","exists","notExists"].includes(e.operator),a=l?C(e,o,u):void 0,t=n=>e.error||`${e.field} ${n}${l?" "+JSON.stringify(a):""}`;switch(e.operator){case "equals":return s===a||t("must equal");case "notEquals":return s!==a||t("must not equal");case "lessThan":return s<a||t("must be less than");case "lessThanEquals":return s<=a||t("must be less than or equal to");case "greaterThan":return s>a||t("must be greater than");case "greaterThanEquals":return s>=a||t("must be greater than or equal to");case "in":return a?.includes(s)||t("must be one of");case "notIn":return !a?.includes(s)||t("must not be one of");case "contains":return s?.includes(a)||t("must contain");case "notContains":return !s?.includes(a)||t("must not contain");case "matches":return !!s?.match(a)||t("must match pattern");case "notMatches":return !s?.match(a)||t("must not match pattern");case "between":if(!Array.isArray(a)||a.length!==2)throw new Error("between operator requires an array of two values");return s>=a[0]&&s<=a[1]||t("must be between");case "notBetween":if(!Array.isArray(a)||a.length!==2)throw new Error("notBetween operator requires an array of two values");return s<a[0]||s>a[1]||t("must not be between");case "isEmpty":return isEmpty(s)||t("must be empty");case "notEmpty":return !isEmpty(s)||t("must not be empty");case "exists":return s!==void 0||t("must exist");case "notExists":return s===void 0||t("must not exist");case "startsWith":return s?.startsWith?.(a)||t("must start with");case "endsWith":return s?.endsWith?.(a)||t("must end with");default:throw new Error("Unknown operator")}},C=(e,o,u)=>{if(e.value!==void 0)return e.value;if(e.path)return e.path.startsWith("$.")?get(o,e.path.substring(2)):get(u,e.path);throw new Error("No value or path specified")};var y=(e,o,u=o)=>typeof e=="boolean"?e:"all"in e?W(e.all,o,u,e.error):"any"in e?M(e.any,o,u,e.error):"arrayOperator"in e?N(e,o,u):"dateOperator"in e?q(e,o,u):"field"in e?T(e,o,u):"if"in e?L(e,o,u):false,W=(e,o,u,s)=>{let r=[];for(let l of e){let a=y(l,o,u);a!==true&&(typeof a=="string"?r.push(a):r.push("false"));}return r.length?s||(r.length===1?r[0]:`All conditions must pass: ${r.join(" AND ")}`):true},M=(e,o,u,s)=>{let r=[];for(let l of e){let a=y(l,o,u);if(typeof a!="string")return true;r.push(a);}return s||(r.length===1?r[0]:`At least one condition must pass: ${r.join(" OR ")}`)},L=(e,o,u)=>y(e.if,o,u)===true?y(e.then,o,u):e.else?y(e.else,o,u):true,N=(e,o,u)=>{let s=get(u,e.field);if(!Array.isArray(s))throw new Error(`${e.field} must be an array`);let r=i=>e.error||`${e.field} ${i}`,l=["all","any","none","atLeast","atMost","exactly"],a=["atLeast","atMost","exactly"];if(l.includes(e.arrayOperator)&&!e.condition)throw new Error(`${e.arrayOperator} requires a condition to check against array elements`);if(a.includes(e.arrayOperator)&&e.count===void 0)throw new Error(`${e.arrayOperator} requires a count`);let t=0,n=0;if(l.includes(e.arrayOperator)){if(!some(s,isObject))throw new Error(`${e.field} contains only primitive values. Use 'in' or 'contains' operators instead of array operators for primitive arrays`);let i=s.map(h=>y(e.condition,h,u));t=i.filter(h=>h===true).length,n=i.filter(h=>typeof h=="string").length;}switch(e.arrayOperator){case "empty":return !s.length||r("must be empty");case "notEmpty":return !!s.length||r("must not be empty");case "all":return t===s.length||r(`all elements must match (${n} failed)`);case "any":return !!t||r("at least one element must match");case "none":return !t||r(`no elements should match (${t} matched)`);case "atLeast":return t>=e.count||r(`at least ${e.count} elements must match (${t} matched)`);case "atMost":return t<=e.count||r(`at most ${e.count} elements must match (${t} matched)`);case "exactly":return t===e.count||r(`exactly ${e.count} elements must match (${t} matched)`);default:throw new Error("Unknown array operator")}};export{E as ArrayOperator,$ as DateOperator,b as Operator,y as check};//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map