UNPKG

odata-builder

Version:

odata builder for easier and typesafe usage

2 lines (1 loc) 18.3 kB
"use strict";const e={string:Object.freeze(["eq","ne","contains","startswith","endswith","substringof","indexof"]),number:Object.freeze(["eq","ne","lt","le","gt","ge"]),boolean:Object.freeze(["eq","ne"]),Date:Object.freeze(["eq","ne","lt","le","gt","ge"]),Guid:Object.freeze(["eq","ne"]),null:Object.freeze(["eq","ne"])},r={string:Object.freeze(["tolower","toupper","trim","length"]),number:Object.freeze(["round","floor","ceiling"]),Date:Object.freeze(["year","month","day","hour","minute","second"]),Guid:Object.freeze(["tolower"])};function t(r,t){return(e[r]||[]).includes(t)}function n(e){return null===e?"null":e instanceof Date?"Date":"string"==typeof e&&("string"==typeof(r=e)&&/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(r))?"Guid":"number"==typeof e?"number":"boolean"==typeof e?"boolean":"string"==typeof e?"string":"unknown";var r}const i=e=>{if(!e||"object"!=typeof e)return!1;const s=e;if("lambdaOperator"in s)return"string"==typeof s.lambdaOperator&&("any"===s.lambdaOperator||"all"===s.lambdaOperator)&&"string"==typeof s.field&&"expression"in s&&(o(s.expression)||i(s.expression));if("field"in s&&"operator"in s&&"value"in s){const e=n(s.value);return"unknown"!==e&&(!!t(e,s.operator)&&!("transform"in s&&!function(e,t){if(!t||0===t.length)return!0;const n=r[e]||[];return t.every((e=>n.includes(e)))}(e,s.transform)))}return!1},o=e=>("and"===e.logic||"or"===e.logic)&&e.filters.every((e=>i(e)||o(e)));class s{visitBasicFilter(e){if(!a(e))throw new Error("visitBasicFilter called with a non-basic filter type. This indicates a dispatching logic error.");const r=e,t=String(r.field);if(r.function){const e=r.function;if(!e||"string"!=typeof e.type||!e.type.trim())throw new Error(`Invalid function definition on field "${t}": "type" property is missing, not a string, or empty.`);const n=this.getTransformedFieldForFunction(r),i=this.processFunction(e,n);if("contains"===e.type||"startswith"===e.type||"endswith"===e.type){const e=r;return void 0===e.operator||void 0===e.value||"eq"!==e.operator&&"ne"!==e.operator||"boolean"!=typeof e.value&&null!==e.value?i:`${i} ${e.operator} ${this.formatValue(e.value)}`}{const n=r;if(void 0===n.operator||void 0===n.value&&null!==n.value)throw new Error(`Operator and value are required for comparing the result of function type "${e.type}" on field "${t}".`);const o="removeQuotes"in n&&!0===n.removeQuotes,s=this.formatValue(n.value,o);return`${i} ${n.operator} ${s}`}}const i=r;if(void 0===i.operator||void 0===i.value&&null!==i.value)throw new Error(`Operator and value are required for standard filter on field "${t}".`);const o=this.getTransformedFieldStandard(i);if(null===i.value)return`${o} ${i.operator} null`;const s=n(i.value);if("unknown"===s)throw new Error(`Unsupported value type: ${typeof i.value} on field "${t}".`);if(this.validateOperator(s,i.operator),i.value instanceof Date){const e=i.value,r=i;if(r.transform&&r.transform.length>0){const t=this.applyDateTransforms(e,r.transform);return`${o} ${r.operator} ${t}`}{const t=e.toISOString();return`${o} ${r.operator} ${t}`}}if("string"==typeof i.value){let e=i.value;const r=i;r.ignoreCase&&(e=e.toLowerCase());const t=r.removeQuotes?e:`'${e.replace(/'/g,"''")}'`,n=r.operator;switch(n){case"contains":case"startswith":case"endswith":return`${n}(${o}, ${t})`;case"substringof":return`substringof(${t}, ${o})`;case"indexof":return`indexof(${o}, ${t})`;default:return`${o} ${n} ${t}`}}return"number"==typeof i.value?`${o} ${i.operator} ${i.value}`:`${o} ${i.operator} ${String(i.value)}`}visitLambdaFilter(e,r){if(!e.expression||"object"!=typeof e.expression)throw new Error(`Invalid LambdaFilter on field "${String(e.field)}": "expression" property is missing or not an object.`);const t=r?String.fromCharCode(r.charCodeAt(0)+1):"s",n=String(e.field),i=this.getPrefixedField(n,r),o=this.visitExpression(e.expression,t);return`${i}/${e.lambdaOperator}(${t}: ${o})`}visitExpression(e,r){if("object"!=typeof e||null===e)throw new Error(`Invalid expression encountered: expression is not an object or is null. Value: ${String(e)}`);if(o(e)){if(!Array.isArray(e.filters))throw new Error('Invalid CombinedFilter: "filters" property is missing or not an array.');return this.visitCombinedFilter(e,r)}if(this.isLambdaFilterInternal(e))return this.visitLambdaFilter(e,r);if(a(e)){const t=e,n=String(t.field),i=(""===n?r:this.getPrefixedField(n,r))||n,o={...t,field:i};return this.visitBasicFilter(o)}throw new Error(`Unsupported expression type in visitExpression. Expression: ${JSON.stringify(e)}`)}visitCombinedFilter(e,r){if(!e.filters||!Array.isArray(e.filters))throw new Error('Invalid CombinedFilter: "filters" property is missing or not an array.');if(0===e.filters.length)return"";const t=e.filters.map((e=>{if("object"!=typeof e||null===e)throw new Error(`Invalid sub-filter in CombinedFilter: sub-filter is not an object or is null. Value: ${String(e)}`);return this.visitExpression(e,r)})).filter((e=>e&&e.length>0)).join(` ${e.logic} `);return t.length>0?1===e.filters.length&&t.startsWith("(")&&t.endsWith(")")?t:`(${t})`:""}getTransformedFieldForFunction(e){let r=String(e.field);if(e.transform&&Array.isArray(e.transform)&&e.transform.length>0){r=e.transform.reduce(((e,r)=>`${r}(${e})`),r)}return r}getTransformedFieldStandard(e){let r=String(e.field);if("string"==typeof e.value&&e.ignoreCase&&(r=`tolower(${r})`),e.transform&&Array.isArray(e.transform)&&e.transform.length>0){r=e.transform.reduce(((e,r)=>`${r}(${e})`),r)}return r}applyDateTransforms(e,r){const t={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()};if(!r||0===r.length)throw new Error("applyDateTransforms called with an invalid or empty transforms array.");const n=r[0];if(!n)throw new Error("Transform not found");if(n&&!(n in t))throw new Error(`Unsupported DateTransform: ${String(n)}`);const i=t[n];if(!i)throw new Error(`Unsupported DateTransform: ${n}`);const o=i(e);if("number"!=typeof o)throw new Error(`Date transformation did not result in a number. Transform: ${String(n)}. Res: ${String(o)}`);return o}processFunction(e,r){const t=r;switch(e.type){case"concat":if(!e.values||!Array.isArray(e.values))throw new Error("Invalid function definition for 'concat': 'values' array is missing or not an array.");break;case"contains":case"endswith":case"indexof":case"startswith":if(void 0===e.value)throw new Error(`Invalid function definition for '${e.type}': 'value' property is missing.`);break;case"substring":if(void 0===e.start)throw new Error("Invalid function definition for 'substring': 'start' property is missing.");break;case"add":case"sub":case"mul":case"div":case"mod":if(void 0===e.operand)throw new Error(`Invalid function definition for '${e.type}': 'operand' property is missing.`);break;case"date":case"time":{const r=e.field;if(void 0===r)throw new Error(`Invalid function definition for '${e.type}': 'field' property is missing.`);this.validateFieldReference(r);break}case"length":case"tolower":case"toupper":case"trim":case"round":case"floor":case"ceiling":case"now":case"year":case"month":case"day":case"hour":case"minute":case"second":break;default:throw new Error(`Unhandled preliminary check for function type in processFunction: ${e.type}`)}switch(e.type){case"concat":return`concat(${t}, ${e.values.map((e=>"string"==typeof e?this.formatValue(e):this.resolveFieldReference(e))).join(", ")})`;case"contains":return`contains(${t}, ${"string"==typeof e.value?this.formatValue(e.value):this.resolveFieldReference(e.value)})`;case"endswith":return`endswith(${t}, ${"string"==typeof e.value?this.formatValue(e.value):this.resolveFieldReference(e.value)})`;case"indexof":return`indexof(${t}, ${"string"==typeof e.value?this.formatValue(e.value):this.resolveFieldReference(e.value)})`;case"length":return`length(${t})`;case"startswith":return`startswith(${t}, ${"string"==typeof e.value?this.formatValue(e.value):this.resolveFieldReference(e.value)})`;case"substring":{const r=["number"==typeof e.start?this.formatValue(e.start):this.resolveFieldReference(e.start)];if(void 0!==e.length){const t="number"==typeof e.length?this.formatValue(e.length):this.resolveFieldReference(e.length);r.push(t)}return`substring(${t}, ${r.join(", ")})`}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`${e.type}(${t})`;case"add":case"sub":case"mul":case"div":case"mod":{const r=e,n="number"==typeof r.operand?this.formatValue(r.operand):this.resolveFieldReference(r.operand);return`${t} ${r.type} ${n}`}case"now":return"now()";case"date":return`date(${this.resolveFieldReference(e.field)})`;case"time":return`time(${this.resolveFieldReference(e.field)})`;default:throw new Error(`Unhandled function type in processFunction execution: ${e.type}`)}}validateFieldReference(e){if("object"!=typeof e||null===e||!("fieldReference"in e)||"string"!=typeof e.fieldReference||!e.fieldReference.trim())throw new Error("Invalid FieldReference: It must be an object with a 'fieldReference' property that is a non-empty string.")}resolveFieldReference(e){return this.validateFieldReference(e),e.fieldReference}formatValue(e,r=!1){if(null===e)return"null";if("string"==typeof e)return r?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 for formatting: "+typeof e)}getPrefixedField(e,r){const t=String(e);return r?""===t?r:`${r}/${t}`:t}validateOperator(e,r){if(!t(e,r))throw new Error(`Invalid operator "${String(r)}" for value type "${e}"`)}isLambdaFilterInternal(e){if("object"!=typeof e||null===e)return!1;const r=e;return"lambdaOperator"in r&&("any"===r.lambdaOperator||"all"===r.lambdaOperator)&&"expression"in r&&null!==r.expression&&"object"==typeof r.expression&&"field"in r&&(a(r.expression)||o(r.expression)||this.isLambdaFilterInternal(r.expression))}}function a(e){return"object"==typeof e&&null!==e&&"field"in e&&!("lambdaOperator"in e)&&("operator"in e&&"value"in e||!!("function"in e&&e.function&&"object"==typeof e.function&&null!==e.function&&"type"in e.function))}function l(e){return"object"==typeof e&&null!==e&&"field"in e&&"lambdaOperator"in e&&"expression"in e&&(a(e.expression)||o(e.expression)||l(e.expression))}function u(e){if("string"!=typeof e)throw new Error("Search term must be a string.");const r=e.trim();if(""===r)throw new Error("Search term cannot be empty or whitespace only.");return r}class f{constructor(e=[]){this.parts=Object.freeze(Array.isArray(e)?[...e]:[])}term(e){const r=e.trim();if(!r)throw new Error("Term cannot be empty or whitespace only.");return new f([...this.parts,u(r)])}phrase(e){const r=e.trim();if(!r)throw new Error("Phrase cannot be empty or whitespace only.");return new f([...this.parts,{phrase:r}])}and(){if(0===this.parts.length)return console.warn("Attempted to start an expression with AND. Operation skipped."),this;const e=this.parts[this.parts.length-1];return"string"!=typeof e||"AND"!==e&&"OR"!==e?new f([...this.parts,"AND"]):(console.warn(`Attempted to add AND after operator '${e}'. Operation skipped.`),this)}or(){if(0===this.parts.length)return console.warn("Attempted to start an expression with OR. Operation skipped."),this;const e=this.parts[this.parts.length-1];return"string"!=typeof e||"AND"!==e&&"OR"!==e?new f([...this.parts,"OR"]):(console.warn(`Attempted to add OR after operator '${e}'. Operation skipped.`),this)}not(e){const r=e.build();if(0===r.length)throw new Error("NOT operator cannot be applied to an empty expression.");return new f([...this.parts,{expression:Object.freeze(["NOT",...r])}])}group(e){const r=e.build();return 0===r.length?this:new f([...this.parts,{expression:r}])}build(){return this.parts}toString(){return this.parts.map((e=>this.stringifyPart(e))).filter(Boolean).join(" ")}equals(e){return JSON.stringify(this.build())===JSON.stringify(e.build())}stringifyPart(e){if("string"==typeof e)return e;if("phrase"in e&&void 0!==e.phrase)return`"${e.phrase}"`;if("expression"in e&&Array.isArray(e.expression)){const r=e.expression;if(0===r.length)return"";if("NOT"===r[0]){const e=r.slice(1);if(0===e.length)throw new Error("Invalid NOT expression: NOT must be followed by a term or group.");const t=e.map((e=>this.stringifyPart(e))).filter(Boolean).join(" ");if(""===t)throw new Error("Invalid NOT expression: expression to negate is empty after stringification.");return t.startsWith("(")&&t.endsWith(")")?`NOT ${t}`:`NOT (${t})`}const t=r.map((e=>this.stringifyPart(e))).filter(Boolean).join(" ");return""===t?"":`(${t})`}throw new Error(`Unsupported SearchExpressionPart: ${JSON.stringify(e)}`)}}const p="/$count";exports.OdataQueryBuilder=class{constructor(){this.queryComponents={}}top(e){if(null==e)return this;if("number"!=typeof e||!Number.isFinite(e)||e<0)throw new Error("Invalid top count: Must be a non-negative finite number.");return void 0!==this.queryComponents.top&&this.queryComponents.top!==e&&console.warn("Overwriting existing top value. Multiple calls to top() will use the last valid value."),e>0?this.queryComponents.top=e:delete this.queryComponents.top,this}skip(e){if(null==e)return this;if("number"!=typeof e||!Number.isFinite(e)||e<0)throw new Error("Invalid skip count: Must be a non-negative finite number.");return void 0!==this.queryComponents.skip&&this.queryComponents.skip!==e&&console.warn("Overwriting existing skip value. Multiple calls to skip() will use the last valid value."),e>0?this.queryComponents.skip=e:delete this.queryComponents.skip,this}select(...e){if(1===e.length&&(null===e[0]||void 0===e[0]))throw new Error("Invalid select input: Argument cannot be null or undefined. Pass an array or individual strings.");if(0===e.length)return this;if(e.some((e=>null==e)))throw new Error("Invalid select input: All properties must be non-empty strings.");if(0===e.length)return this;if(e.some((e=>"string"!=typeof e||!e.trim())))throw new Error("Invalid select input: All properties must be non-empty strings.");return this.addComponent("select",e.map((e=>e.trim())).filter((e=>e))),this}filter(...e){if(1===e.length&&(null===e[0]||void 0===e[0]))throw new Error("Invalid filter input: Argument cannot be null or undefined. Pass an array or individual filter objects.");if(0===e.length)return this;if(e.some((e=>null==e)))throw new Error("Invalid filter input: Filter cannot be null or undefined.");if(0===e.length)return this;for(const r of e){if(!r)throw new Error("Invalid filter input: Filter array contains null or undefined element.");if(a(r)){if(null!==r.value){const e=n(r.value);if(!t(e,r.operator))throw new Error(`Invalid operator "${String(r.operator)}" for type "${e}" on field "${String(r.field)}".`)}}else if(!o(r)&&!i(r))throw new Error(`Invalid filter input structure: ${JSON.stringify(r)}`)}return this.addComponent("filter",e),this}expand(...e){if(1===e.length&&(null===e[0]||void 0===e[0]))throw new Error("Invalid expand input: Argument cannot be null or undefined. Pass an array or individual strings.");if(0===e.length)return this;if(e.some((e=>null==e)))throw new Error("Invalid expand input: All fields must be non-empty strings.");if(0===e.length)return this;if(e.some((e=>"string"!=typeof e||!e.trim())))throw new Error("Invalid expand input: All fields must be non-empty strings.");return this.addComponent("expand",e.map((e=>e.trim())).filter((e=>e))),this}count(e=!1){const r=e?p:"$count=true";return void 0!==this.queryComponents.count&&this.queryComponents.count!==r&&console.warn("Overwriting existing count setting. Multiple calls to count() will use the last setting."),this.queryComponents.count=r,this}orderBy(...e){if(0===e.length)return this;if(1===e.length&&(null===e[0]||void 0===e[0]))return this;const r=e.filter((e=>{return null!==(r=e)&&"object"==typeof r&&"field"in r&&"string"==typeof r.field&&""!==r.field.trim()&&(!("orderDirection"in r)||"asc"===r.orderDirection||"desc"===r.orderDirection);var r}));return 0===r.length||this.addComponent("orderBy",r),this}search(e){if(null==e)return delete this.queryComponents.search,this;if(!("string"==typeof(r=e)||r instanceof f))throw new Error("Invalid search input. Must be a non-empty string or an instance of SearchExpressionBuilder.");var r;let t;if("string"==typeof e){if(t=e.trim(),""===t)return delete this.queryComponents.search,this}else if(t=e.toString().trim(),""===t)return delete this.queryComponents.search,this;return this.queryComponents.search=t,this}toQuery(){const e={count:e=>e,filter:e=>(e=>{if(0===e.length)return"";const r=new s;return e.reduce(((e,t,n)=>{if(!o(t)&&!l(t)&&!a(t))throw new Error(`Invalid filter: ${JSON.stringify(t)}`);let i;return i=o(t)?r.visitCombinedFilter(t):l(t)?r.visitLambdaFilter(t):r.visitBasicFilter(t),e+(n>0?" and ":"")+i}),"$filter=")})(Array.from(e)),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===(r=Array.from(e)).length?"":`$select=${r.join(",")}`;var r},expand:e=>{return 0===(r=Array.from(e)).length?"":`$expand=${r.join(", ")}`;var r},orderBy:e=>(e=>{if(!e||0===e.length)return"";const r=e.filter((e=>e&&e.field)).reduce(((e,r,t,n)=>{var i;return e+`${r.field} ${null!==(i=r.orderDirection)&&void 0!==i?i:"asc"}${t<n.length-1?", ":""}`}),"");return r?`$orderby=${r}`:""})(Array.from(e)),search:e=>e?`$search=${encodeURIComponent(e)}`:""},r=["count","filter","search","top","skip","select","orderBy","expand"],t=[];for(const n of r){const r=n,i=this.queryComponents[r];if(null!=i){const n=(0,e[r])(i);n&&t.push(n)}}const n=t.join("&");if(this.queryComponents.count===p&&n.startsWith(p)){const e=n.substring(7);return e.startsWith("&")?`${p}?${e.substring(1)}`:""===e?p:`${p}${e}`}return n.length>0?`?${n}`:""}addComponent(e,r){if(!r||0===r.length)return this;this.queryComponents[e]||(this.queryComponents[e]=new Set);const t=this.queryComponents[e];for(const e of r)null!=e&&t.add(e);return this}},exports.SearchExpressionBuilder=f,exports.isCombinedFilter=o,exports.isQueryFilter=i;