UNPKG

@abaktiar/ql-parser

Version:

Framework-agnostic QL (Query Language) parser and builder for creating complex queries with support for logical operators, parameterized functions, and ORDER BY clauses

3 lines (2 loc) 11.2 kB
"use strict";var e=Object.defineProperty,t=(t,r,n)=>((t,r,n)=>r in t?e(t,r,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[r]=n)(t,"symbol"!=typeof r?r+"":r,n);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});class r{constructor(){t(this,"tokens",[]),t(this,"position",0)}parse(e){if(this.tokens=e.filter((e=>"whitespace"!==e.type)),this.position=0,0===this.tokens.length)return null;return this.tokens.some((e=>"keyword"!==e.type&&"whitespace"!==e.type))?this.parseExpression():null}parseExpression(){return this.parseOrExpression()}parseOrExpression(){var e,t;let r=this.parseAndExpression();for(;"logical"===(null==(e=this.current())?void 0:e.type)&&"OR"===(null==(t=this.current())?void 0:t.value);){this.advance();const e=this.parseAndExpression();this.isLogicalGroup(r)&&"OR"===r.operator?r.conditions.push(e):r={operator:"OR",conditions:[r,e]}}return r}parseAndExpression(){var e,t;let r=this.parsePrimaryExpression();for(;"logical"===(null==(e=this.current())?void 0:e.type)&&"AND"===(null==(t=this.current())?void 0:t.value);){this.advance();const e=this.parsePrimaryExpression();this.isLogicalGroup(r)&&"AND"===r.operator?r.conditions.push(e):r={operator:"AND",conditions:[r,e]}}return r}parsePrimaryExpression(){var e,t;const r=this.current();if(!r)throw new Error("Unexpected end of input");if("logical"===r.type&&"NOT"===r.value){this.advance();const e=this.parsePrimaryExpression();return{...e,not:!0}}if("parenthesis"===r.type&&"("===r.value){this.advance();const r=this.parseExpression();if("parenthesis"!==(null==(e=this.current())?void 0:e.type)||")"!==(null==(t=this.current())?void 0:t.value))throw new Error("Expected closing parenthesis");return this.advance(),r}if("field"===r.type)return this.parseCondition();throw new Error(`Unexpected token: ${r.type} "${r.value}"`)}parseCondition(){const e=this.current();if(!e||"field"!==e.type)throw new Error("Expected field name");const t={field:e.value};this.advance();const r=this.current();if(!r||"operator"!==r.type)throw new Error("Expected operator");return t.operator=r.value,this.advance(),this.operatorRequiresValue(t.operator)&&(t.value=this.parseValue(t.operator)),t}parseValue(e){if("IN"===e||"NOT IN"===e)return this.parseInList();const t=this.current();if(!t)throw new Error("Expected value");if("value"===t.type||"field"===t.type||"function"===t.type||"unknown"===t.type){let e=t.value;const r=this.tokens[this.position+1];return r&&"parenthesis"===r.type&&"("===r.value?this.parseFunctionCall(e):((e.startsWith('"')&&e.endsWith('"')||e.startsWith("'")&&e.endsWith("'"))&&(e=e.slice(1,-1)),this.advance(),e)}throw new Error(`Expected value, got ${t.type} "${t.value}"`)}parseFunctionCall(e){var t,r,n,i,s;this.advance(),this.advance();const o=[];for(;this.current()&&("parenthesis"!==(null==(t=this.current())?void 0:t.type)||")"!==(null==(r=this.current())?void 0:r.value));){const e=this.current();if("value"!==(null==e?void 0:e.type)&&"field"!==(null==e?void 0:e.type)&&"function"!==(null==e?void 0:e.type)&&"unknown"!==(null==e?void 0:e.type))throw new Error(`Unexpected token in function parameters: ${null==e?void 0:e.type} "${null==e?void 0:e.value}"`);{let t=e.value;const r=this.tokens[this.position+1];r&&"parenthesis"===r.type&&"("===r.value?t=this.parseFunctionCall(t):((t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1)),this.advance()),o.push(t),"comma"===(null==(n=this.current())?void 0:n.type)&&this.advance()}}if("parenthesis"!==(null==(i=this.current())?void 0:i.type)||")"!==(null==(s=this.current())?void 0:s.value))throw new Error("Expected closing parenthesis for function call");return this.advance(),0===o.length?`${e}()`:`${e}(${o.join(", ")})`}parseInList(){var e,t,r,n,i,s,o;if("parenthesis"!==(null==(e=this.current())?void 0:e.type)||"("!==(null==(t=this.current())?void 0:t.value))throw new Error("Expected opening parenthesis for IN list");this.advance();const a=[];for(;this.current()&&("parenthesis"!==(null==(r=this.current())?void 0:r.type)||")"!==(null==(n=this.current())?void 0:n.value));){const e=this.current();if("value"!==(null==e?void 0:e.type)&&"field"!==(null==e?void 0:e.type)&&"function"!==(null==e?void 0:e.type)&&"unknown"!==(null==e?void 0:e.type))throw new Error(`Unexpected token in IN list: ${null==e?void 0:e.type} "${null==e?void 0:e.value}"`);{let t=e.value;const r=this.tokens[this.position+1];r&&"parenthesis"===r.type&&"("===r.value?t=this.parseFunctionCall(t):((t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1)),this.advance()),a.push(t),"comma"===(null==(i=this.current())?void 0:i.type)&&this.advance()}}if("parenthesis"!==(null==(s=this.current())?void 0:s.type)||")"!==(null==(o=this.current())?void 0:o.value))throw new Error("Expected closing parenthesis for IN list");return this.advance(),a}operatorRequiresValue(e){return!["IS EMPTY","IS NOT EMPTY"].includes(e)}current(){return this.tokens[this.position]}advance(){this.position++}isLogicalGroup(e){return"operator"in e&&"conditions"in e}}const n=["AND","OR","NOT"],i=["=","!=",">","<",">=","<=","~","!~","IN","NOT IN","IS EMPTY","IS NOT EMPTY"],s=["ORDER","BY","ASC","DESC"];function o(e){return"field"in e&&"operator"in e}function a(e){return"operator"in e&&"conditions"in e}function p(e){return o(e)?function(e){const{field:t,operator:r,value:n}=e;switch(r){case"=":return{[t]:n};case"!=":return{[t]:{$ne:n}};case">":return{[t]:{$gt:n}};case"<":return{[t]:{$lt:n}};case">=":return{[t]:{$gte:n}};case"<=":return{[t]:{$lte:n}};case"IN":return{[t]:{$in:Array.isArray(n)?n:[n]}};case"NOT IN":return{[t]:{$nin:Array.isArray(n)?n:[n]}};case"~":return{[t]:{$regex:n,$options:"i"}};case"!~":return{[t]:{$not:{$regex:n,$options:"i"}}};case"IS EMPTY":return{$or:[{[t]:null},{[t]:{$exists:!1}}]};case"IS NOT EMPTY":return{[t]:{$ne:null,$exists:!0}};default:return{[t]:n}}}(e):a(e)?function(e){const t=e.conditions.map(p);if("AND"===e.operator)return{$and:t};if("OR"===e.operator)return{$or:t};return{}}(e):{}}function l(e){return o(e)?function(e){const{field:t,operator:r,value:n}=e;switch(r){case"=":default:return`${t} = '${n}'`;case"!=":return`${t} != '${n}'`;case">":return`${t} > '${n}'`;case"<":return`${t} < '${n}'`;case">=":return`${t} >= '${n}'`;case"<=":return`${t} <= '${n}'`;case"IN":return`${t} IN (${(Array.isArray(n)?n:[n]).map((e=>`'${e}'`)).join(", ")})`;case"NOT IN":return`${t} NOT IN (${(Array.isArray(n)?n:[n]).map((e=>`'${e}'`)).join(", ")})`;case"~":return`${t} LIKE '%${n}%'`;case"!~":return`${t} NOT LIKE '%${n}%'`;case"IS EMPTY":return`${t} IS NULL`;case"IS NOT EMPTY":return`${t} IS NOT NULL`}}(e):a(e)?function(e){const t=e.conditions.map(l),r=e.operator;if(1===t.length)return t[0];return`(${t.join(` ${r} `)})`}(e):""}exports.QLExpressionParser=r,exports.QLParser=class{constructor(e){}tokenize(e){const t=[];let r=0;for(;r<e.length;){const o=e[r],a=e.slice(r);if(/\s/.test(o)){const n=r;for(;r<e.length&&/\s/.test(e[r]);)r++;t.push({type:"whitespace",value:e.slice(n,r),start:n,end:r});continue}let p=!1;if('"'===o||"'"===o){const n=o,i=r;for(r++;r<e.length&&e[r]!==n;)"\\"===e[r]?r+=2:r++;r<e.length&&r++,t.push({type:"value",value:e.slice(i,r),start:i,end:r});continue}if("("===o||")"===o){t.push({type:"parenthesis",value:o,start:r,end:r+1}),r++;continue}if(","===o){t.push({type:"comma",value:o,start:r,end:r+1}),r++;continue}if(["=","!",">","<","~"].includes(o)){const n=r;let i=o;if(r+1<e.length){const t=e[r+1];("!"===o&&"="===t||">"===o&&"="===t||"<"===o&&"="===t||"!"===o&&"~"===t)&&(i+=t,r++)}t.push({type:"operator",value:i,start:n,end:r+1}),r++;continue}for(const e of i.sort(((e,t)=>t.length-e.length)))if(a.toUpperCase().startsWith(e)){const n=a[e.length];if(!n||/\s/.test(n)||"("===n||")"===n){t.push({type:"operator",value:e,start:r,end:r+e.length}),r+=e.length,p=!0;break}}if(p)continue;for(const e of n)if(a.toUpperCase().startsWith(e)){const n=a[e.length];if(!n||/\s/.test(n)||"("===n||")"===n){t.push({type:"logical",value:e,start:r,end:r+e.length}),r+=e.length,p=!0;break}}if(p)continue;for(const e of s)if(a.toUpperCase().startsWith(e)){const n=a[e.length];if(!n||/\s/.test(n)||"("===n||")"===n){t.push({type:"keyword",value:e,start:r,end:r+e.length}),r+=e.length,p=!0;break}}if(p)continue;const l=r;for(;r<e.length&&!/\s/.test(e[r])&&"("!==e[r]&&")"!==e[r]&&","!==e[r]&&'"'!==e[r]&&"'"!==e[r];)r++;if(r>l){const n=e.slice(l,r);t.push({type:"unknown",value:n,start:l,end:r})}else r++}return this.classifyTokens(t)}classifyTokens(e){const t={expectingField:!0,expectingOperator:!1,expectingValue:!1,expectingLogical:!1,parenthesesLevel:0,inOrderBy:!1};for(let r=0;r<e.length;r++){const n=e[r];"whitespace"!==n.type&&("unknown"===n.type&&(t.expectingField?(n.type="field",t.expectingField=!1,t.expectingOperator=!0):t.expectingValue?(n.type="value",t.expectingValue=!1,t.expectingLogical=!0):n.type="field"),"field"===n.type?(t.expectingOperator=!0,t.expectingField=!1,t.expectingValue=!1,t.expectingLogical=!1):"operator"===n.type?(t.expectingValue=!0,t.expectingField=!1,t.expectingOperator=!1,t.expectingLogical=!1):"value"===n.type?(t.expectingLogical=!0,t.expectingField=!1,t.expectingOperator=!1,t.expectingValue=!1):"logical"===n.type?(t.expectingField=!0,t.expectingOperator=!1,t.expectingValue=!1,t.expectingLogical=!1):"parenthesis"===n.type?"("===n.value?(t.parenthesesLevel++,t.expectingField=!0,t.expectingOperator=!1,t.expectingValue=!1,t.expectingLogical=!1):(t.parenthesesLevel--,t.expectingLogical=!0,t.expectingField=!1,t.expectingOperator=!1,t.expectingValue=!1):"keyword"===n.type&&("ORDER"===n.value.toUpperCase()?(t.inOrderBy=!0,t.expectingField=!1,t.expectingOperator=!1,t.expectingValue=!1,t.expectingLogical=!1):"BY"===n.value.toUpperCase()&&(t.expectingField=!0,t.expectingOperator=!1,t.expectingValue=!1,t.expectingLogical=!1)))}return e}parse(e){const t=[];let n,i=[];try{const{whereClause:t,orderByClause:s}=this.splitQuery(e);if(t.trim()){const e=this.tokenize(t),i=new r;n=i.parse(e)||void 0}s.trim()&&(i=this.parseOrderBy(s))}catch(s){t.push(s instanceof Error?s.message:"Parse error")}return{where:n||void 0,orderBy:i,raw:e,valid:0===t.length,errors:t}}splitQuery(e){const t=e.match(/\s*ORDER\s+BY\s+/i);if(!t)return{whereClause:e,orderByClause:""};const r=t.index+t[0].length;return{whereClause:e.substring(0,t.index),orderByClause:e.substring(r)}}parseOrderBy(e){const t=[],r=e.split(",").map((e=>e.trim()));for(const n of r){const e=n.trim().split(/\s+/);if(0===e.length||!e[0])continue;const r=e[0],i=e.length>1&&"DESC"===e[1].toUpperCase()?"DESC":"ASC";t.push({field:r,direction:i})}return t}},exports.countConditions=function e(t){return o(t)?1:a(t)?t.conditions.reduce(((t,r)=>t+e(r)),0):0},exports.getUsedFields=function e(t){return o(t)?[t.field]:a(t)?t.conditions.flatMap(e):[]},exports.isCondition=o,exports.isLogicalGroup=a,exports.printExpression=function e(t,r=0){const n=" ".repeat(r);if(o(t))return`${n}${t.field} ${t.operator} ${Array.isArray(t.value)?`[${t.value.join(", ")}]`:t.value}`;if(a(t)){const i=t.conditions.map((t=>e(t,r+1))).join("\n");return`${n}${t.operator}:\n${i}`}return""},exports.toMongooseQuery=p,exports.toSQLQuery=l; //# sourceMappingURL=index.js.map