sqlparser-devexpress 2.1.1 → 2.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlparser-devexpress",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -136,13 +136,21 @@ function DevExpressConverter() {
136
136
  }
137
137
 
138
138
  // Handle "IN" condition, including comma-separated values
139
- if (operator === "IN") {
140
- return handleInOperator(ast);
139
+ if (operator === "IN" || operator === "NOT IN") {
140
+ return handleInOperator(ast, operator);
141
141
  }
142
142
 
143
143
  const left = ast.left !== undefined ? processAstNode(ast.left) : convertValue(ast.field);
144
144
  const right = ast.right !== undefined ? processAstNode(ast.right) : convertValue(ast.value);
145
- const comparison = [left, ast.operator.toLowerCase(), right];
145
+ const operatorToken = ast.operator.toLowerCase();
146
+
147
+ let comparison = [left, operatorToken, right];
148
+
149
+ if (isFunctionNullCheck(ast.left, true)) {
150
+ comparison = [[left, operatorToken, right], 'or', [left, operatorToken, null]];
151
+ } else if (isFunctionNullCheck(ast.right, true)) {
152
+ comparison = [[left, operatorToken, right], 'or', [right, operatorToken, null]];
153
+ }
146
154
 
147
155
  // Apply short-circuit evaluation if enabled
148
156
  if (EnableShortCircuit) {
@@ -179,7 +187,7 @@ function DevExpressConverter() {
179
187
  * @param {Object} ast - The comparison operator AST node.
180
188
  * @returns {Array} DevExpress format filter.
181
189
  */
182
- function handleInOperator(ast) {
190
+ function handleInOperator(ast, operator) {
183
191
  let resolvedValue = convertValue(ast.value);
184
192
 
185
193
  // Handle comma-separated values in a string
@@ -190,15 +198,18 @@ function DevExpressConverter() {
190
198
  } else {
191
199
  resolvedValue = firstValue;
192
200
  }
201
+ } else if (typeof resolvedValue === 'string' && resolvedValue.includes(',')) {
202
+ resolvedValue = resolvedValue.split(',').map(v => v.trim());
193
203
  }
194
204
 
195
- if (Array.isArray(resolvedValue) && resolvedValue.length) {
196
-
197
- return resolvedValue.flatMap(i => [[ast.field, '=', i], 'or']).slice(0, -1);
205
+ let operatorToken = operator === "IN" ? '=' : operator === "NOT IN" ? '!=' : operator;
206
+ let joinOperatorToken = operator === "IN" ? 'or' : operator === "NOT IN" ? 'and' : operator;
198
207
 
208
+ if (Array.isArray(resolvedValue) && resolvedValue.length) {
209
+ return resolvedValue.flatMap(i => [[ast.field, operatorToken, i], joinOperatorToken]).slice(0, -1);
199
210
  }
200
211
 
201
- return [ast.field, "=", resolvedValue];
212
+ return [ast.field, operatorToken, resolvedValue];
202
213
  }
203
214
 
204
215
  /**
@@ -221,7 +232,7 @@ function DevExpressConverter() {
221
232
  }
222
233
 
223
234
  // Special handling for ISNULL function
224
- if (val.type === "function" && val.name === "ISNULL" && val.args?.length >= 2) {
235
+ if (isFunctionNullCheck(val)) {
225
236
  return convertValue(val.args[0]);
226
237
  }
227
238
 
@@ -256,6 +267,18 @@ function DevExpressConverter() {
256
267
  return node?.type === "function" && node.name === "ISNULL" && valueNode?.type === "value";
257
268
  }
258
269
 
270
+ /**
271
+ * Checks if a node is a ISNULL function without value
272
+ * @param {Object} node
273
+ * @returns {boolean} True if this is an ISNULL check.
274
+ */
275
+ function isFunctionNullCheck(node, isPlaceholderCheck = false) {
276
+ const isValidFunction = node?.type === "function" && node?.name === "ISNULL" && node?.args?.length >= 2;
277
+
278
+ return isPlaceholderCheck ? isValidFunction && node?.args[0]?.value?.type !== "placeholder" : isValidFunction;
279
+ }
280
+
281
+
259
282
  /**
260
283
  * Determines whether the logical tree should be flattened.
261
284
  * This is based on the parent operator and the current operator.
@@ -208,8 +208,10 @@ export function parse(input, variables = []) {
208
208
  return { type: "placeholder", value: val };
209
209
  }
210
210
 
211
+ operatorToken = operatorToken.toUpperCase();
212
+
211
213
  // Handle IN operator which requires a list of values
212
- if (operatorToken && operatorToken.toUpperCase() === "IN") return parseInList(token);
214
+ if (operatorToken && (operatorToken === "IN" || operatorToken === "NOT IN")) return parseInList(token);
213
215
 
214
216
  throw new Error(`Unexpected value: ${token.value}`);
215
217
  }
@@ -6,7 +6,7 @@ const tokenPatterns = {
6
6
  number: "\\d+", // Matches numerical values
7
7
  placeholder: "'?\\{[^}]+\\}'?", // Matches placeholders like {variable} or '{variable}'
8
8
  string: "'(?:''|[^'])*'", // Matches strings, allowing for escaped single quotes ('')
9
- operator: "=>|<=|!=|>=|=|<>|>|<|\\bAND\\b|\\bOR\\b|\\bBETWEEN\\b|\\bIN\\b|\\bLIKE\\b|\\bIS NOT\\b|\\bNOT LIKE\\b|\\bIS\\b", // Matches SQL operators and logical keywords
9
+ 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
10
10
  identifier: "[\\w.]+", // Matches identifiers, including table.column format
11
11
  paren: "[()]", // Matches parentheses
12
12
  comma: "," // Matches commas
@@ -49,14 +49,14 @@ class Tokenizer {
49
49
 
50
50
  if (type === "operator") {
51
51
  const lowerValue = value.toLowerCase();
52
-
52
+
53
53
  if (lowerValue === "is") {
54
54
  value = "=";
55
55
  } else if (lowerValue === "is not") {
56
56
  value = "!=";
57
57
  }
58
58
  }
59
-
59
+
60
60
  return { type, value };
61
61
  }
62
62
 
@@ -66,7 +66,7 @@ class Tokenizer {
66
66
 
67
67
  peekNextToken() {
68
68
  if (this.index >= this.input.length) return null;
69
-
69
+
70
70
  const savedIndex = this.index; // Save current index
71
71
  try {
72
72
  return this.nextToken(); // Get next token
@@ -78,7 +78,7 @@ class Tokenizer {
78
78
  reset() {
79
79
  this.index = 0; // Reset index to the beginning of the input
80
80
  }
81
-
81
+
82
82
  }
83
83
 
84
84
  export { Tokenizer };
package/src/index.js CHANGED
@@ -5,12 +5,12 @@ import { sanitizeQuery } from "./core/sanitizer.js";
5
5
 
6
6
  export function convertSQLToAst(filterString, enableConsoleLogs = false) {
7
7
  let { sanitizedSQL, extractedVariables } = sanitizeQuery(filterString);
8
- enableConsoleLogs && console.log("Sanitized SQL:", sanitizedSQL, "\n");
8
+ !!enableConsoleLogs && console.log("Sanitized SQL:", sanitizedSQL, "\n");
9
9
 
10
10
  const parsedResult = parse(sanitizedSQL, extractedVariables);
11
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");
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
14
 
15
15
  return parsedResult;
16
16
  }
@@ -37,7 +37,7 @@ describe("Parser SQL to dx Filter Builder", () => {
37
37
  {
38
38
  input: "ID IN ({WorkOrderLine.ApplicableUoms}) AND (CompanyID = {WorkOrderDocument.CompanyID} OR {WorkOrderDocument.CompanyID} = 0)",
39
39
  expected: [
40
- [["ID", "=", "UOM1"] , 'or', ["ID", "=", "UOM2"], 'or', ["ID", "=", "UOM3"]],
40
+ [["ID", "=", "UOM1"], 'or', ["ID", "=", "UOM2"], 'or', ["ID", "=", "UOM3"]],
41
41
  "and",
42
42
  // [
43
43
  ["CompanyID", "=", 42],
@@ -145,12 +145,44 @@ describe("Parser SQL to dx Filter Builder", () => {
145
145
  expected: [
146
146
  ["SourceID", "=", 2],
147
147
  "or",
148
- ["SourceID", "=", 0]
148
+ ["SourceID", "=", null],
149
+ "or",
150
+ ["SourceID", "=", 0],
151
+ "or",
152
+ ["SourceID", "=", null]
149
153
  ]
150
154
  },
151
155
  {
152
156
  input: "CompanyID = CompanyID2 = {AccountingRule.CompanyID}",
153
157
  expected: "Error: Invalid comparison: CompanyID = CompanyID2",
158
+ },
159
+ {
160
+ input: "(CompanyID = {LeadDocument.CompanyID} OR ISNULL(CompanyID,0) = 0) AND (ISNULL(IsSubdealer,0) = {LeadDocument.AllowSubDealer})",
161
+ expected: [
162
+ [
163
+ ["CompanyID", "=", 7],
164
+ "or",
165
+ [
166
+ ["CompanyID", "=", 0],
167
+ "or",
168
+ ["CompanyID", "=", null]
169
+ ]
170
+ ],
171
+ "and",
172
+ [
173
+ ["IsSubdealer", "=", true],
174
+ "or",
175
+ ["IsSubdealer", "=", null]
176
+ ]
177
+ ]
178
+ },
179
+ {
180
+ input: 'AddressType NOT IN (2, 4)',
181
+ expected: [
182
+ ["AddressType", "!=", 2],
183
+ "and",
184
+ ["AddressType", "!=", 4]
185
+ ]
154
186
  }
155
187
  ];
156
188
 
@@ -165,7 +197,7 @@ describe("Parser SQL to dx Filter Builder", () => {
165
197
  try {
166
198
  astwithVariables = convertSQLToAst(input);
167
199
  } catch (error) {
168
- expect(error.message).toEqual(expected.replace("Error: ",""));
200
+ expect(error.message).toEqual(expected.replace("Error: ", ""));
169
201
  return;
170
202
  }
171
203
 
@@ -186,7 +218,7 @@ describe("Parser SQL to dx Filter Builder", () => {
186
218
 
187
219
  expect(result).toEqual(expected);
188
220
  });
189
- });
221
+ });
190
222
  });
191
223
 
192
224
 
@@ -213,5 +245,6 @@ const sampleData = {
213
245
  "TransferOutwardDocument.CompanyID": 7,
214
246
  "LeadDocument.BranchID": 42,
215
247
  "LeadDocument.CompanyID": 7,
216
- "ServiceOrderDocument.SourceID": 2
248
+ "ServiceOrderDocument.SourceID": 2,
249
+ "LeadDocument.AllowSubDealer": true
217
250
  };