sqlparser-devexpress 2.3.0 → 2.3.2
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/core/converter.js +1 -1
- package/src/core/parser.js +10 -1
- package/src/core/tokenizer.js +5 -5
- package/src/debug.js +2 -2
- package/tests/error.test.js +66 -0
- package/tests/parser.test.js +17 -22
package/package.json
CHANGED
package/src/core/converter.js
CHANGED
|
@@ -248,7 +248,7 @@ function DevExpressConverter() {
|
|
|
248
248
|
if (!resultObject) return `{${placeholder}}`;
|
|
249
249
|
|
|
250
250
|
|
|
251
|
-
return resultObject.hasOwnProperty(placeholder) ? resultObject[placeholder] : `{${placeholder}}`;
|
|
251
|
+
return resultObject.hasOwnProperty(placeholder) ? resultObject[placeholder] : `{${placeholder.value ?? placeholder}}`;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/**
|
package/src/core/parser.js
CHANGED
|
@@ -184,6 +184,8 @@ export function parse(input, variables = []) {
|
|
|
184
184
|
function parseValue(operatorToken) {
|
|
185
185
|
if (!currentToken) throw new Error("Unexpected end of input");
|
|
186
186
|
|
|
187
|
+
if(currentToken.type === "function") return parseFunction();
|
|
188
|
+
|
|
187
189
|
const token = currentToken;
|
|
188
190
|
next(); // Move to the next token
|
|
189
191
|
|
|
@@ -199,11 +201,18 @@ export function parse(input, variables = []) {
|
|
|
199
201
|
return { type: "placeholder", value: val };
|
|
200
202
|
}
|
|
201
203
|
|
|
202
|
-
operatorToken = operatorToken
|
|
204
|
+
operatorToken = operatorToken?.toUpperCase();
|
|
203
205
|
|
|
204
206
|
// Handle IN operator which requires a list of values
|
|
205
207
|
if (operatorToken && (operatorToken === "IN" || operatorToken === "NOT IN")) return parseInList(token);
|
|
206
208
|
|
|
209
|
+
// Handle ({Placeholder}) syntax for placeholders inside parentheses
|
|
210
|
+
const nextToken = tokenizer.peekNextToken();
|
|
211
|
+
if(token.type === "paren" && currentToken && currentToken.type === "placeholder" && nextToken && nextToken.type === "paren") {
|
|
212
|
+
const val = parseValue();
|
|
213
|
+
return { type: "placeholder", value: val };
|
|
214
|
+
}
|
|
215
|
+
|
|
207
216
|
throw new Error(`Unexpected value: ${token.value}`);
|
|
208
217
|
}
|
|
209
218
|
|
package/src/core/tokenizer.js
CHANGED
|
@@ -6,7 +6,7 @@ const tokenPatterns = {
|
|
|
6
6
|
function: "\\b(ISNULL)\\b", // Matches function names like ISNULL (case-insensitive)
|
|
7
7
|
null: "\\bNULL\\b|\\(\\s*NULL\\s*\\)", // Matches NULL as a keyword
|
|
8
8
|
number: "\\(\\d+\\)|\\d+", // Matches numbers while stripping unnecessary parentheses
|
|
9
|
-
placeholder: "'?\\{[^}]+\\}'?", // Matches placeholders like {variable} or '{variable}'
|
|
9
|
+
placeholder: "\\('?\\{[^}]+\\}'?\\)|'?\\{[^}]+\\}'?", // Matches placeholders like {variable} or '{variable}' or ({variable}) or ('{variable}')
|
|
10
10
|
string: "\\('\\w+\\'\\)|'(?:''|[^'])*'", // Matches strings, allowing for escaped single quotes ('')
|
|
11
11
|
operator: "=>|<=|!=|>=|=|<>|>|<|\\bAND\\b|\\bOR\\b|\\bBETWEEN\\b|\\bIN\\b|\\bNOT IN\\b|\\bLIKE\\b|\\bIS NOT\\b|\\bNOT LIKE\\b|\\bIS\\b", // Matches SQL operators and logical keywords
|
|
12
12
|
identifier: "[\\w.]+|\"[^\"]+\"|\\[[^\\]]+\\]", // Matches regular identifiers, quoted identifiers ("identifier"), and bracketed identifiers [identifier]
|
|
@@ -47,7 +47,7 @@ class Tokenizer {
|
|
|
47
47
|
let value = match.groups[type];
|
|
48
48
|
|
|
49
49
|
// Remove surrounding single quotes from placeholders
|
|
50
|
-
if (type === "placeholder") value = value.replace(/^['"]
|
|
50
|
+
if (type === "placeholder") value = value.replace(/^[\s'"\(\)]+|[\s'"\(\)]+|[\s]+/g, "");
|
|
51
51
|
|
|
52
52
|
if (type === "operator") {
|
|
53
53
|
const lowerValue = value.toLowerCase();
|
|
@@ -59,15 +59,15 @@ class Tokenizer {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
if (LITERALS.includes(type)){
|
|
62
|
+
if (LITERALS.includes(type)) {
|
|
63
63
|
value = value.replace(/^[(]|[)]$/g, "");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
if (type === "identifier") {
|
|
67
67
|
value = value.replace(/^["\[]|["\]]$/g, "");
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
|
|
70
|
+
|
|
71
71
|
return { type, value };
|
|
72
72
|
}
|
|
73
73
|
|
package/src/debug.js
CHANGED
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
// const astTree = parsedResult.ast;
|
|
26
26
|
// console.log("AST Tree:", JSON.stringify(astTree, null, 2), "\n");
|
|
27
27
|
|
|
28
|
-
// return convertToDevExpressFormat({ ast: astTree,
|
|
28
|
+
// return convertToDevExpressFormat({ ast: astTree, resultObject: sampleData });
|
|
29
29
|
// }
|
|
30
30
|
|
|
31
|
-
// const devexpress = parseFilterString("
|
|
31
|
+
// const devexpress = parseFilterString("(ISNULL(TicketID, 0) = ISNULL({CustomerOrders.OrderID}, 0))", sampleData);
|
|
32
32
|
// console.log("DevExpress Filter:", JSON.stringify(devexpress, null, 2));
|
|
33
33
|
// // const devexpress = parseFilterString("(RS2ID in ({LeadStatementGlobalRpt.StateID}) Or ({LeadStatementGlobalRpt.StateID} =0)) And (RS3ID in (0,{LeadStatementGlobalRpt.RegionID}) Or {LeadStatementGlobalRpt.RegionID} =0 )", sampleData);
|
|
34
34
|
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
import { convertAstToDevextreme, convertSQLToAst } from "../src";
|
|
6
|
+
|
|
7
|
+
describe("Parser SQL to dx Filter Builder", () => {
|
|
8
|
+
const testCases = [
|
|
9
|
+
{
|
|
10
|
+
input: "NULL",
|
|
11
|
+
expected: []
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
input: "SELECT DISTINCT O.OrderID AS ID, O.CustomerName, O.OrderType, O.CustomerName AS [Online Order], O.OrderDate AS OrderDate, D.DeliveryStatus, O.OrderDate AS [Online Order Date], O.CompanyID, CAST(CAST(O.OrderDate AS DATE) AS VARCHAR(10)) AS DocumentDate FROM Orders O INNER JOIN Payment P ON P.OrderID = O.OrderID INNER JOIN Shipment S ON S.PaymentID = P.PaymentID INNER JOIN Delivery D ON D.ShipmentID = S.ShipmentID ",
|
|
15
|
+
expect: null
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
input: "CompanyID = CompanyID2 = {AccountingRule.CompanyID}",
|
|
19
|
+
expected: "Error: Invalid comparison: CompanyID = CompanyID2",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
input: "( CompanyID = {AccountingRule.CompanyID}",
|
|
23
|
+
expected: "Error: Missing closing parenthesis"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
testCases.forEach(({ input, expected }, index) => {
|
|
29
|
+
it(`Test Case ${index + 1}: ${input}`, () => {
|
|
30
|
+
|
|
31
|
+
if (expected == undefined) {
|
|
32
|
+
expected = null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let astwithVariables;
|
|
36
|
+
try {
|
|
37
|
+
astwithVariables = convertSQLToAst(input);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
expect(error.message).toEqual(expected.replace("Error: ", ""));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (astwithVariables == null) {
|
|
44
|
+
expect(null).toEqual(expected);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const variables = astwithVariables.variables;
|
|
49
|
+
const ast = astwithVariables.ast;
|
|
50
|
+
|
|
51
|
+
const result = convertAstToDevextreme(ast, sampleData);
|
|
52
|
+
|
|
53
|
+
if (result == null || result == true || result == false) {
|
|
54
|
+
expect([]).toEqual(expected);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expect(result).toEqual(expected);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
const sampleData = {
|
|
65
|
+
"AccountingRule.CompanyID": 42,
|
|
66
|
+
};
|
package/tests/parser.test.js
CHANGED
|
@@ -102,10 +102,6 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
102
102
|
["ItemGroupType", "=", ""]
|
|
103
103
|
]
|
|
104
104
|
},
|
|
105
|
-
{
|
|
106
|
-
input: "NULL",
|
|
107
|
-
expected: []
|
|
108
|
-
},
|
|
109
105
|
{
|
|
110
106
|
input: "((ISNULL({0}, 0) = 0 AND CompanyID = {1}) OR CompanyID IS NULL) OR BranchID = {0} | [LeadDocument.BranchID] | [LeadDocument.CompanyID]",
|
|
111
107
|
expected: [
|
|
@@ -122,10 +118,6 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
122
118
|
["BranchID", "=", 42]
|
|
123
119
|
]
|
|
124
120
|
},
|
|
125
|
-
{
|
|
126
|
-
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",
|
|
127
|
-
expect: null
|
|
128
|
-
},
|
|
129
121
|
{
|
|
130
122
|
input: "FromDate Between '10-10-2021' AND '10-10-2022'",
|
|
131
123
|
expected: [
|
|
@@ -152,10 +144,6 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
152
144
|
["SourceID", "=", null]
|
|
153
145
|
]
|
|
154
146
|
},
|
|
155
|
-
{
|
|
156
|
-
input: "CompanyID = CompanyID2 = {AccountingRule.CompanyID}",
|
|
157
|
-
expected: "Error: Invalid comparison: CompanyID = CompanyID2",
|
|
158
|
-
},
|
|
159
147
|
{
|
|
160
148
|
input: "(CompanyID = {LeadDocument.CompanyID} OR ISNULL(CompanyID,0) = 0) AND (ISNULL(IsSubdealer,0) = {LeadDocument.AllowSubDealer})",
|
|
161
149
|
expected: [
|
|
@@ -185,11 +173,21 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
185
173
|
]
|
|
186
174
|
},
|
|
187
175
|
{
|
|
188
|
-
input: "AddressType IN ('2', ('4'))",
|
|
176
|
+
input: "AddressType IN ('2', ('4')) OR AddressType =({ServiceOrderDocument .SourceID})",
|
|
189
177
|
expected: [
|
|
190
178
|
["AddressType", "=", '2'],
|
|
191
179
|
"or",
|
|
192
|
-
["AddressType", "=", '4']
|
|
180
|
+
["AddressType", "=", '4'],
|
|
181
|
+
"or",
|
|
182
|
+
["AddressType", "=", 2]
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
input: "(ISNULL(TicketID, 0) = ISNULL({SupportResolution.TicketID}, 0))",
|
|
187
|
+
expected: [
|
|
188
|
+
["TicketID", "=", 123],
|
|
189
|
+
"or",
|
|
190
|
+
["TicketID", "=", null]
|
|
193
191
|
]
|
|
194
192
|
}
|
|
195
193
|
];
|
|
@@ -202,12 +200,8 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
202
200
|
}
|
|
203
201
|
|
|
204
202
|
let astwithVariables;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
} catch (error) {
|
|
208
|
-
expect(error.message).toEqual(expected.replace("Error: ", ""));
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
203
|
+
astwithVariables = convertSQLToAst(input);
|
|
204
|
+
|
|
211
205
|
|
|
212
206
|
if (astwithVariables == null) {
|
|
213
207
|
expect(null).toEqual(expected);
|
|
@@ -217,7 +211,7 @@ describe("Parser SQL to dx Filter Builder", () => {
|
|
|
217
211
|
const variables = astwithVariables.variables;
|
|
218
212
|
const ast = astwithVariables.ast;
|
|
219
213
|
|
|
220
|
-
const result = convertAstToDevextreme(ast,
|
|
214
|
+
const result = convertAstToDevextreme(ast, sampleData);
|
|
221
215
|
|
|
222
216
|
if (result == null || result == true || result == false) {
|
|
223
217
|
expect([]).toEqual(expected);
|
|
@@ -254,5 +248,6 @@ const sampleData = {
|
|
|
254
248
|
"LeadDocument.BranchID": 42,
|
|
255
249
|
"LeadDocument.CompanyID": 7,
|
|
256
250
|
"ServiceOrderDocument.SourceID": 2,
|
|
257
|
-
"LeadDocument.AllowSubDealer": true
|
|
251
|
+
"LeadDocument.AllowSubDealer": true,
|
|
252
|
+
"SupportResolution.TicketID": 123
|
|
258
253
|
};
|