sqlparser-devexpress 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LISCENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Rohit Mahajan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # SQLParser
2
+
3
+ SQLParser is a JavaScript library that converts SQL-like filter strings into DevExpress format filters. It provides utilities for parsing, sanitizing, and converting SQL-like expressions into a format that can be used with DevExpress components.
4
+
5
+ ## Usage
6
+
7
+ ### Convert SQL to AST
8
+
9
+ To convert a SQL-like filter string to an Abstract Syntax Tree (AST):
10
+
11
+ ```javascript
12
+ const filterString= "(ID <> {Item.ID}) AND (ItemGroupType IN ({Item.AllowedItemGroupType}))";
13
+ const parsedResult = convertSQLToAst(filterString);
14
+ ```
15
+
16
+ ### Convert AST to DevExpress Format
17
+
18
+ To convert an AST to DevExpress format:
19
+
20
+ ```javascript
21
+ const ast = { /* your AST here */ };
22
+ const variables = [/* your variables here */];
23
+ const state = { /* your state here */ };
24
+
25
+ const devExpressFilter = convertAstToDevextreme(ast, variables, state);
26
+
27
+ console.log(devExpressFilter);
28
+ ```
29
+
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "sqlparser-devexpress",
3
+ "version": "2.0.1",
4
+ "main": "index.js",
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "vitest"
8
+ },
9
+ "keywords": [],
10
+ "author": "Rohit Mahajan",
11
+ "license": "MIT",
12
+ "description": "",
13
+ "dependencies": {
14
+ "vitest": "^3.0.5"
15
+ }
16
+ }
@@ -0,0 +1,351 @@
1
+ const logicalOperators = ['and', 'or'];
2
+
3
+ /**
4
+ * Main conversion function that sets up the global context
5
+ * @param {Object} ast - The abstract syntax tree
6
+ * @param {Array} vars - Array of variable names
7
+ * @param {Object} results - Optional object for placeholder resolution
8
+ * @returns {Array|null} DevExpress format filter
9
+ */
10
+ function DevExpressConverter() {
11
+ // Global variables accessible throughout the converter
12
+ let resultObject = null;
13
+ let primaryEntity = null;
14
+ let primaryKey = null;
15
+ let variables = [];
16
+ const EnableShortCircuit = true;
17
+
18
+ /**
19
+ * Main conversion function that sets up the global context
20
+ * @param {Object} ast - The abstract syntax tree
21
+ * @param {Array} vars - Array of variable names
22
+ * @param {Object} ResultObject - Optional object for placeholder resolution
23
+ * @param {string} primaryEntity - Optional primary entity name
24
+ * @param {string} primaryKey - Optional primary key value
25
+ * @returns {Array|null} DevExpress format filter
26
+ */
27
+ function convert(ast, vars, ResultObject = null, PrimaryEntity = null, PrimaryKey = null) {
28
+ // Set up global context
29
+ variables = vars;
30
+ primaryKey = PrimaryKey;
31
+ primaryEntity = PrimaryEntity;
32
+ resultObject = ResultObject;
33
+
34
+ // Process the AST
35
+ return processAstNode(ast);
36
+ }
37
+
38
+ /**
39
+ * Process an AST node based on its type
40
+ * @param {Object} ast - The AST node to process
41
+ * @param {string} parentOperator - The operator of the parent logical node (if any)
42
+ * @returns {Array|null|boolean} DevExpress format filter or boolean for short-circuit
43
+ */
44
+ function processAstNode(ast, parentOperator = null) {
45
+ if (!ast) return null; // Return null if the AST is empty
46
+
47
+ switch (ast.type) {
48
+ case "logical":
49
+ return handleLogicalOperator(ast, parentOperator);
50
+ case "comparison":
51
+ return handleComparisonOperator(ast);
52
+ case "function":
53
+ return handleFunction(ast);
54
+ case "field":
55
+ case "value":
56
+ return convertValue(ast.value);
57
+ default:
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Handles logical operators (AND, OR) and applies short-circuit optimizations.
64
+ * @param {Object} ast - The logical operator AST node.
65
+ * @param {string} parentOperator - The operator of the parent logical node.
66
+ * @returns {Array|boolean} DevExpress format filter or boolean for short-circuit.
67
+ */
68
+ function handleLogicalOperator(ast, parentOperator) {
69
+ const operator = ast.operator.toLowerCase();
70
+
71
+ // Special case: Handle ISNULL comparison with a value
72
+ if (isNullCheck(ast.left, ast.right)) {
73
+ const resolvedValue = convertValue(ast.right);
74
+
75
+ // Short-circuit: If left argument is a placeholder, return boolean result directly
76
+ if (EnableShortCircuit && ast.left.args[0]?.value?.type === "placeholder") {
77
+ return resolvedValue == null;
78
+ }
79
+
80
+ return [processAstNode(ast.left), operator, null];
81
+ }
82
+ if (isNullCheck(ast.right, ast.left)) {
83
+ const resolvedValue = convertValue(ast.left);
84
+
85
+ // Short-circuit: If right argument is a placeholder, return boolean result directly
86
+ if (EnableShortCircuit && ast.right.args[0]?.value?.type === "placeholder") {
87
+ return resolvedValue == null;
88
+ }
89
+
90
+ return [null, operator, processAstNode(ast.right)];
91
+ }
92
+
93
+ // Recursively process left and right operands
94
+ const left = processAstNode(ast.left, operator);
95
+ const right = processAstNode(ast.right, operator);
96
+
97
+ if (EnableShortCircuit) {
98
+ // Short-circuit: always-true conditions
99
+ if (left === true || right === true) {
100
+ if (operator === 'or') return true;
101
+ return left === true ? right : left;
102
+ }
103
+
104
+ // Short-circuit: always-false conditions
105
+ if (left === false || right === false) {
106
+ return left === false ? right : left;
107
+ }
108
+
109
+ }
110
+
111
+ // Detect and flatten nested logical expressions
112
+ if (parentOperator === null) {
113
+ if (left.length === 3 && logicalOperators.includes(left[1])) parentOperator = left[1];
114
+ if (right.length === 3 && logicalOperators.includes(right[1])) parentOperator = right[1];
115
+ }
116
+
117
+ // Flatten nested logical expressions if applicable
118
+ if (shouldFlattenLogicalTree(parentOperator, operator, ast)) {
119
+ return flattenLogicalTree(left, operator, right);
120
+ }
121
+ return [left, operator, right];
122
+ }
123
+
124
+ /**
125
+ * Handles comparison operators (=, <>, IN, IS) and applies optimizations.
126
+ * @param {Object} ast - The comparison operator AST node.
127
+ * @returns {Array|boolean} DevExpress format filter or boolean for short-circuit.
128
+ */
129
+ function handleComparisonOperator(ast) {
130
+ const operator = ast.operator.toUpperCase();
131
+
132
+ // Handle "IS NULL" condition
133
+ if (operator === "IS" && ast.value === null) {
134
+ return [ast.field, "=", null];
135
+ }
136
+
137
+ // Handle "IN" condition, including comma-separated values
138
+ if (operator === "IN") {
139
+ return handleInOperator(ast);
140
+ }
141
+
142
+ const left = convertValue(ast.field);
143
+ const right = convertValue(ast.value);
144
+ const comparison = [left, ast.operator.toLowerCase(), right];
145
+
146
+ // Apply short-circuit evaluation if enabled
147
+ if (EnableShortCircuit) {
148
+ if (isAlwaysTrue(comparison)) return true;
149
+ if (isAlwaysFalse(comparison)) return false;
150
+ }
151
+
152
+ return comparison;
153
+ }
154
+
155
+ /**
156
+ * Handles function calls, focusing on ISNULL.
157
+ * @param {Object} ast - The function AST node.
158
+ * @returns {*} Resolved function result.
159
+ */
160
+ function handleFunction(ast) {
161
+ if (ast.name === "ISNULL" && ast.args && ast.args.length >= 2) {
162
+ const firstArg = ast.args[0];
163
+
164
+ // Resolve placeholders
165
+ if (firstArg.type === "placeholder") {
166
+ return resolvePlaceholderFromResultObject(firstArg.value);
167
+ }
168
+ return convertValue(firstArg);
169
+ }
170
+
171
+ // this should never happen as we are only handling ISNULL and should throw an error
172
+ throw new Error(`Unsupported function: ${ast.name}`);
173
+ }
174
+
175
+
176
+ /**
177
+ * Handles the IN operator specifically.
178
+ * @param {Object} ast - The comparison operator AST node.
179
+ * @returns {Array} DevExpress format filter.
180
+ */
181
+ function handleInOperator(ast) {
182
+ let resolvedValue = convertValue(ast.value);
183
+
184
+ // Handle comma-separated values in a string
185
+ if (Array.isArray(resolvedValue) && resolvedValue.length === 1) {
186
+ const firstValue = resolvedValue[0];
187
+ if (typeof firstValue === 'string' && firstValue.includes(',')) {
188
+ resolvedValue = firstValue.split(',').map(v => v.trim());
189
+ } else {
190
+ resolvedValue = firstValue;
191
+ }
192
+ }
193
+
194
+ return [ast.field, "in", resolvedValue];
195
+ }
196
+
197
+ /**
198
+ * Converts a single value, resolving placeholders and handling special cases.
199
+ * @param {*} val - The value to convert.
200
+ * @returns {*} Converted value.
201
+ */
202
+ function convertValue(val) {
203
+ if (val === null) return null;
204
+
205
+ // Handle array values
206
+ if (Array.isArray(val)) {
207
+ return val.map(item => convertValue(item));
208
+ }
209
+
210
+ // Handle object-based values
211
+ if (typeof val === "object") {
212
+ if (val.type === "placeholder") {
213
+ return resolvePlaceholderFromResultObject(val.value);
214
+ }
215
+
216
+ // Special handling for ISNULL function
217
+ if (val.type === "function" && val.name === "ISNULL" && val.args?.length >= 2) {
218
+ return convertValue(val.args[0]);
219
+ }
220
+
221
+ // Handle nested AST nodes
222
+ if (val.type) {
223
+ return processAstNode(val);
224
+ }
225
+ }
226
+
227
+ return val;
228
+ }
229
+
230
+ /**
231
+ * Resolves placeholder values from the result object.
232
+ * @param {string} placeholder - The placeholder to resolve.
233
+ * @returns {*} Resolved placeholder value.
234
+ */
235
+ function resolvePlaceholderFromResultObject(placeholder) {
236
+ if (!resultObject) return `{${placeholder}}`;
237
+
238
+
239
+ return resultObject.hasOwnProperty(placeholder) ? resultObject[placeholder] : `{${placeholder}}`;
240
+ }
241
+
242
+ /**
243
+ * Checks if a node is an ISNULL function check.
244
+ * @param {Object} node - The node to check.
245
+ * @param {Object} valueNode - The value node.
246
+ * @returns {boolean} True if this is an ISNULL check.
247
+ */
248
+ function isNullCheck(node, valueNode) {
249
+ return node?.type === "function" && node.name === "ISNULL" && valueNode?.type === "value";
250
+ }
251
+
252
+ /**
253
+ * Determines whether the logical tree should be flattened.
254
+ * This is based on the parent operator and the current operator.
255
+ * @param {string} parentOperator - The operator of the parent logical node.
256
+ * @param {string} operator - The operator of the current logical node.
257
+ * @param {Object} ast - The current AST node.
258
+ * @returns {boolean} True if the tree should be flattened, false otherwise.
259
+ */
260
+ function shouldFlattenLogicalTree(parentOperator, operator, ast) {
261
+ return parentOperator !== null && operator === parentOperator || ast.operator === ast.right?.operator;
262
+ }
263
+
264
+ /**
265
+ * Flattens a logical tree by combining nested logical nodes.
266
+ * @param {Array} left - The left side of the logical expression.
267
+ * @param {string} operator - The logical operator.
268
+ * @param {Array} right - The right side of the logical expression.
269
+ * @returns {Array} The flattened logical tree.
270
+ */
271
+ function flattenLogicalTree(left, operator, right) {
272
+ const parts = [];
273
+
274
+ // Flatten left side if it has the same operator
275
+ if (Array.isArray(left) && left[1] === operator) {
276
+ parts.push(...left);
277
+ } else {
278
+ parts.push(left);
279
+ }
280
+
281
+ // Add the operator
282
+ parts.push(operator);
283
+
284
+ // Flatten right side if it has the same operator
285
+ if (Array.isArray(right) && right[1] === operator) {
286
+ parts.push(...right);
287
+ } else {
288
+ parts.push(right);
289
+ }
290
+
291
+ return parts;
292
+ }
293
+
294
+
295
+ /**
296
+ * Checks if a condition is always true.
297
+ * @param {Array} condition - The condition to check.
298
+ * @returns {boolean} True if the condition is always true.
299
+ */
300
+ function isAlwaysTrue(condition) {
301
+ return Array.isArray(condition) && condition.length === 3 && evaluateExpression(...condition) == true;
302
+ }
303
+
304
+ /**
305
+ * Checks if a condition is always false.
306
+ * @param {Array} condition - The condition to check.
307
+ * @returns {boolean} True if the condition is always false.
308
+ */
309
+ function isAlwaysFalse(condition) {
310
+ return Array.isArray(condition) && condition.length === 3 && evaluateExpression(...condition) == false;
311
+ }
312
+
313
+ /**
314
+ * Evaluates a simple expression.
315
+ * @param {*} left - The left operand.
316
+ * @param {string} operator - The operator.
317
+ * @param {*} right - The right operand.
318
+ * @returns {boolean|null} The result of the evaluation or null if not evaluable.
319
+ */
320
+ function evaluateExpression(left, operator, right) {
321
+ if (isNaN(left) || isNaN(right) || left === null || right === null) return null;
322
+ switch (operator) {
323
+ case '=': case '==': return left === right;
324
+ case '<>': case '!=': return left !== right;
325
+ case '>': return left > right;
326
+ case '>=': return left >= right;
327
+ case '<': return left < right;
328
+ case '<=': return left <= right;
329
+ default: return false;
330
+ }
331
+ }
332
+
333
+ return { init: convert };
334
+
335
+ }
336
+
337
+ // Create a global instance
338
+ const devExpressConverter = DevExpressConverter();
339
+
340
+ /**
341
+ * Converts an abstract syntax tree to DevExpress format
342
+ * @param {Object} ast - The abstract syntax tree
343
+ * @param {Array} variables - Array of variable names
344
+ * @param {Object} resultObject - Optional object for placeholder resolution
345
+ * @param {string} primaryEntity - Optional primary entity name
346
+ * @param {string} primaryKey - Optional primary key value
347
+ * @returns {Array|null} DevExpress format filter
348
+ */
349
+ export function convertToDevExpressFormat({ ast, variables, resultObject = null, primaryEntity = null, primaryKey = null }) {
350
+ return devExpressConverter.init(ast, variables, resultObject, primaryEntity, primaryKey);
351
+ }
@@ -0,0 +1,156 @@
1
+ import { Tokenizer } from "./tokenizer.js";
2
+
3
+ // Define operator precedence for parsing expressions
4
+ const precedence = {
5
+ "OR": 1, "AND": 2, "=": 3, "!=": 3, ">": 3, "<": 3, ">=": 3, "<=": 3,
6
+ "IN": 3, "<>": 3, "LIKE": 3, "IS": 3, "BETWEEN": 3
7
+ };
8
+
9
+ // Regular expression to check for unsupported SQL patterns (like SELECT-FROM or JOIN statements)
10
+ const unsupportedPattern = /\bSELECT\b.*\bFROM\b|\bINNER\s+JOIN\b/i;
11
+
12
+ export function parse(input, variables = []) {
13
+
14
+ // Return null if the input contains unsupported SQL statements
15
+ if (unsupportedPattern.test(input)) {
16
+ return null;
17
+ }
18
+
19
+ const tokenizer = new Tokenizer(input);
20
+ let currentToken = tokenizer.nextToken();
21
+
22
+ // Moves to the next token in the input
23
+ function next() {
24
+ currentToken = tokenizer.nextToken();
25
+ }
26
+
27
+ // Parses logical expressions using operator precedence
28
+ function parseExpression(minPrecedence = 0) {
29
+ let left = parseTerm();
30
+
31
+ // Continue parsing while the current token is an operator with sufficient precedence
32
+ while (currentToken && currentToken.type === "operator" && precedence[currentToken.value.toUpperCase()] >= minPrecedence) {
33
+ const operator = currentToken.value.toUpperCase();
34
+ next(); // Move to the next token
35
+
36
+ // Recursively parse the right-hand expression with adjusted precedence
37
+ const right = parseExpression(precedence[operator]);
38
+ left = { type: "logical", operator, left, right };
39
+ }
40
+
41
+ return left;
42
+ }
43
+
44
+ // Parses individual terms, including literals, functions, and comparisons
45
+ function parseTerm() {
46
+ if (!currentToken) throw new Error("Unexpected end of input");
47
+
48
+ // Handle parenthesized expressions
49
+ if (currentToken.type === "paren" && currentToken.value === "(") {
50
+ next();
51
+ const expr = parseExpression();
52
+ if (!currentToken || currentToken.value !== ")") throw new Error("Missing closing parenthesis");
53
+ next();
54
+ return expr;
55
+ }
56
+
57
+ // Handle function calls like ISNULL(field)
58
+ if (currentToken.type === "function") {
59
+ const funcName = currentToken.value.toUpperCase();
60
+ next();
61
+ if (!currentToken || currentToken.value !== "(") throw new Error(`Expected ( after ${funcName}`);
62
+ next();
63
+
64
+ const args = [];
65
+ while (currentToken && currentToken.value !== ")") {
66
+ args.push(parseExpression());
67
+ if (currentToken && currentToken.value === ",") next();
68
+ }
69
+
70
+ next(); // Consume the closing parenthesis
71
+ return { type: "function", name: funcName, args };
72
+ }
73
+
74
+ // Handle literal values (numbers, strings, null)
75
+ if (["number", "string", "null"].includes(currentToken.type)) {
76
+ const value = parseValue();
77
+ return { type: "value", value };
78
+ }
79
+
80
+ // Otherwise, assume it's a field name
81
+ const field = parseValue();
82
+
83
+ // Check if it's part of a comparison expression
84
+ if (currentToken && currentToken.type === "operator") {
85
+ const operator = currentToken.value.toLowerCase();
86
+ next();
87
+
88
+ if (operator === "between") {
89
+ // Parse BETWEEN operator which requires two values separated by AND
90
+ const firstValue = parseValue();
91
+
92
+ if (!currentToken || currentToken.value.toUpperCase() !== "AND") {
93
+ throw new Error("Expected AND after BETWEEN");
94
+ }
95
+ next(); // Consume AND
96
+
97
+ const secondValue = parseValue();
98
+
99
+ return {
100
+ type: "comparison",
101
+ field,
102
+ operator,
103
+ value: [firstValue, secondValue] // Store both values in an array
104
+ };
105
+ }
106
+
107
+ // For other comparison operators, parse a single right-hand value
108
+ const value = parseValue(operator);
109
+ return { type: "comparison", field, operator, value };
110
+ }
111
+
112
+ return { type: "field", value: field };
113
+ }
114
+
115
+ // Parses values including numbers, strings, placeholders, and IN lists
116
+ function parseValue(operatorToken) {
117
+ if (!currentToken) throw new Error("Unexpected end of input");
118
+
119
+ const token = currentToken;
120
+ next(); // Move to the next token
121
+
122
+ if (token.type === "number") return Number(token.value);
123
+ if (token.type === "string") return token.value.slice(1, -1).replace(/''/g, "");
124
+ if (token.type === "identifier") return token.value;
125
+ if (token.type === "null") return null;
126
+
127
+ // Handle placeholders like `{VariableName}`
128
+ if (token.type === "placeholder") {
129
+ const val = token.value.slice(1, -1);
130
+ if (!variables.includes(val)) variables.push(val);
131
+ return { type: "placeholder", value: val };
132
+ }
133
+
134
+ // Handle IN operator which requires a list of values
135
+ if (operatorToken && operatorToken.toUpperCase() === "IN") {
136
+ if (!token || token.value !== "(") throw new Error("Expected ( after IN");
137
+
138
+ const values = [];
139
+ while (currentToken && currentToken.value !== ")") {
140
+ if (currentToken.type === "comma") {
141
+ next();
142
+ continue;
143
+ }
144
+ values.push(parseValue());
145
+ }
146
+
147
+ if (currentToken && currentToken.value === ")") next(); // Consume closing parenthesis
148
+ return { type: "value", value: values };
149
+ }
150
+
151
+ throw new Error(`Unexpected value: ${token.value}`);
152
+ }
153
+
154
+ // Start parsing and return the AST with extracted variables
155
+ return { ast: parseExpression(), variables };
156
+ }
@@ -0,0 +1,43 @@
1
+ function sanitizeQuery(sql) {
2
+ // First check if the SQL has the pipe format for placeholders
3
+ if (sql.includes("|")) {
4
+ const [mainQuery, ...placeholderSections] = sql.split("|").map(s => s.trim());
5
+ const variables = [];
6
+
7
+ // Extract placeholders from | [arg1] | [arg2] sections
8
+ placeholderSections.forEach(section => {
9
+ if (section.startsWith("[") && section.endsWith("]")) {
10
+ const placeholders = section
11
+ .slice(1, -1) // Remove [ and ]
12
+ .split(",")
13
+ .map(s => s.trim());
14
+ variables.push(...placeholders);
15
+ }
16
+ });
17
+
18
+ // Replace {0}, {1}, etc., with variable names
19
+ const sanitizedSQL = mainQuery.replace(/\{(\d+)\}/g, (_, index) => {
20
+ return variables[index] ? `{${variables[index]}}` : `{placeholder${index}}`;
21
+ });
22
+
23
+ return { sanitizedSQL, variables };
24
+ }
25
+ // Handle SQL with directly embedded placeholders like {WorkOrderLine.ApplicableUoms}
26
+ else {
27
+ const variables = [];
28
+ const placeholderRegex = /\{([^}]+)\}/g;
29
+ let match;
30
+
31
+ // Extract all placeholders
32
+ while ((match = placeholderRegex.exec(sql)) !== null) {
33
+ const placeholder = match[1];
34
+ if (!variables.includes(placeholder)) {
35
+ variables.push(placeholder);
36
+ }
37
+ }
38
+
39
+ return { sanitizedSQL: sql, variables };
40
+ }
41
+ }
42
+
43
+ export { sanitizeQuery };
@@ -0,0 +1,58 @@
1
+ // Define regex patterns for different token types
2
+ const tokenPatterns = {
3
+ whitespace: "\\s+", // Matches spaces, tabs, and newlines
4
+ function: "\\b(ISNULL)\\b", // Matches function names like ISNULL (case-insensitive)
5
+ null: "\\bNULL\\b", // Matches NULL as a keyword
6
+ number: "\\d+", // Matches numerical values
7
+ placeholder: "'?\\{[^}]+\\}'?", // Matches placeholders like {variable} or '{variable}'
8
+ string: "'(?:''|[^'])*'", // Matches strings, allowing for escaped single quotes ('')
9
+ operator: "=>|<=|!=|>=|=|<>|>|<|AND|OR|BETWEEN|IN|LIKE|IS", // Matches SQL operators and logical keywords
10
+ identifier: "[\\w.]+", // Matches identifiers, including table.column format
11
+ paren: "[()]", // Matches parentheses
12
+ comma: "," // Matches commas
13
+ };
14
+
15
+ // Create a Map for O(1) token type lookup
16
+ const tokenTypeMap = new Map(Object.entries(tokenPatterns));
17
+
18
+ // Combine all token patterns into a single regular expression using named capture groups
19
+ const combinedRegex = new RegExp(
20
+ [...tokenTypeMap.keys()].map(name => `(?<${name}>${tokenPatterns[name]})`).join("|"),
21
+ "iy" // 'i' makes it case-insensitive, 'y' ensures it matches from the current index
22
+ );
23
+
24
+ class Tokenizer {
25
+ constructor(input) {
26
+ this.input = input; // The input SQL-like string to be tokenized
27
+ this.index = 0; // Tracks the current position in the input
28
+ }
29
+
30
+ nextToken() {
31
+ if (this.index >= this.input.length) return null; // Stop if we've reached the end
32
+
33
+ combinedRegex.lastIndex = this.index; // Ensure regex starts from the current index
34
+ const match = combinedRegex.exec(this.input); // Execute regex to find the next token
35
+
36
+ if (match) {
37
+ this.index = combinedRegex.lastIndex; // Move index to the end of the matched token
38
+
39
+ // Find the first matched token type in O(1) time using the tokenTypeMap
40
+ const type = [...tokenTypeMap.keys()].find(name => match.groups[name] !== undefined);
41
+
42
+ // Skip whitespace tokens
43
+ if (!type || type === "whitespace") return this.nextToken();
44
+
45
+ let value = match.groups[type];
46
+
47
+ // Remove surrounding single quotes from placeholders
48
+ if (type === "placeholder") value = value.replace(/^['"]|['"]$/g, "");
49
+
50
+ return { type, value };
51
+ }
52
+
53
+ // If no valid token is found, throw an error with the remaining input for debugging
54
+ throw new Error(`Unexpected token at: ${this.input.slice(this.index)}`);
55
+ }
56
+ }
57
+
58
+ export { Tokenizer };
package/src/index.js ADDED
@@ -0,0 +1,43 @@
1
+ import { convertToDevExpressFormat } from "./core/converter.js";
2
+ import { parse } from "./core/parser.js";
3
+ import { sanitizeQuery } from "./core/sanitizer.js";
4
+
5
+
6
+ export function convertSQLToAst(filterString, SampleData = null, enableConsoleLogs = false) {
7
+ let { sanitizedSQL, extractedVariables } = sanitizeQuery(filterString);
8
+ enableConsoleLogs && console.log("Sanitized SQL:", sanitizedSQL, "\n");
9
+
10
+ const parsedResult = parse(sanitizedSQL, extractedVariables);
11
+
12
+ enableConsoleLogs && console.log("Extracted Variables:", JSON.stringify(parsedResult.variables, null, 2), "\n");
13
+ enableConsoleLogs && console.log("AST Tree:", JSON.stringify(parsedResult.ast, null, 2), "\n");
14
+
15
+ return parsedResult;
16
+ }
17
+
18
+ export function convertAstToDevextreme(ast, variables, state) {
19
+ return convertToDevExpressFormat({ ast, variables, resultObject: state })
20
+ }
21
+
22
+ // export function parseFilterString(filterString, sampleData = null) {
23
+ // if (filterString.toUpperCase().startsWith("SELECT")) return null; // Skip full SQL queries
24
+
25
+ // let { sanitizedSQL, extractedVariables } = sanitizeQuery(filterString);
26
+ // console.log("Sanitized SQL:", sanitizedSQL, "\n");
27
+
28
+ // const parsedResult = parse(sanitizedSQL, extractedVariables);
29
+ // extractedVariables = parsedResult.variables;
30
+ // console.log("Extracted Variables:", JSON.stringify(extractedVariables, null, 2), "\n");
31
+
32
+ // const astTree = parsedResult.ast;
33
+ // console.log("AST Tree:", JSON.stringify(astTree, null, 2), "\n");
34
+
35
+ // return convertToDevExpressFormat({ ast: astTree, variables: extractedVariables, resultObject: sampleData });
36
+ // }
37
+
38
+ // Example usage
39
+ // const devExpressFilter = parseFilterString("((ISNULL({0}, 0) = 0 AND CompanyID = {1}) OR CompanyID IS NULL) OR BranchID = {0} | [LeadDocument.BranchID] | [LeadDocument.CompanyID]", sampleResultObject);
40
+ // const devExpressFilter = parseFilterString("FromDate <= '{TransferOutwardDocument.DocDate}' ", sampleResultObject, "TransferOutwardDocument", "789");
41
+ // const devExpressFilter = parseFilterString("(RS2ID in ({SaleOrderStatusStmtGlobalRpt.StateID}) Or ({SaleOrderStatusStmtGlobalRpt.StateID} =0)) And (RS3ID in (0,{SaleOrderStatusStmtGlobalRpt.RegionID}) Or {SaleOrderStatusStmtGlobalRpt.RegionID} =0 )", sampleResultObject,);
42
+
43
+ // console.log("DevExpress Filter:", JSON.stringify(devExpressFilter, null, 2));
package/src/utils.js ADDED
@@ -0,0 +1,14 @@
1
+ export function replacePlaceholders(sql, staticValues = {}, dynamicValues = []) {
2
+ // Replace static placeholders (e.g., {0})
3
+ sql = sql.replace(/\{(\w+)\}/g, (_, key) => staticValues[key] ?? `{${key}}`);
4
+
5
+ // Replace dynamic placeholders (e.g., | [AccountTaxPaymentDocument.CompanyID])
6
+ sql = sql.replace(/\|\s*\[([^\]]+)\]/g, (_, keys) => {
7
+ const replacements = keys.split(",").map(key => dynamicValues.shift() ?? `{${key.trim()}}`);
8
+ return replacements.join(", ");
9
+ });
10
+
11
+ return sql;
12
+ }
13
+
14
+ module.exports = { replacePlaceholders };
@@ -0,0 +1,197 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { convertToDevExpressFormat } from "../src/core/converter";
3
+ import { parse } from "../src/core/parser";
4
+ import { sanitizeQuery } from "../src/core/sanitizer";
5
+
6
+ describe("Parser SQL to dx Filter Builder", () => {
7
+ const testCases = [
8
+ {
9
+ input: "(ID = {CoreEntity0022.CompanyGroupID} OR ISNULL({CoreEntity0022.CompanyGroupID},0) = 0)",
10
+ expected: [
11
+ // [
12
+ "ID", "=", 42
13
+ // ],
14
+ // "or",
15
+ // [42, "=", null],
16
+ ],
17
+ },
18
+ {
19
+ input: "GroupNo = {Employee.District} OR ISNULL(GroupNo,0) = 0 OR {Employee.District} = 0",
20
+ expected: [
21
+ // ["GroupNo", "=", 0],
22
+ // "or",
23
+ // ["GroupNo", "=", null],
24
+ // "or",
25
+ // [0, "=", 0],
26
+ ],
27
+ },
28
+ {
29
+ input: "ContactID = {SaleInvoiceDocument.ContactID} AND AddressType IN (2, 4)",
30
+ expected: [
31
+ ["ContactID", "=", 42],
32
+ "and",
33
+ ["AddressType", "in", [2, 4]],
34
+ ],
35
+ },
36
+ {
37
+ input: "ID IN ({WorkOrderLine.ApplicableUoms}) AND (CompanyID = {WorkOrderDocument.CompanyID} OR {WorkOrderDocument.CompanyID} = 0)",
38
+ expected: [
39
+ ["ID", "in", ["UOM1", "UOM2", "UOM3"]],
40
+ "and",
41
+ // [
42
+ ["CompanyID", "=", 42],
43
+ // "or",
44
+ // [42, "=", 0],
45
+ // ],
46
+ ],
47
+ },
48
+ {
49
+ input: "CompanyID = {AccountingRule.CompanyID}",
50
+ expected: [
51
+ "CompanyID", "=", 42,
52
+ ],
53
+ },
54
+ {
55
+ input: "(Level > {Area.AreaType})",
56
+ expected: [
57
+ "Level", ">", 42
58
+ ]
59
+ },
60
+ {
61
+ input: "(ID <> {Item.ID}) AND (ItemGroupType IN ({Item.AllowedItemGroupType}))",
62
+ expected: [
63
+ ["ID", "<>", 42],
64
+ 'and',
65
+ ["ItemGroupType", "in", ["1", "2"]]
66
+ ]
67
+ },
68
+ {
69
+ input: "((FromDate <= '{TransferOutwardDocument.DocDate}' AND ToDate >= '{TransferOutwardDocument.DocDate}') OR ToDate is NULL) AND (BranchID = {TransferOutwardDocument.RefBranchID} OR RefBranchID is NULL) AND (CompanyID = {TransferOutwardDocument.CompanyID} OR {TransferOutwardDocument.CompanyID} = 0 OR CompanyID is NULL)",
70
+ expected: [
71
+ [
72
+ [
73
+ ["FromDate", "<=", "2022-01-01"],
74
+ 'and',
75
+ ["ToDate", ">=", "2022-01-01"]
76
+ ],
77
+ 'or',
78
+ ["ToDate", "=", null]
79
+ ],
80
+ "and",
81
+ [
82
+ ["BranchID", "=", 42],
83
+ "or",
84
+ ["RefBranchID", "=", null]
85
+ ],
86
+ "and",
87
+ [
88
+ ["CompanyID", "=", 7],
89
+ // "or",
90
+ // [7,"=",0],
91
+ "or",
92
+ ["CompanyID", "=", null]
93
+ ]
94
+ ]
95
+ },
96
+ {
97
+ input: "(ID <> {Item.ID}) AND ( ItemGroupType = '''')",
98
+ expected: [
99
+ ["ID", "<>", 42],
100
+ 'and',
101
+ ["ItemGroupType", "=", ""]
102
+ ]
103
+ },
104
+ {
105
+ input: "NULL",
106
+ expected: null
107
+ },
108
+ {
109
+ input: "((ISNULL({0}, 0) = 0 AND CompanyID = {1}) OR CompanyID IS NULL) OR BranchID = {0} | [LeadDocument.BranchID] | [LeadDocument.CompanyID]",
110
+ expected: [
111
+ // [
112
+ // [
113
+ // [42,"=",null],
114
+ // "and",
115
+ ["CompanyID", "=", 7],
116
+ // ],
117
+ "or",
118
+ ["CompanyID", "=", null],
119
+ // ],
120
+ 'or',
121
+ ["BranchID", "=", 42]
122
+ ]
123
+ },
124
+ {
125
+ input: "SELECT DISTINCT OP.DocID ID,OP.DocName,OP.DocType,OP.DocName [Work Purchase Order],OP.DocDate DocDate,SP.WoStatus,OP.DocDate [Work Purchase Order Date], OP.CompanyID, cast(cast(OP.DocDate as date) as varchar(10)) DocumentDate FROM OpenDocuments OP inner join PurchaseHeader PH on PH.Id=op.DocID inner JOIN PurchasePosting PP ON PP.DocID = PH.ID inner JOIN SalePosting SP ON SP.PurchasePostingLineID = PP.ID",
126
+ expect: null
127
+ },
128
+ {
129
+ input: "FromDate Between '10-10-2021' AND '10-10-2022'",
130
+ expected: [
131
+ "FromDate", "between", ["10-10-2021", "10-10-2022"]
132
+ ]
133
+ }
134
+ ];
135
+
136
+ testCases.forEach(({ input, expected }, index) => {
137
+ it(`Test Case ${index + 1}: ${input}`, () => {
138
+
139
+ if (expected == undefined) {
140
+ expected = null
141
+ }
142
+
143
+ // Need to handle NULL as a special case
144
+ if (input.toLowerCase() === "null") {
145
+ expect(null).toEqual(null);
146
+ return;
147
+ }
148
+
149
+ let { sanitizedSQL, variables } = sanitizeQuery(input);
150
+
151
+ const astwithVariables = parse(sanitizedSQL, variables);
152
+
153
+ if (astwithVariables == null) {
154
+ expect(null).toEqual(expected);
155
+ return;
156
+ }
157
+
158
+ variables = astwithVariables.variables;
159
+ const ast = astwithVariables.ast;
160
+
161
+ const result = convertToDevExpressFormat({ ast, variables, resultObject: sampleData });
162
+
163
+ if (result == null || result == true || result == false) {
164
+ expect([]).toEqual(expected);
165
+ return;
166
+ }
167
+
168
+ expect(result).toEqual(expected);
169
+ });
170
+ });
171
+ });
172
+
173
+
174
+ const sampleData = {
175
+ "CoreEntity0022.CompanyGroupID": 42,
176
+ "CoreEntity0022.BranchID": 7,
177
+ "Employee.District": 0,
178
+ "Employee.BranchID": 7,
179
+ "SaleInvoiceDocument.ContactID": 42,
180
+ "SaleInvoiceDocument.BranchID": 7,
181
+ "AccountingRule.CompanyID": 42,
182
+ "AccountingRule.BranchID": 7,
183
+ "Area.AreaType": 42,
184
+ "Area.BranchID": 7,
185
+ "Item.ID": 42,
186
+ "Item.BranchID": 7,
187
+ "Item.AllowedItemGroupType": "1,2",
188
+ "WorkOrderLine.ApplicableUoms": ["UOM1", "UOM2", "UOM3"],
189
+ "WorkOrderLine.CompanyID": 2,
190
+ "WorkOrderDocument.CompanyID": 42,
191
+ "WorkOrderDocument.BranchID": 7,
192
+ "TransferOutwardDocument.DocDate": "2022-01-01",
193
+ "TransferOutwardDocument.RefBranchID": 42,
194
+ "TransferOutwardDocument.CompanyID": 7,
195
+ "LeadDocument.BranchID": 42,
196
+ "LeadDocument.CompanyID": 7
197
+ };