sqlparser-devexpress 2.3.9 → 2.3.11
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/package.json +1 -1
- package/src/@types/core/converter.d.ts +22 -0
- package/src/@types/core/parser.d.ts +27 -0
- package/src/@types/core/sanitizer.d.ts +28 -0
- package/src/@types/core/tokenizer.d.ts +8 -0
- package/src/@types/default.d.ts +39 -33
- package/src/core/converter.js +29 -12
- package/src/core/parser.js +21 -9
- package/tests/parser.test.js +16 -1
package/package.json
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ASTNode } from "./parser.js";
|
|
2
|
+
|
|
3
|
+
export interface ResultObject {
|
|
4
|
+
[key: string]: any;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type DevExpressFilter = any[] | null;
|
|
8
|
+
|
|
9
|
+
export interface ConvertOptions {
|
|
10
|
+
ast: ASTNode;
|
|
11
|
+
resultObject?: ResultObject;
|
|
12
|
+
enableShortCircuit?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts an abstract syntax tree (AST) to DevExpress filter format.
|
|
17
|
+
* This function uses short-circuit evaluation for optimization.
|
|
18
|
+
*
|
|
19
|
+
* @param options - The conversion options containing AST, result object, and short-circuit flag.
|
|
20
|
+
* @returns DevExpressFilter - The DevExpress compatible filter array or null.
|
|
21
|
+
*/
|
|
22
|
+
export function convertToDevExpressFormat(options: ConvertOptions): DevExpressFilter;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface ASTNode {
|
|
2
|
+
type: string;
|
|
3
|
+
operator?: string;
|
|
4
|
+
field?: string;
|
|
5
|
+
value?: any;
|
|
6
|
+
left?: ASTNode;
|
|
7
|
+
right?: ASTNode;
|
|
8
|
+
args?: ASTNode[];
|
|
9
|
+
name?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Represents the result of the parse function.
|
|
14
|
+
*/
|
|
15
|
+
export interface ParseResult {
|
|
16
|
+
ast: ASTNode;
|
|
17
|
+
variables: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The main parse function that converts SQL-like queries into AST.
|
|
22
|
+
*
|
|
23
|
+
* @param input - The SQL-like string to be parsed.
|
|
24
|
+
* @param variables - The list of extracted variables during parsing.
|
|
25
|
+
* @returns ParseResult - The resulting AST and extracted variables.
|
|
26
|
+
*/
|
|
27
|
+
export function parse(input: string, variables?: string[]): ParseResult;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents the result of sanitizing the SQL query.
|
|
3
|
+
*/
|
|
4
|
+
export interface SanitizeResult {
|
|
5
|
+
sanitizedSQL: string;
|
|
6
|
+
variables: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sanitizes the SQL query by replacing placeholders or pipe-separated variables
|
|
11
|
+
* with standardized `{placeholder}` format and extracts all variable names.
|
|
12
|
+
*
|
|
13
|
+
* Example Input:
|
|
14
|
+
* ```
|
|
15
|
+
* SELECT * FROM Orders WHERE CustomerID = {0} | [CustomerID]
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Output:
|
|
19
|
+
* ```
|
|
20
|
+
* {
|
|
21
|
+
* sanitizedSQL: "SELECT * FROM Orders WHERE CustomerID = {CustomerID}",
|
|
22
|
+
* variables: ["CustomerID"]
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* @param sql - The raw SQL query containing placeholders or pipes.
|
|
26
|
+
* @returns SanitizeResult - The cleaned SQL query and extracted variables.
|
|
27
|
+
*/
|
|
28
|
+
export function sanitizeQuery(sql: string): SanitizeResult;
|
package/src/@types/default.d.ts
CHANGED
|
@@ -1,35 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
|
|
1
|
+
import { DevExpressFilter, ResultObject } from "./core/converter";
|
|
2
|
+
import { ASTNode, ParseResult } from "./core/parser";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts an SQL-like filter string into an Abstract Syntax Tree (AST).
|
|
6
|
+
* It also extracts variables from placeholders like `{CustomerID}` or pipe-separated sections.
|
|
7
|
+
* Optionally logs the conversion process if `enableConsoleLogs` is `true`.
|
|
8
|
+
*
|
|
9
|
+
* Example:
|
|
10
|
+
* ```
|
|
11
|
+
* const { ast, variables } = convertSQLToAst("ID = {CustomerID} AND Status = {OrderStatus}");
|
|
12
|
+
* console.log(ast);
|
|
13
|
+
* console.log(variables);
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @param filterString - The raw SQL-like filter string.
|
|
17
|
+
* @param enableConsoleLogs - Whether to log the parsing and sanitization process.
|
|
18
|
+
* @returns ParseResult - The AST and extracted variables.
|
|
19
|
+
*/
|
|
20
|
+
export function convertSQLToAst(filterString: string, enableConsoleLogs?: boolean): ParseResult;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Converts an Abstract Syntax Tree (AST) into a DevExpress-compatible filter format.
|
|
24
|
+
* Optionally supports a result object for dynamic value resolution and short-circuit evaluation.
|
|
25
|
+
*
|
|
26
|
+
* Example:
|
|
27
|
+
* ```
|
|
28
|
+
* const filter = convertAstToDevextreme(ast, state, true);
|
|
29
|
+
* console.log(filter);
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @param ast - The parsed AST from `convertSQLToAst`.
|
|
33
|
+
* @param state - An optional result object to resolve placeholders to actual values.
|
|
34
|
+
* @param enableShortCircuit - Whether to apply short-circuit evaluation.
|
|
35
|
+
* @returns DevExpressFilter - The DevExpress-compatible filter array or null.
|
|
36
|
+
*/
|
|
31
37
|
export function convertAstToDevextreme(
|
|
32
|
-
ast:
|
|
33
|
-
state?:
|
|
38
|
+
ast: ASTNode,
|
|
39
|
+
state?: ResultObject | null,
|
|
34
40
|
enableShortCircuit?: boolean,
|
|
35
|
-
):
|
|
41
|
+
): DevExpressFilter;
|
package/src/core/converter.js
CHANGED
|
@@ -48,7 +48,7 @@ function DevExpressConverter() {
|
|
|
48
48
|
return handleFunction(ast);
|
|
49
49
|
case "field":
|
|
50
50
|
case "value":
|
|
51
|
-
return convertValue(ast.value);
|
|
51
|
+
return convertValue(ast.value, parentOperator);
|
|
52
52
|
default:
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
@@ -113,6 +113,7 @@ function DevExpressConverter() {
|
|
|
113
113
|
if (shouldFlattenLogicalTree(parentOperator, operator, ast)) {
|
|
114
114
|
return flattenLogicalTree(left, operator, right);
|
|
115
115
|
}
|
|
116
|
+
|
|
116
117
|
return [left, operator, right];
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -135,7 +136,9 @@ function DevExpressConverter() {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
const left = ast.left !== undefined ? processAstNode(ast.left) : convertValue(ast.field);
|
|
139
|
+
const leftDefault = ast.left?.args[1]?.value;
|
|
138
140
|
const right = ast.right !== undefined ? processAstNode(ast.right) : convertValue(ast.value);
|
|
141
|
+
const rightDefault = ast.right?.args[1]?.value;
|
|
139
142
|
let operatorToken = ast.operator.toLowerCase();
|
|
140
143
|
|
|
141
144
|
if (operatorToken === "like") {
|
|
@@ -155,8 +158,8 @@ function DevExpressConverter() {
|
|
|
155
158
|
|
|
156
159
|
// Apply short-circuit evaluation if enabled
|
|
157
160
|
if (EnableShortCircuit) {
|
|
158
|
-
if (isAlwaysTrue(comparison)) return true;
|
|
159
|
-
if (isAlwaysFalse(comparison)) return false;
|
|
161
|
+
if (isAlwaysTrue(comparison, leftDefault, rightDefault)) return true;
|
|
162
|
+
if (isAlwaysFalse(comparison, leftDefault, rightDefault)) return false;
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
return comparison;
|
|
@@ -205,20 +208,21 @@ function DevExpressConverter() {
|
|
|
205
208
|
|
|
206
209
|
let operatorToken = operator === "IN" ? '=' : operator === "NOT IN" ? '!=' : operator;
|
|
207
210
|
let joinOperatorToken = operator === "IN" ? 'or' : operator === "NOT IN" ? 'and' : operator;
|
|
208
|
-
|
|
211
|
+
let field = convertValue(ast.field);
|
|
209
212
|
if (Array.isArray(resolvedValue) && resolvedValue.length) {
|
|
210
|
-
return resolvedValue.flatMap(i => [[
|
|
213
|
+
return resolvedValue.flatMap(i => [[field, operatorToken, i], joinOperatorToken]).slice(0, -1);
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
return [
|
|
216
|
+
return [field, operatorToken, resolvedValue];
|
|
214
217
|
}
|
|
215
218
|
|
|
216
219
|
/**
|
|
217
220
|
* Converts a single value, resolving placeholders and handling special cases.
|
|
218
221
|
* @param {*} val - The value to convert.
|
|
222
|
+
* @param {string} parentOperator - The operator of the parent logical node (if any).
|
|
219
223
|
* @returns {*} Converted value.
|
|
220
224
|
*/
|
|
221
|
-
function convertValue(val) {
|
|
225
|
+
function convertValue(val, parentOperator = null) {
|
|
222
226
|
if (val === null) return null;
|
|
223
227
|
|
|
224
228
|
// Handle array values
|
|
@@ -249,6 +253,10 @@ function DevExpressConverter() {
|
|
|
249
253
|
}
|
|
250
254
|
}
|
|
251
255
|
|
|
256
|
+
if (parentOperator && parentOperator.toUpperCase() === "IN" && typeof val === "string") {
|
|
257
|
+
return val.split(',').map(v => v.trim());
|
|
258
|
+
}
|
|
259
|
+
|
|
252
260
|
return val;
|
|
253
261
|
}
|
|
254
262
|
|
|
@@ -332,19 +340,23 @@ function DevExpressConverter() {
|
|
|
332
340
|
/**
|
|
333
341
|
* Checks if a condition is always true.
|
|
334
342
|
* @param {Array} condition - The condition to check.
|
|
343
|
+
* @param {*} leftDefault - The default value for the left operand.
|
|
344
|
+
* @param {*} rightDefault - The default value for the right operand.
|
|
335
345
|
* @returns {boolean} True if the condition is always true.
|
|
336
346
|
*/
|
|
337
|
-
function isAlwaysTrue(condition) {
|
|
338
|
-
return Array.isArray(condition) && condition.length >= 3 && evaluateExpression(...condition) == true;
|
|
347
|
+
function isAlwaysTrue(condition, leftDefault, rightDefault) {
|
|
348
|
+
return Array.isArray(condition) && condition.length >= 3 && evaluateExpression(...condition, leftDefault, rightDefault) == true;
|
|
339
349
|
}
|
|
340
350
|
|
|
341
351
|
/**
|
|
342
352
|
* Checks if a condition is always false.
|
|
343
353
|
* @param {Array} condition - The condition to check.
|
|
354
|
+
* @param {*} leftDefault - The default value for the left operand.
|
|
355
|
+
* @param {*} rightDefault - The default value for the right operand.
|
|
344
356
|
* @returns {boolean} True if the condition is always false.
|
|
345
357
|
*/
|
|
346
|
-
function isAlwaysFalse(condition) {
|
|
347
|
-
return Array.isArray(condition) && condition.length >= 3 && evaluateExpression(...condition) == false;
|
|
358
|
+
function isAlwaysFalse(condition, leftDefault, rightDefault) {
|
|
359
|
+
return Array.isArray(condition) && condition.length >= 3 && evaluateExpression(...condition, leftDefault, rightDefault) == false;
|
|
348
360
|
}
|
|
349
361
|
|
|
350
362
|
/**
|
|
@@ -352,9 +364,14 @@ function DevExpressConverter() {
|
|
|
352
364
|
* @param {*} left - The left operand.
|
|
353
365
|
* @param {string} operator - The operator.
|
|
354
366
|
* @param {*} right - The right operand.
|
|
367
|
+
* @param {*} leftDefault - The default value for the left operand.
|
|
368
|
+
* @param {*} rightDefault - The default value for the right
|
|
355
369
|
* @returns {boolean|null} The result of the evaluation or null if not evaluable.
|
|
356
370
|
*/
|
|
357
|
-
function evaluateExpression(left, operator, right) {
|
|
371
|
+
function evaluateExpression(left, operator, right, leftDefault, rightDefault) {
|
|
372
|
+
if (left == null && leftDefault != undefined) left = leftDefault;
|
|
373
|
+
if (right == null && rightDefault != undefined) right = rightDefault;
|
|
374
|
+
|
|
358
375
|
if ((left !== null && isNaN(left)) || (right !== null && isNaN(right))) return null;
|
|
359
376
|
|
|
360
377
|
if (left === null || right === null) {
|
package/src/core/parser.js
CHANGED
|
@@ -102,15 +102,15 @@ export function parse(input, variables = []) {
|
|
|
102
102
|
const rightOperand = parseValue(); // Parse the value after the operator
|
|
103
103
|
const nodeType = LOGICAL_OPERATORS.includes(operator.toLowerCase()) ? "logical" : "comparison";
|
|
104
104
|
|
|
105
|
-
if(nodeType === "logical") {
|
|
106
|
-
return { type: "logical", operator, left: { type: "function", name: functionName, args: functionArgs }, right: rightOperand
|
|
105
|
+
if (nodeType === "logical") {
|
|
106
|
+
return { type: "logical", operator, left: { type: "function", name: functionName, args: functionArgs }, right: rightOperand };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
return {
|
|
110
110
|
type: "comparison",
|
|
111
111
|
left: { type: "function", name: functionName, args: functionArgs },
|
|
112
112
|
operator,
|
|
113
|
-
value: rightOperand
|
|
113
|
+
value: rightOperand
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -126,9 +126,21 @@ export function parse(input, variables = []) {
|
|
|
126
126
|
const operator = currentToken.value.toUpperCase();
|
|
127
127
|
next(); // Move to the next token
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
if (operator === "IN" || operator === "NOT IN") {
|
|
130
|
+
const rightList = parseValue(operator);
|
|
131
|
+
left = {
|
|
132
|
+
type: "comparison",
|
|
133
|
+
field: left,
|
|
134
|
+
operator: operator,
|
|
135
|
+
value: rightList
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (LOGICAL_OPERATORS.includes(operator.toLowerCase())) {
|
|
140
|
+
// Recursively parse the right-hand expression with adjusted precedence
|
|
141
|
+
const right = parseExpression(OPERATOR_PRECEDENCE[operator]);
|
|
142
|
+
left = { type: "logical", operator, left, right };
|
|
143
|
+
}
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
return left;
|
|
@@ -173,7 +185,7 @@ export function parse(input, variables = []) {
|
|
|
173
185
|
if (currentToken.type === "function") {
|
|
174
186
|
const functionNode = parseFunction();
|
|
175
187
|
|
|
176
|
-
if(fieldType === "identifier" && functionNode.type === "function") {
|
|
188
|
+
if (fieldType === "identifier" && functionNode.type === "function") {
|
|
177
189
|
return {
|
|
178
190
|
type: "comparison",
|
|
179
191
|
field,
|
|
@@ -189,7 +201,7 @@ export function parse(input, variables = []) {
|
|
|
189
201
|
operator,
|
|
190
202
|
value: functionNode.left
|
|
191
203
|
};
|
|
192
|
-
|
|
204
|
+
|
|
193
205
|
functionNode.left = leftComparison;
|
|
194
206
|
return functionNode;
|
|
195
207
|
}
|
|
@@ -237,7 +249,7 @@ export function parse(input, variables = []) {
|
|
|
237
249
|
case "placeholder": {
|
|
238
250
|
const val = token.value.slice(1, -1);
|
|
239
251
|
if (!variables.includes(val)) variables.push(val);
|
|
240
|
-
return {
|
|
252
|
+
return { ...token, type: "placeholder", value: val };
|
|
241
253
|
}
|
|
242
254
|
|
|
243
255
|
case "paren": {
|
package/tests/parser.test.js
CHANGED
|
@@ -210,6 +210,19 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
210
210
|
"and",
|
|
211
211
|
["BranchName", "notcontains", "42"]
|
|
212
212
|
]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
input: "(RS2ID in ({SaleOrderStatusStmtGlobalRpt.StateID}) Or (ISNULL({SaleOrderStatusStmtGlobalRpt.StateID},0) =0)) And (RS3ID in (0,{SaleOrderStatusStmtGlobalRpt.RegionID}) Or ISNULL({SaleOrderStatusStmtGlobalRpt.RegionID},0) =0 )",
|
|
216
|
+
expected: []
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
input: "0 IN ('1,2')",
|
|
220
|
+
expected: [
|
|
221
|
+
[0, "=", "1"],
|
|
222
|
+
"or",
|
|
223
|
+
[0, "=", "2"]
|
|
224
|
+
|
|
225
|
+
]
|
|
213
226
|
}
|
|
214
227
|
];
|
|
215
228
|
|
|
@@ -270,5 +283,7 @@ const sampleData = {
|
|
|
270
283
|
"LeadDocument.CompanyID": 7,
|
|
271
284
|
"ServiceOrderDocument.SourceID": 2,
|
|
272
285
|
"LeadDocument.AllowSubDealer": true,
|
|
273
|
-
"SupportResolution.TicketID": 123
|
|
286
|
+
"SupportResolution.TicketID": 123,
|
|
287
|
+
"SaleOrderStatusStmtGlobalRpt.StateID": null,
|
|
288
|
+
"SaleOrderStatusStmtGlobalRpt.RegionID": null,
|
|
274
289
|
};
|