sqlparser-devexpress 2.3.10 → 2.3.12
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 -5
- package/src/core/parser.js +21 -9
- package/tests/parser.test.js +8 -0
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
|
|
|
@@ -205,22 +206,41 @@ function DevExpressConverter() {
|
|
|
205
206
|
resolvedValue = resolvedValue.split(',').map(v => v.trim());
|
|
206
207
|
}
|
|
207
208
|
|
|
209
|
+
// handle short circuit evaluation for IN operator
|
|
210
|
+
if (EnableShortCircuit && ast.field?.type === "value" && ast.value?.type === "value") {
|
|
211
|
+
const fieldVal = convertValue(ast.field);
|
|
212
|
+
if (Array.isArray(resolvedValue)) {
|
|
213
|
+
// normalize numeric strings if LHS is number
|
|
214
|
+
const list = resolvedValue.map(x =>
|
|
215
|
+
(typeof x === "string" && !isNaN(x) && typeof fieldVal === "number")
|
|
216
|
+
? Number(x)
|
|
217
|
+
: x
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (operator === "IN")
|
|
221
|
+
return list.includes(fieldVal);
|
|
222
|
+
else if (operator === "NOT IN")
|
|
223
|
+
return !list.includes(fieldVal);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
208
227
|
let operatorToken = operator === "IN" ? '=' : operator === "NOT IN" ? '!=' : operator;
|
|
209
228
|
let joinOperatorToken = operator === "IN" ? 'or' : operator === "NOT IN" ? 'and' : operator;
|
|
210
|
-
|
|
229
|
+
let field = convertValue(ast.field);
|
|
211
230
|
if (Array.isArray(resolvedValue) && resolvedValue.length) {
|
|
212
|
-
return resolvedValue.flatMap(i => [[
|
|
231
|
+
return resolvedValue.flatMap(i => [[field, operatorToken, i], joinOperatorToken]).slice(0, -1);
|
|
213
232
|
}
|
|
214
233
|
|
|
215
|
-
return [
|
|
234
|
+
return [field, operatorToken, resolvedValue];
|
|
216
235
|
}
|
|
217
236
|
|
|
218
237
|
/**
|
|
219
238
|
* Converts a single value, resolving placeholders and handling special cases.
|
|
220
239
|
* @param {*} val - The value to convert.
|
|
240
|
+
* @param {string} parentOperator - The operator of the parent logical node (if any).
|
|
221
241
|
* @returns {*} Converted value.
|
|
222
242
|
*/
|
|
223
|
-
function convertValue(val) {
|
|
243
|
+
function convertValue(val, parentOperator = null) {
|
|
224
244
|
if (val === null) return null;
|
|
225
245
|
|
|
226
246
|
// Handle array values
|
|
@@ -251,6 +271,10 @@ function DevExpressConverter() {
|
|
|
251
271
|
}
|
|
252
272
|
}
|
|
253
273
|
|
|
274
|
+
if (parentOperator && parentOperator.toUpperCase() === "IN" && typeof val === "string") {
|
|
275
|
+
return val.split(',').map(v => v.trim());
|
|
276
|
+
}
|
|
277
|
+
|
|
254
278
|
return val;
|
|
255
279
|
}
|
|
256
280
|
|
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
|
@@ -214,6 +214,14 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
214
214
|
{
|
|
215
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
216
|
expected: []
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
input: "ID IN ('1,2') AND 0 IN ('0,2')",
|
|
220
|
+
expected: [
|
|
221
|
+
["ID", "=", "1"],
|
|
222
|
+
"or",
|
|
223
|
+
["ID", "=", "2"]
|
|
224
|
+
]
|
|
217
225
|
}
|
|
218
226
|
];
|
|
219
227
|
|