UNPKG

odata-builder

Version:

Type-safe OData v4.01 query builder for TypeScript with compile-time validation. Fluent FilterBuilder API, lambda expressions (any/all), in/not/has operators.

2 lines (1 loc) 17.7 kB
const e={string:["eq","ne","contains","startswith","endswith","substringof","indexof","concat"],number:["eq","ne","lt","le","gt","ge"],boolean:["eq","ne"],Date:["eq","ne","lt","le","gt","ge"],Guid:["eq","ne"],null:["eq","ne"]},t={string:["tolower","toupper","trim","length"],number:["round","floor","ceiling"],Date:["year","month","day","hour","minute","second"],Guid:["tolower"]},r=(t,r)=>(e[t]||[]).includes(r),n=e=>null===e?"null":e instanceof Date?"Date":"string"==typeof e&&i(e)?"Guid":"number"==typeof e?"number":"boolean"==typeof e?"boolean":"string"==typeof e?"string":"unknown",i=e=>"string"==typeof e&&/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(e),o=e=>{if(!e||"object"!=typeof e)return!1;const i=e;if("lambdaOperator"in i)return"string"==typeof i.lambdaOperator&&("any"===i.lambdaOperator||"all"===i.lambdaOperator)&&"string"==typeof i.field&&"expression"in i&&(s(i.expression)||o(i.expression));if("field"in i&&"operator"in i&&"in"===i.operator&&"values"in i&&Array.isArray(i.values))return!0;if("type"in i&&"not"===i.type&&"filter"in i&&(o(i.filter)||s(i.filter)))return!0;if("field"in i&&"operator"in i&&"has"===i.operator&&"value"in i&&"string"==typeof i.value)return!0;if("field"in i&&"operator"in i&&"value"in i){const e=n(i.value);return"unknown"!==e&&(!!r(e,i.operator)&&!("transform"in i&&!((e,r)=>{if(!r)return!0;const n=t[e]||[];return r.every(e=>n.includes(e))})(e,i.transform)))}return!1},s=e=>("and"===e.logic||"or"===e.logic)&&e.filters.every(e=>o(e)||s(e));class a{constructor(e={}){this.context=e}visitBasicFilter(e){if(!("value"in e))throw new Error('Invalid BasicFilter: missing "value" property');if(null===e.value){return`${"function"in e&&e.function?this.processFunction(e.function,e.field):this.getTransformedField(e)} ${e.operator} null`}const t=n(e.value);if("unknown"===t)throw new Error("Unsupported value type: "+typeof e.value);this.validateOperator(t,e.operator);const r=this.getTransformedField(e);if(e.value instanceof Date){if("transform"in e&&e.transform.length){const t=this.applyDateTransforms(e.value,e.transform);return`${r} ${e.operator} ${t}`}{const t="function"in e&&e.function?this.processFunction(e.function,e.field):r,n=e.value.toISOString();return`${t} ${e.operator} ${n}`}}if("string"==typeof e.value){const t=("ignoreCase"in e&&e.ignoreCase?e.value.toLowerCase():e.value).replace(/'/g,"''"),n="removeQuotes"in e&&e.removeQuotes?t:`'${t}'`;if("function"in e&&e.function){const t="ignoreCase"in e&&e.ignoreCase?`tolower(${String(e.field)})`:String(e.field);if(["contains","startswith","endswith"].includes(e.function.type))return this.processFunction(e.function,t);return`${this.processFunction(e.function,t)} ${e.operator} ${n}`}return`${r} ${e.operator} ${n}`}if("number"==typeof e.value){return`${"function"in e&&e.function?this.processFunction(e.function,e.field):r} ${e.operator} ${e.value}`}if("boolean"==typeof e.value&&"function"in e&&e.function){const t=e.function;if("type"in t&&["contains","startswith","endswith"].includes(t.type)){const r="ignoreCase"in e&&e.ignoreCase?`tolower(${String(e.field)})`:String(e.field),n="ignoreCase"in e&&e.ignoreCase&&"value"in t&&"string"==typeof t.value?{...t,value:t.value.toLowerCase()}:t;return this.processFunction(n,r)}}return`${r} ${e.operator} ${String(e.value)}`}visitLambdaFilter(e,t){if(!("lambdaOperator"in e&&e.lambdaOperator&&"expression"in e&&e.expression))throw new Error(`Invalid LambdaFilter: ${JSON.stringify(e)}`);const r=t?String.fromCharCode(t.charCodeAt(0)+1):"s",n=e.expression,i=this.getPrefixedField(e.field,t);if(s(n)){const t=this.visitCombinedFilter(n,r);return`${i}/${e.lambdaOperator}(${r}: ${t})`}if(u(n)){const t=this.visitLambdaFilter(n,r);return`${i}/${e.lambdaOperator}(${r}: ${t})`}if(l(n)){const t={...n,field:this.getPrefixedField(n.field||"",r),ignoreCase:"ignoreCase"in n?n.ignoreCase:void 0,removeQuotes:"removeQuotes"in n?n.removeQuotes:void 0,transform:"transform"in n?n.transform:void 0},o=this.visitBasicFilter(t);return`${i}/${e.lambdaOperator}(${r}: ${o})`}throw new Error(`Invalid expression in LambdaFilter: ${JSON.stringify(n)}`)}visitCombinedFilter(e,t){const r=e.filters.map(e=>{if(s(e))return this.visitCombinedFilter(e,t);if(u(e))return this.visitLambdaFilter(e,t);if(c(e)){const r={...e,field:this.getPrefixedField(e.field,t)};return this.visitInFilter(r)}if(p(e))return this.visitNegatedFilter(e);if(d(e)){const r={...e,field:this.getPrefixedField(e.field,t)};return this.visitHasFilter(r)}if(l(e)){const r={...e,field:this.getPrefixedField(e.field,t)};return this.visitBasicFilter(r)}throw new Error(`Invalid sub-filter: ${JSON.stringify(e)}`)}).join(` ${e.logic} `);return r.includes(" ")?`(${r})`:r}visitInFilter(e){const t=String(e.field),r=e.values.map(e=>this.formatInValue(e));if(this.context.legacyInOperator){return`(${r.map(e=>`${t} eq ${e}`).join(" or ")})`}return`${t} in (${r.join(", ")})`}visitNegatedFilter(e){const t=e.filter;let r;if(s(t))r=this.visitCombinedFilter(t);else if(p(t))r=this.visitNegatedFilter(t);else if(c(t))r=this.visitInFilter(t);else if(d(t))r=this.visitHasFilter(t);else if(u(t))r=this.visitLambdaFilter(t);else{if(!l(t))throw new Error(`Invalid filter inside not(): ${JSON.stringify(t)}`);r=this.visitBasicFilter(t)}return`not (${r})`}visitHasFilter(e){return`${e.field} has ${e.value}`}formatInValue(e){if(null===e)return"null";if("string"==typeof e)return i(e)?e:`'${e.replace(/'/g,"''")}'`;if(e instanceof Date)return e.toISOString();if("number"==typeof e||"boolean"==typeof e)return String(e);throw new Error("Unsupported value type in 'in' filter: "+typeof e)}getTransformedField(e){return[..."ignoreCase"in e&&e.ignoreCase?["tolower"]:[],..."transform"in e&&Array.isArray(e.transform)?e.transform:[]].reduce((e,t)=>`${t}(${e})`,`${String(e.field)}`)}applyDateTransforms(e,t){const r={year:e=>e.getUTCFullYear(),month:e=>e.getUTCMonth()+1,day:e=>e.getUTCDate(),hour:e=>e.getUTCHours(),minute:e=>e.getUTCMinutes(),second:e=>e.getUTCSeconds()};return t.reduce((e,t)=>{const n=r[t];if(!n)throw new Error(`Unsupported DateTransform: ${t}`);return n(new Date(e))},+e)}processFunction(e,t){if(!e.type)throw new Error('Invalid function definition: missing "type" property');switch(e.type){case"concat":return`concat(${[t,...e.values.map(e=>this.formatValue(e))].join(", ")})`;case"contains":return`contains(${t}, ${this.formatValue(e.value)})`;case"endswith":return`endswith(${t}, ${this.formatValue(e.value)})`;case"indexof":return`indexof(${t}, ${this.formatValue(e.value)})`;case"length":return`length(${t})`;case"startswith":return`startswith(${t}, ${this.formatValue(e.value)})`;case"substring":{const r=[e.start];return void 0!==e.length&&r.push(e.length),`substring(${t}, ${r.map(e=>this.formatValue(e)).join(", ")})`}case"add":case"sub":case"mul":case"div":case"mod":return this.createArithmeticHandler(e.type)(e,t);case"now":return"now()";case"date":return`date(${this.resolveField(e.field)})`;case"time":return`time(${this.resolveField(e.field)})`;default:throw new Error(`Unsupported function type: ${e.type}`)}}resolveField(e){if("string"==typeof e)return e;if("/"in e)return e.fieldReference;throw new Error("Unsupported FieldReference type")}createArithmeticHandler(e){return(t,r)=>{if(!("operand"in t))throw new Error('Invalid function definition: missing "operand" property');return`${r} ${e} ${this.formatValue(t.operand)}`}}formatValue(e){if("string"==typeof e)return`'${e.replace(/'/g,"''")}'`;if(e instanceof Date)return`${e.toISOString()}`;if("number"==typeof e||"boolean"==typeof e)return String(e);throw new Error("Unsupported value type: "+typeof e)}getPrefixedField(e,t){const r=String(e);return t?"s"===r?t:r===t?r:r?`${t}/${r}`:t:r}validateOperator(e,t){if(!r(e,t))throw new Error(`Invalid operator "${t}" for type "${e}"`)}}function u(e){return"lambdaOperator"in e&&"string"==typeof e.lambdaOperator&&"expression"in e}function l(e){return"object"==typeof e&&null!==e&&"field"in e&&"operator"in e&&"value"in e&&!("lambdaOperator"in e)}function f(e){return"object"==typeof e&&null!==e&&"field"in e&&"lambdaOperator"in e&&"expression"in e&&(l(e.expression)||s(e.expression)||f(e.expression))}function c(e){return"object"==typeof e&&null!==e&&"field"in e&&"operator"in e&&"in"===e.operator&&"values"in e&&Array.isArray(e.values)}function p(e){return"object"==typeof e&&null!==e&&"type"in e&&"not"===e.type&&"filter"in e}function d(e){return"object"==typeof e&&null!==e&&"field"in e&&"operator"in e&&"has"===e.operator&&"value"in e}function h(e){if("string"!=typeof e)throw new Error("Search term must be a string.");const t=e.trim();if(""===t)throw new Error("Search term cannot be empty or whitespace only.");return t}class g{constructor(e=[]){this.parts=e}term(e){const t=e.trim();if(!t)throw new Error("Term cannot be empty or whitespace only.");return new g([...this.parts,h(t)])}phrase(e){const t=e.trim();if(!t)throw new Error("Phrase cannot be empty or whitespace only.");return new g([...this.parts,{phrase:t}])}and(){return new g([...this.parts,"AND"])}or(){return new g([...this.parts,"OR"])}not(e){return new g([...this.parts,{expression:["NOT",...e.build()]}])}group(e){return new g([...this.parts,{expression:e.build()}])}build(){return this.parts}toString(){return this.parts.map(this.stringifyPart.bind(this)).join(" ")}equals(e){return JSON.stringify(this.build())===JSON.stringify(e.build())}stringifyPart(e){if("string"==typeof e)return e;if("phrase"in e)return`"${e.phrase}"`;if("expression"in e){return`(${e.expression.map(this.stringifyPart.bind(this)).join(" ")})`}throw new Error(`Unsupported SearchExpressionPart: ${JSON.stringify(e)}`)}}const m=new Set(["eq","ne","gt","ge","lt","le","in","has","contains","startswith","endswith","length","indexof","substring","concat","tolower","toupper","trim","round","floor","ceiling","year","month","day","hour","minute","second","add","sub","mul","div","mod","ignoreCase","removeQuotes","any","all","isTrue","isFalse"]);function y(e={path:[],transforms:[],ignoreCase:!1,removeQuotes:!1}){return new Proxy({},{get(t,r){if("symbol"!=typeof r)return m.has(r)?function(e,t){const r=t.path.join("/");0===t.path.length&&["any","all"].includes(e);switch(e){case"eq":case"ne":case"gt":case"ge":case"lt":case"le":return n=>w(r,e,n,t);case"in":return e=>{if(!Array.isArray(e)||0===e.length)throw new Error(`FilterBuilder: in() requires at least one value. Field: '${r}'`);return function(e,t){return{_type:"expression",_filter:{field:e,operator:"in",values:t}}}(r,e)};case"has":return e=>function(e,t){return{_type:"expression",_filter:{field:e,operator:"has",value:t}}}(r,e);case"contains":case"startswith":case"endswith":return n=>function(e,t,r){const n={field:e,function:t,operator:"eq",value:!0};r.ignoreCase&&(n.ignoreCase=!0);return{_type:"expression",_filter:n}}(r,{type:e,value:n},t);case"length":return()=>y({...t,func:{type:"length"}});case"indexof":return e=>y({...t,func:{type:"indexof",value:e}});case"substring":return(e,r)=>{const n={type:"substring",start:e};return void 0!==r&&(n.length=r),y({...t,func:n})};case"concat":return(...e)=>y({...t,func:{type:"concat",values:e}});case"tolower":case"toupper":case"trim":case"round":case"floor":case"ceiling":case"year":case"month":case"day":case"hour":case"minute":case"second":return()=>y({...t,transforms:[...t.transforms,e]});case"add":case"sub":case"mul":case"div":case"mod":return r=>y({...t,func:{type:e,operand:r}});case"ignoreCase":return()=>y({...t,ignoreCase:!0});case"removeQuotes":return()=>y({...t,removeQuotes:!0});case"isTrue":return()=>w(r,"eq",!0,t);case"isFalse":return()=>w(r,"eq",!1,t);case"any":case"all":return t=>{const n=t(y({path:[],transforms:[],ignoreCase:!1,removeQuotes:!1}));return function(e,t,r){const n=r._filter;return{_type:"expression",_filter:{field:e,lambdaOperator:t,expression:n}}}(r,e,n)};default:throw new Error(`FilterBuilder: Unknown operation '${e}'. This might be a typo or the operation is not supported.`)}}(r,e):y({...e,path:[...e.path,r]})}})}function w(e,t,r,n){const i={field:e,operator:t,value:r};return n.transforms.length>0&&(i.transform=[...n.transforms]),n.ignoreCase&&(i.ignoreCase=!0),n.removeQuotes&&(i.removeQuotes=!0),n.func&&(i.function=n.func),{_type:"expression",_filter:i}}class v{constructor(e=[]){this.parts=e}where(e){const t=this.resolveInput(e);return t?new v([...this.parts,{filter:t}]):this}and(e){if(0===this.parts.length)throw new Error("FilterBuilder: Cannot use .and() on empty builder. Use .where() first.");const t=this.resolveInput(e);return t?new v([...this.parts,{logic:"and",filter:t}]):this}or(e){if(0===this.parts.length)throw new Error("FilterBuilder: Cannot use .or() on empty builder. Use .where() first.");const t=this.resolveInput(e);return t?new v([...this.parts,{logic:"or",filter:t}]):this}not(){if(0===this.parts.length)throw new Error("FilterBuilder: Cannot use .not() on empty builder. Use .where() first.");const e=this.build();if(!e)throw new Error("FilterBuilder: Cannot negate empty filter expression.");return new v([{filter:{type:"not",filter:e}}])}group(e){const t=e.build();if(!t)throw new Error("FilterBuilder: Cannot group empty FilterBuilder. Add at least one condition.");return new v([{filter:t}])}build(){if(0===this.parts.length)return null;if(1===this.parts.length){const e=this.parts[0];if(e)return e.filter}return this.buildWithPrecedence()}buildArray(){const e=this.build();return e?[e]:[]}isEmpty(){return 0===this.parts.length}resolveInput(e){if(this.isFilterBuilderLike(e))return e.build();if("function"!=typeof e)throw new Error("FilterBuilder: Expected a predicate function (x => x.field.eq(value)) or a FilterBuilder instance.");const t=e(y());if(!t||"expression"!==t._type)throw new Error("FilterBuilder: Predicate must return a filter expression. Did you forget to call an operation like .eq(), .contains(), etc.?");return t._filter}isFilterBuilderLike(e){return"object"==typeof e&&null!==e&&"build"in e&&"function"==typeof e.build}buildWithPrecedence(){const e=[];let t=[];for(const r of this.parts)"or"===r.logic?(t.length>0&&e.push(t),t=[r.filter]):t.push(r.filter);t.length>0&&e.push(t);const r=e.flatMap(e=>{if(1===e.length){const t=e[0];return t?[t]:[]}return[{logic:"and",filters:e}]});if(1===r.length){const e=r[0];if(e)return e}return{logic:"or",filters:r}}}function $(){return new v}class b{constructor(e={}){this.queryComponents={},this.filterContext={legacyInOperator:e.legacyInOperator}}top(e){if(!e||this.queryComponents.top)return this;if(e<0)throw new Error("Invalid top count");return this.queryComponents.top=e,this}skip(e){if(!e||this.queryComponents.skip)return this;if(e<0)throw new Error("Invalid skip count");return this.queryComponents.skip=e,this}select(...e){if(0===e.length)return this;if(e.some(e=>!e))throw new Error("Invalid select input");return this.addComponent("select",e)}filter(...e){if(1===e.length&&"function"==typeof e[0]){const t=(0,e[0])(new v).build();return t?this.addComponent("filter",[t]):this}const t=e;if(0===t.length)return this;for(const e of t){if(!e)throw new Error("Invalid filter input");if(c(e));else if(p(e));else if(d(e));else if(l(e)){const t=n(e.value);if(!r(t,e.operator))throw new Error(`Invalid operator "${e.operator}" for type "${t}"`)}else if(f(e));else if(!s(e))throw new Error(`Invalid filter input: ${JSON.stringify(e)}`)}return this.addComponent("filter",t)}expand(...e){if(0===e.length)return this;if(e.some(e=>!e))throw new Error("Field missing for expand");return this.addComponent("expand",e)}count(e=!1){return this.queryComponents.count||(this.queryComponents.count=e?"/$count":"$count=true"),this}orderBy(...e){return 0===e.length?this:this.addComponent("orderBy",e)}search(e){if(!e)return delete this.queryComponents.search,this;if("string"!=typeof e&&!(e instanceof g))throw new Error("search() expects a string or SearchExpressionBuilder");return this.queryComponents.search="string"==typeof e?h(e):e.toString(),this}toQuery(){const e={count:e=>e,filter:e=>((e,t={})=>{if(0===e.length)return"";const r=new a(t);return e.reduce((e,t,n)=>{if(!(s(t)||f(t)||l(t)||c(t)||p(t)||d(t)))throw new Error(`Invalid filter: ${JSON.stringify(t)}`);let i;return i=s(t)?r.visitCombinedFilter(t):f(t)?r.visitLambdaFilter(t):c(t)?r.visitInFilter(t):p(t)?r.visitNegatedFilter(t):d(t)?r.visitHasFilter(t):r.visitBasicFilter(t),e+(n>0?" and ":"")+i},"$filter=")})(Array.from(e),this.filterContext),top:e=>(e=>{if(!Number.isFinite(e)||!Number.isInteger(e)||e<0)throw new Error("Invalid top count");return 0===e?"":`$top=${e}`})(e),skip:e=>(e=>{if(!Number.isFinite(e)||!Number.isInteger(e)||e<0)throw new Error("Invalid skip count");return 0===e?"":`$skip=${e}`})(e),select:e=>{return 0===(t=Array.from(e)).length?"":`$select=${t.join(", ")}`;var t},expand:e=>{return 0===(t=Array.from(e)).length?"":`$expand=${t.join(", ")}`;var t},orderBy:e=>(e=>{if(!e||0===e.length)return"";const t=e.filter(e=>e&&e.field).reduce((e,t,r,n)=>{var i;return e+`${t.field} ${null!==(i=t.orderDirection)&&void 0!==i?i:"asc"}${r<n.length-1?", ":""}`},"");return t?`$orderby=${t}`:""})(Array.from(e)),search:e=>`$search=${encodeURIComponent(e)}`},t=Object.entries(this.queryComponents).sort(([t],[r])=>Object.keys(e).indexOf(t)-Object.keys(e).indexOf(r)),r=[];for(const[n,i]of t){if(!i)continue;const t=e[n](i);t&&r.push(t)}const n=r.join("&");if(n.startsWith("/$count")){const e=n.slice(7);return e.length>0?`/$count?${e.substring(1)}`:"/$count"}return n.length>0?`?${n}`:""}addComponent(e,t){if(0===t.length)return this;this.queryComponents[e]||(this.queryComponents[e]=new Set);const r=this.queryComponents[e];for(const e of t)r.add(e);return this}}export{v as FilterBuilder,b as OdataQueryBuilder,g as SearchExpressionBuilder,$ as filter,s as isCombinedFilter,o as isQueryFilter};