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 +21 -0
- package/README.md +29 -0
- package/package.json +16 -0
- package/src/core/converter.js +351 -0
- package/src/core/parser.js +156 -0
- package/src/core/sanitizer.js +43 -0
- package/src/core/tokenizer.js +58 -0
- package/src/index.js +43 -0
- package/src/utils.js +14 -0
- package/tests/parser.test.js +197 -0
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
|
+
};
|