search-input-query-parser 0.1.0
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/dist/cjs/first-pass-parser.js +77 -0
- package/dist/cjs/lexer.js +322 -0
- package/dist/cjs/parse-in-values.js +65 -0
- package/dist/cjs/parse-primary.js +154 -0
- package/dist/cjs/parse-range-expression.js +174 -0
- package/dist/cjs/parser.js +85 -0
- package/dist/cjs/search-query-to-sql.js +346 -0
- package/dist/cjs/transform-to-expression.js +130 -0
- package/dist/cjs/validate-expression-fields.js +244 -0
- package/dist/cjs/validate-in-expression.js +33 -0
- package/dist/cjs/validate-string.js +65 -0
- package/dist/cjs/validate-wildcard.js +40 -0
- package/dist/cjs/validator.js +34 -0
- package/dist/esm/first-pass-parser.js +73 -0
- package/dist/esm/lexer.js +315 -0
- package/dist/esm/parse-in-values.js +61 -0
- package/dist/esm/parse-primary.js +147 -0
- package/dist/esm/parse-range-expression.js +170 -0
- package/dist/esm/parser.js +81 -0
- package/dist/esm/search-query-to-sql.js +341 -0
- package/dist/esm/transform-to-expression.js +126 -0
- package/dist/esm/validate-expression-fields.js +240 -0
- package/dist/esm/validate-in-expression.js +29 -0
- package/dist/esm/validate-string.js +61 -0
- package/dist/esm/validate-wildcard.js +36 -0
- package/dist/esm/validator.js +30 -0
- package/dist/types/first-pass-parser.d.ts +40 -0
- package/dist/types/lexer.d.ts +27 -0
- package/dist/types/parse-in-values.d.ts +3 -0
- package/dist/types/parse-primary.d.ts +6 -0
- package/dist/types/parse-range-expression.d.ts +2 -0
- package/dist/types/parser.d.ts +68 -0
- package/dist/types/search-query-to-sql.d.ts +18 -0
- package/dist/types/transform-to-expression.d.ts +3 -0
- package/dist/types/validate-expression-fields.d.ts +4 -0
- package/dist/types/validate-in-expression.d.ts +3 -0
- package/dist/types/validate-string.d.ts +3 -0
- package/dist/types/validate-wildcard.d.ts +3 -0
- package/dist/types/validator.d.ts +8 -0
- package/package.json +52 -0
- package/src/first-pass-parser.test.ts +441 -0
- package/src/first-pass-parser.ts +144 -0
- package/src/lexer.test.ts +439 -0
- package/src/lexer.ts +387 -0
- package/src/parse-in-values.ts +74 -0
- package/src/parse-primary.ts +179 -0
- package/src/parse-range-expression.ts +187 -0
- package/src/parser.test.ts +982 -0
- package/src/parser.ts +219 -0
- package/src/search-query-to-sql.test.ts +503 -0
- package/src/search-query-to-sql.ts +506 -0
- package/src/transform-to-expression.ts +153 -0
- package/src/validate-expression-fields.ts +296 -0
- package/src/validate-in-expression.ts +36 -0
- package/src/validate-string.ts +73 -0
- package/src/validate-wildcard.ts +45 -0
- package/src/validator.test.ts +192 -0
- package/src/validator.ts +53 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// Token types and data structures
|
|
2
|
+
export var TokenType;
|
|
3
|
+
(function (TokenType) {
|
|
4
|
+
TokenType["STRING"] = "STRING";
|
|
5
|
+
TokenType["QUOTED_STRING"] = "QUOTED_STRING";
|
|
6
|
+
TokenType["LPAREN"] = "LPAREN";
|
|
7
|
+
TokenType["RPAREN"] = "RPAREN";
|
|
8
|
+
TokenType["AND"] = "AND";
|
|
9
|
+
TokenType["OR"] = "OR";
|
|
10
|
+
TokenType["NOT"] = "NOT";
|
|
11
|
+
TokenType["EOF"] = "EOF";
|
|
12
|
+
TokenType["IN"] = "IN";
|
|
13
|
+
TokenType["COMMA"] = "COMMA";
|
|
14
|
+
TokenType["NUMBER"] = "NUMBER";
|
|
15
|
+
})(TokenType || (TokenType = {}));
|
|
16
|
+
// Tokenizer functions
|
|
17
|
+
export const createStream = (tokens) => ({
|
|
18
|
+
tokens,
|
|
19
|
+
position: 0,
|
|
20
|
+
});
|
|
21
|
+
export const currentToken = (stream) => stream.position < stream.tokens.length
|
|
22
|
+
? stream.tokens[stream.position]
|
|
23
|
+
: { type: TokenType.EOF, value: "", position: stream.position, length: 0 };
|
|
24
|
+
export const advanceStream = (stream) => ({
|
|
25
|
+
...stream,
|
|
26
|
+
position: stream.position + 1,
|
|
27
|
+
});
|
|
28
|
+
const isSpecialChar = (char) => /[\s"():(),]/.test(char);
|
|
29
|
+
const isEscapeChar = (char) => char === "\\";
|
|
30
|
+
const isQuoteChar = (char) => char === '"';
|
|
31
|
+
const isWhitespace = (char) => /\s/.test(char);
|
|
32
|
+
const isWildcard = (char) => char === "*";
|
|
33
|
+
const readUntil = (input, start, predicate) => {
|
|
34
|
+
let result = "";
|
|
35
|
+
let pos = start;
|
|
36
|
+
let foundWildcard = false;
|
|
37
|
+
while (pos < input.length) {
|
|
38
|
+
const char = input[pos];
|
|
39
|
+
// Once we find a wildcard, include everything up to the next whitespace or special char
|
|
40
|
+
if (isWildcard(char)) {
|
|
41
|
+
foundWildcard = true;
|
|
42
|
+
}
|
|
43
|
+
if (isWhitespace(char) || (!foundWildcard && !predicate(char))) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
result += char;
|
|
47
|
+
pos++;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
const tokenizeQuotedString = (input, position) => {
|
|
52
|
+
let value = '"'; // Start with opening quote
|
|
53
|
+
let pos = position + 1; // Skip opening quote in input processing
|
|
54
|
+
let length = 2; // Start with 2 for the quotes
|
|
55
|
+
while (pos < input.length) {
|
|
56
|
+
const char = input[pos];
|
|
57
|
+
if (isQuoteChar(char)) {
|
|
58
|
+
// Add closing quote
|
|
59
|
+
value += '"';
|
|
60
|
+
// Move past closing quote
|
|
61
|
+
pos++;
|
|
62
|
+
// Read any wildcards after the closing quote
|
|
63
|
+
let wildcards = "";
|
|
64
|
+
while (pos < input.length && isWildcard(input[pos])) {
|
|
65
|
+
wildcards += "*";
|
|
66
|
+
pos++;
|
|
67
|
+
length++;
|
|
68
|
+
}
|
|
69
|
+
if (wildcards) {
|
|
70
|
+
value += wildcards;
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
type: TokenType.QUOTED_STRING,
|
|
75
|
+
value,
|
|
76
|
+
position,
|
|
77
|
+
length,
|
|
78
|
+
},
|
|
79
|
+
pos,
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
if (isEscapeChar(char) && pos + 1 < input.length) {
|
|
83
|
+
value += input[pos] + input[pos + 1]; // Include escape char and escaped char
|
|
84
|
+
length += 2;
|
|
85
|
+
pos += 2;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
value += char;
|
|
89
|
+
length++;
|
|
90
|
+
pos++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw { message: "Unterminated quoted string", position, length };
|
|
94
|
+
};
|
|
95
|
+
const tokenizeString = (input, position) => {
|
|
96
|
+
let pos = position;
|
|
97
|
+
if (/^-?\d+(\.\d+)?/.test(input.slice(pos))) {
|
|
98
|
+
const match = input.slice(pos).match(/^-?\d+(\.\d+)?/);
|
|
99
|
+
if (match) {
|
|
100
|
+
const numValue = match[0];
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
type: TokenType.NUMBER,
|
|
104
|
+
value: numValue,
|
|
105
|
+
position: pos,
|
|
106
|
+
length: numValue.length,
|
|
107
|
+
},
|
|
108
|
+
pos + numValue.length,
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Read until we hit a special character, whitespace, or colon
|
|
113
|
+
const fieldPart = readUntil(input, pos, (char) => !isWhitespace(char) && char !== ":" && !isSpecialChar(char));
|
|
114
|
+
pos += fieldPart.length;
|
|
115
|
+
// Check if this is a field:value pattern
|
|
116
|
+
if (pos < input.length && input[pos] === ":") {
|
|
117
|
+
// Skip colon
|
|
118
|
+
pos++;
|
|
119
|
+
// Handle quoted values
|
|
120
|
+
if (pos < input.length && input[pos] === '"') {
|
|
121
|
+
const [quotedToken, newPos] = tokenizeQuotedString(input, pos);
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
type: TokenType.QUOTED_STRING,
|
|
125
|
+
value: `${fieldPart}:${quotedToken.value}`,
|
|
126
|
+
position: position,
|
|
127
|
+
length: newPos - position,
|
|
128
|
+
},
|
|
129
|
+
newPos,
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
// Handle unquoted values
|
|
133
|
+
const valuePart = readUntil(input, pos, (char) => !isWhitespace(char) && !isSpecialChar(char));
|
|
134
|
+
pos += valuePart.length;
|
|
135
|
+
// Check for wildcard after the value
|
|
136
|
+
if (pos < input.length && isWildcard(input[pos])) {
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
type: TokenType.STRING,
|
|
140
|
+
value: `${fieldPart}:${valuePart}*`,
|
|
141
|
+
position,
|
|
142
|
+
length: pos + 1 - position,
|
|
143
|
+
},
|
|
144
|
+
pos + 1,
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
type: TokenType.STRING,
|
|
150
|
+
value: `${fieldPart}:${valuePart}`,
|
|
151
|
+
position,
|
|
152
|
+
length: pos - position,
|
|
153
|
+
},
|
|
154
|
+
pos,
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
// Handle logical operators (case-insensitive)
|
|
158
|
+
const upperFieldPart = fieldPart.toUpperCase();
|
|
159
|
+
if (upperFieldPart === "AND" ||
|
|
160
|
+
upperFieldPart === "OR" ||
|
|
161
|
+
upperFieldPart === "NOT") {
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
type: upperFieldPart === "AND"
|
|
165
|
+
? TokenType.AND
|
|
166
|
+
: upperFieldPart === "OR"
|
|
167
|
+
? TokenType.OR
|
|
168
|
+
: TokenType.NOT,
|
|
169
|
+
value: upperFieldPart,
|
|
170
|
+
position,
|
|
171
|
+
length: fieldPart.length,
|
|
172
|
+
},
|
|
173
|
+
pos,
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
// Handle IN operator (case-insensitive)
|
|
177
|
+
if (upperFieldPart === "IN") {
|
|
178
|
+
return [
|
|
179
|
+
{
|
|
180
|
+
type: TokenType.IN,
|
|
181
|
+
value: "IN",
|
|
182
|
+
position,
|
|
183
|
+
length: fieldPart.length,
|
|
184
|
+
},
|
|
185
|
+
pos,
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
// Read any wildcards after the string
|
|
189
|
+
let wildcards = "";
|
|
190
|
+
while (pos < input.length && isWildcard(input[pos])) {
|
|
191
|
+
wildcards += "*";
|
|
192
|
+
pos++;
|
|
193
|
+
}
|
|
194
|
+
if (wildcards) {
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
type: TokenType.STRING,
|
|
198
|
+
value: fieldPart + wildcards,
|
|
199
|
+
position,
|
|
200
|
+
length: pos - position,
|
|
201
|
+
},
|
|
202
|
+
pos,
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
// Handle plain strings
|
|
206
|
+
return [
|
|
207
|
+
{
|
|
208
|
+
type: TokenType.STRING,
|
|
209
|
+
value: fieldPart,
|
|
210
|
+
position,
|
|
211
|
+
length: fieldPart.length,
|
|
212
|
+
},
|
|
213
|
+
pos,
|
|
214
|
+
];
|
|
215
|
+
};
|
|
216
|
+
export const tokenize = (input) => {
|
|
217
|
+
const tokens = [];
|
|
218
|
+
let position = 0;
|
|
219
|
+
while (position < input.length) {
|
|
220
|
+
const char = input[position];
|
|
221
|
+
if (isWhitespace(char)) {
|
|
222
|
+
position++;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
switch (char) {
|
|
226
|
+
case "-": {
|
|
227
|
+
// Check if this is the start of a term/expression
|
|
228
|
+
if (position === 0 || isWhitespace(input[position - 1])) {
|
|
229
|
+
tokens.push({
|
|
230
|
+
type: TokenType.NOT,
|
|
231
|
+
value: "NOT",
|
|
232
|
+
position,
|
|
233
|
+
length: 1,
|
|
234
|
+
});
|
|
235
|
+
position++;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// If minus is not at start of term, treat it as part of the term
|
|
239
|
+
const [token, newPos] = tokenizeString(input, position);
|
|
240
|
+
tokens.push(token);
|
|
241
|
+
position = newPos;
|
|
242
|
+
}
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case '"': {
|
|
246
|
+
// Before tokenizing a quoted string, check if it's adjacent to a previous quoted string
|
|
247
|
+
if (tokens.length > 0) {
|
|
248
|
+
const prevToken = tokens[tokens.length - 1];
|
|
249
|
+
const prevEnd = prevToken.position + prevToken.length;
|
|
250
|
+
// If there's no whitespace between this quote and the previous token's end
|
|
251
|
+
if (position === prevEnd &&
|
|
252
|
+
prevToken.type !== TokenType.COMMA &&
|
|
253
|
+
(prevToken.type === TokenType.QUOTED_STRING ||
|
|
254
|
+
prevToken.type === TokenType.STRING)) {
|
|
255
|
+
throw {
|
|
256
|
+
message: "Invalid syntax: Missing operator or whitespace between terms",
|
|
257
|
+
position: position,
|
|
258
|
+
length: 1,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const [token, newPos] = tokenizeQuotedString(input, position);
|
|
263
|
+
// After tokenizing, check if the next character is not a whitespace or special character
|
|
264
|
+
if (newPos < input.length &&
|
|
265
|
+
!isWhitespace(input[newPos]) &&
|
|
266
|
+
!isSpecialChar(input[newPos])) {
|
|
267
|
+
throw {
|
|
268
|
+
message: "Invalid syntax: Missing operator or whitespace between terms",
|
|
269
|
+
position: newPos,
|
|
270
|
+
length: 1,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
tokens.push(token);
|
|
274
|
+
position = newPos;
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case "(": {
|
|
278
|
+
tokens.push({
|
|
279
|
+
type: TokenType.LPAREN,
|
|
280
|
+
value: "(",
|
|
281
|
+
position,
|
|
282
|
+
length: 1,
|
|
283
|
+
});
|
|
284
|
+
position++;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case ")": {
|
|
288
|
+
tokens.push({
|
|
289
|
+
type: TokenType.RPAREN,
|
|
290
|
+
value: ")",
|
|
291
|
+
position,
|
|
292
|
+
length: 1,
|
|
293
|
+
});
|
|
294
|
+
position++;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case ",": {
|
|
298
|
+
tokens.push({
|
|
299
|
+
type: TokenType.COMMA,
|
|
300
|
+
value: ",",
|
|
301
|
+
position,
|
|
302
|
+
length: 1,
|
|
303
|
+
});
|
|
304
|
+
position++;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
default: {
|
|
308
|
+
const [token, newPos] = tokenizeString(input, position);
|
|
309
|
+
tokens.push(token);
|
|
310
|
+
position = newPos;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return tokens;
|
|
315
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { currentToken, TokenType, advanceStream } from "./lexer";
|
|
2
|
+
export const parseInValues = (stream, inValuePosition) => {
|
|
3
|
+
const values = [];
|
|
4
|
+
let currentStream = stream;
|
|
5
|
+
// Expect opening parenthesis
|
|
6
|
+
if (currentToken(currentStream).type !== TokenType.LPAREN) {
|
|
7
|
+
throw {
|
|
8
|
+
message: "Expected '(' after IN",
|
|
9
|
+
position: inValuePosition, // Use the position passed from the caller
|
|
10
|
+
length: 1,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
currentStream = advanceStream(currentStream);
|
|
14
|
+
while (true) {
|
|
15
|
+
const token = currentToken(currentStream);
|
|
16
|
+
if (token.type === TokenType.RPAREN) {
|
|
17
|
+
if (values.length === 0) {
|
|
18
|
+
throw {
|
|
19
|
+
message: "IN operator requires at least one value",
|
|
20
|
+
position: token.position,
|
|
21
|
+
length: 1,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
result: values,
|
|
26
|
+
stream: advanceStream(currentStream),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (token.type === TokenType.EOF ||
|
|
30
|
+
(token.type !== TokenType.STRING &&
|
|
31
|
+
token.type !== TokenType.QUOTED_STRING &&
|
|
32
|
+
token.type !== TokenType.NUMBER &&
|
|
33
|
+
token.type !== TokenType.COMMA)) {
|
|
34
|
+
throw {
|
|
35
|
+
message: "Expected ',' or ')' after IN value",
|
|
36
|
+
position: token.position,
|
|
37
|
+
length: 1,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (token.type === TokenType.STRING ||
|
|
41
|
+
token.type === TokenType.QUOTED_STRING ||
|
|
42
|
+
token.type === TokenType.NUMBER) {
|
|
43
|
+
values.push(token.value);
|
|
44
|
+
currentStream = advanceStream(currentStream);
|
|
45
|
+
const nextToken = currentToken(currentStream);
|
|
46
|
+
if (nextToken.type === TokenType.COMMA) {
|
|
47
|
+
currentStream = advanceStream(currentStream);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (nextToken.type === TokenType.RPAREN) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
throw {
|
|
54
|
+
message: "Expected ',' or ')' after IN value",
|
|
55
|
+
position: nextToken.position,
|
|
56
|
+
length: 1,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
currentStream = advanceStream(currentStream);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { parseExpression } from "./first-pass-parser";
|
|
2
|
+
import { parseInValues } from "./parse-in-values";
|
|
3
|
+
import { currentToken, TokenType, advanceStream } from "./lexer";
|
|
4
|
+
export const expectToken = (stream, type, message) => {
|
|
5
|
+
const token = currentToken(stream);
|
|
6
|
+
if (token.type !== type) {
|
|
7
|
+
throw {
|
|
8
|
+
message: message ? message : `Expected ${type}`,
|
|
9
|
+
position: token.position,
|
|
10
|
+
length: token.length,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return advanceStream(stream);
|
|
14
|
+
};
|
|
15
|
+
// Helper to check if a string value represents a field:value pattern
|
|
16
|
+
export const isFieldValuePattern = (value) => {
|
|
17
|
+
return value.includes(":");
|
|
18
|
+
};
|
|
19
|
+
// Helper to extract field and value from a field:value pattern
|
|
20
|
+
export const extractFieldValue = (value) => {
|
|
21
|
+
const [field, ...valueParts] = value.split(":");
|
|
22
|
+
return [field, valueParts.join(":")];
|
|
23
|
+
};
|
|
24
|
+
export const parsePrimary = (stream) => {
|
|
25
|
+
const token = currentToken(stream);
|
|
26
|
+
switch (token.type) {
|
|
27
|
+
case TokenType.NOT: {
|
|
28
|
+
const nextStream = advanceStream(stream);
|
|
29
|
+
const nextToken = currentToken(nextStream);
|
|
30
|
+
if (nextToken.type === TokenType.LPAREN) {
|
|
31
|
+
const afterLParen = advanceStream(nextStream);
|
|
32
|
+
const exprResult = parseExpression(afterLParen);
|
|
33
|
+
const finalStream = expectToken(exprResult.stream, TokenType.RPAREN, "Expected ')'");
|
|
34
|
+
return {
|
|
35
|
+
result: {
|
|
36
|
+
type: "NOT",
|
|
37
|
+
expression: exprResult.result,
|
|
38
|
+
position: token.position,
|
|
39
|
+
length: token.length,
|
|
40
|
+
},
|
|
41
|
+
stream: finalStream,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const exprResult = parsePrimary(nextStream);
|
|
45
|
+
return {
|
|
46
|
+
result: {
|
|
47
|
+
type: "NOT",
|
|
48
|
+
expression: exprResult.result,
|
|
49
|
+
position: token.position,
|
|
50
|
+
length: token.length,
|
|
51
|
+
},
|
|
52
|
+
stream: exprResult.stream,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
case TokenType.LPAREN: {
|
|
56
|
+
const innerStream = advanceStream(stream);
|
|
57
|
+
const exprResult = parseExpression(innerStream);
|
|
58
|
+
const finalStream = expectToken(exprResult.stream, TokenType.RPAREN, "Expected ')'");
|
|
59
|
+
return { result: exprResult.result, stream: finalStream };
|
|
60
|
+
}
|
|
61
|
+
case TokenType.STRING:
|
|
62
|
+
case TokenType.QUOTED_STRING: {
|
|
63
|
+
const { value } = token;
|
|
64
|
+
const isQuoted = token.type === TokenType.QUOTED_STRING;
|
|
65
|
+
// Check for field:IN pattern
|
|
66
|
+
if (value.includes(":")) {
|
|
67
|
+
const [field, remainder] = value.split(":");
|
|
68
|
+
if (remainder.toUpperCase() === "IN") {
|
|
69
|
+
const nextStream = advanceStream(stream);
|
|
70
|
+
const colonIndex = value.indexOf(":");
|
|
71
|
+
const inValuePosition = token.position + colonIndex + 2; // After field:IN
|
|
72
|
+
const inValuesResult = parseInValues(nextStream, inValuePosition);
|
|
73
|
+
return {
|
|
74
|
+
result: {
|
|
75
|
+
type: "IN",
|
|
76
|
+
field,
|
|
77
|
+
values: inValuesResult.result,
|
|
78
|
+
position: token.position,
|
|
79
|
+
length: token.length + inValuesResult.stream.position - nextStream.position,
|
|
80
|
+
},
|
|
81
|
+
stream: inValuesResult.stream,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Handle field:value patterns
|
|
86
|
+
if (isFieldValuePattern(value)) {
|
|
87
|
+
const [field, rawValue] = extractFieldValue(value);
|
|
88
|
+
// If it has a trailing wildcard
|
|
89
|
+
if (rawValue.endsWith("*")) {
|
|
90
|
+
return {
|
|
91
|
+
result: {
|
|
92
|
+
type: "WILDCARD",
|
|
93
|
+
prefix: `${field}:${rawValue.slice(0, -1)}`,
|
|
94
|
+
quoted: isQuoted,
|
|
95
|
+
position: token.position,
|
|
96
|
+
length: token.length,
|
|
97
|
+
},
|
|
98
|
+
stream: advanceStream(stream),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Handle regular terms with wildcards
|
|
103
|
+
if (value.endsWith("*")) {
|
|
104
|
+
return {
|
|
105
|
+
result: {
|
|
106
|
+
type: "WILDCARD",
|
|
107
|
+
prefix: value.slice(0, -1),
|
|
108
|
+
quoted: isQuoted,
|
|
109
|
+
position: token.position,
|
|
110
|
+
length: token.length,
|
|
111
|
+
},
|
|
112
|
+
stream: advanceStream(stream),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Regular string without wildcards
|
|
116
|
+
return {
|
|
117
|
+
result: {
|
|
118
|
+
type: "STRING",
|
|
119
|
+
value,
|
|
120
|
+
quoted: token.type === TokenType.QUOTED_STRING,
|
|
121
|
+
position: token.position,
|
|
122
|
+
length: token.length,
|
|
123
|
+
},
|
|
124
|
+
stream: advanceStream(stream),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
case TokenType.AND:
|
|
128
|
+
case TokenType.OR:
|
|
129
|
+
throw {
|
|
130
|
+
message: `${token.value} is a reserved word`,
|
|
131
|
+
position: token.position,
|
|
132
|
+
length: token.length,
|
|
133
|
+
};
|
|
134
|
+
case TokenType.RPAREN:
|
|
135
|
+
throw {
|
|
136
|
+
message: 'Unexpected ")"',
|
|
137
|
+
position: token.position,
|
|
138
|
+
length: token.length,
|
|
139
|
+
};
|
|
140
|
+
default:
|
|
141
|
+
throw {
|
|
142
|
+
message: "Unexpected token",
|
|
143
|
+
position: token.position,
|
|
144
|
+
length: token.length,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const isRangeOperator = (str) => {
|
|
2
|
+
return [">=", ">", "<=", "<"].includes(str);
|
|
3
|
+
};
|
|
4
|
+
export const parseRangeExpression = (fieldName, value, schema, position, colonIndex) => {
|
|
5
|
+
// Handle ..20 (less than or equal)
|
|
6
|
+
if (value.startsWith("..")) {
|
|
7
|
+
const numValue = value.slice(2);
|
|
8
|
+
return {
|
|
9
|
+
type: "RANGE",
|
|
10
|
+
field: {
|
|
11
|
+
type: "FIELD",
|
|
12
|
+
value: fieldName,
|
|
13
|
+
position,
|
|
14
|
+
length: colonIndex,
|
|
15
|
+
},
|
|
16
|
+
operator: "<=",
|
|
17
|
+
value: {
|
|
18
|
+
type: "VALUE",
|
|
19
|
+
value: numValue,
|
|
20
|
+
position: position + colonIndex + 3, // after colon and ..
|
|
21
|
+
length: numValue.length,
|
|
22
|
+
},
|
|
23
|
+
position,
|
|
24
|
+
length: colonIndex + 1 + value.length,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// Handle 10.. (greater than or equal)
|
|
28
|
+
if (value.endsWith("..")) {
|
|
29
|
+
const numValue = value.slice(0, -2);
|
|
30
|
+
return {
|
|
31
|
+
type: "RANGE",
|
|
32
|
+
field: {
|
|
33
|
+
type: "FIELD",
|
|
34
|
+
value: fieldName,
|
|
35
|
+
position,
|
|
36
|
+
length: colonIndex,
|
|
37
|
+
},
|
|
38
|
+
operator: ">=",
|
|
39
|
+
value: {
|
|
40
|
+
type: "VALUE",
|
|
41
|
+
value: numValue,
|
|
42
|
+
position: position + colonIndex + 1,
|
|
43
|
+
length: numValue.length,
|
|
44
|
+
},
|
|
45
|
+
position,
|
|
46
|
+
length: colonIndex + 1 + value.length,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Handle date ranges with YYYY-MM-DD format
|
|
50
|
+
if ((schema === null || schema === void 0 ? void 0 : schema.type) === "date") {
|
|
51
|
+
const betweenMatch = value.match(/^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/);
|
|
52
|
+
if (betweenMatch) {
|
|
53
|
+
const [_, start, end] = betweenMatch;
|
|
54
|
+
return {
|
|
55
|
+
type: "RANGE",
|
|
56
|
+
field: {
|
|
57
|
+
type: "FIELD",
|
|
58
|
+
value: fieldName,
|
|
59
|
+
position,
|
|
60
|
+
length: colonIndex,
|
|
61
|
+
},
|
|
62
|
+
operator: "BETWEEN",
|
|
63
|
+
value: {
|
|
64
|
+
type: "VALUE",
|
|
65
|
+
value: start,
|
|
66
|
+
position: position + colonIndex + 1,
|
|
67
|
+
length: start.length,
|
|
68
|
+
},
|
|
69
|
+
value2: {
|
|
70
|
+
type: "VALUE",
|
|
71
|
+
value: end,
|
|
72
|
+
position: position + colonIndex + start.length + 3,
|
|
73
|
+
length: end.length,
|
|
74
|
+
},
|
|
75
|
+
position,
|
|
76
|
+
length: colonIndex + 1 + value.length,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Handle 10..20 (between), handling floats and negative numbers
|
|
81
|
+
const betweenMatch = value.match(/^(-?\d*\.?\d+)\.\.(-?\d*\.?\d+)$/);
|
|
82
|
+
if (betweenMatch) {
|
|
83
|
+
const [_, start, end] = betweenMatch;
|
|
84
|
+
return {
|
|
85
|
+
type: "RANGE",
|
|
86
|
+
field: {
|
|
87
|
+
type: "FIELD",
|
|
88
|
+
value: fieldName,
|
|
89
|
+
position,
|
|
90
|
+
length: colonIndex,
|
|
91
|
+
},
|
|
92
|
+
operator: "BETWEEN",
|
|
93
|
+
value: {
|
|
94
|
+
type: "VALUE",
|
|
95
|
+
value: start,
|
|
96
|
+
position: position + colonIndex + 1,
|
|
97
|
+
length: start.length,
|
|
98
|
+
},
|
|
99
|
+
value2: {
|
|
100
|
+
type: "VALUE",
|
|
101
|
+
value: end,
|
|
102
|
+
position: position + colonIndex + start.length + 3,
|
|
103
|
+
length: end.length,
|
|
104
|
+
},
|
|
105
|
+
position,
|
|
106
|
+
length: colonIndex + 1 + value.length,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// Handle >100, >=100, <100, <=100
|
|
110
|
+
if (value.length > 1 && isRangeOperator(value.slice(0, 2))) {
|
|
111
|
+
const operator = value.slice(0, 2);
|
|
112
|
+
const numValue = value.slice(2);
|
|
113
|
+
return {
|
|
114
|
+
type: "RANGE",
|
|
115
|
+
field: {
|
|
116
|
+
type: "FIELD",
|
|
117
|
+
value: fieldName,
|
|
118
|
+
position,
|
|
119
|
+
length: colonIndex,
|
|
120
|
+
},
|
|
121
|
+
operator,
|
|
122
|
+
value: {
|
|
123
|
+
type: "VALUE",
|
|
124
|
+
value: numValue,
|
|
125
|
+
position: position + colonIndex + 3,
|
|
126
|
+
length: numValue.length,
|
|
127
|
+
},
|
|
128
|
+
position,
|
|
129
|
+
length: colonIndex + 1 + value.length,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (value.length > 0 && isRangeOperator(value.slice(0, 1))) {
|
|
133
|
+
const operator = value.slice(0, 1);
|
|
134
|
+
const numValue = value.slice(1);
|
|
135
|
+
return {
|
|
136
|
+
type: "RANGE",
|
|
137
|
+
field: {
|
|
138
|
+
type: "FIELD",
|
|
139
|
+
value: fieldName,
|
|
140
|
+
position,
|
|
141
|
+
length: colonIndex,
|
|
142
|
+
},
|
|
143
|
+
operator,
|
|
144
|
+
value: {
|
|
145
|
+
type: "VALUE",
|
|
146
|
+
value: numValue,
|
|
147
|
+
position: position + colonIndex + 2,
|
|
148
|
+
length: numValue.length,
|
|
149
|
+
},
|
|
150
|
+
position,
|
|
151
|
+
length: colonIndex + 1 + value.length,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// If no range pattern is matched, return a regular field value
|
|
155
|
+
return {
|
|
156
|
+
type: "FIELD_VALUE",
|
|
157
|
+
field: {
|
|
158
|
+
type: "FIELD",
|
|
159
|
+
value: fieldName,
|
|
160
|
+
position,
|
|
161
|
+
length: colonIndex,
|
|
162
|
+
},
|
|
163
|
+
value: {
|
|
164
|
+
type: "VALUE",
|
|
165
|
+
value,
|
|
166
|
+
position: position + colonIndex + 1,
|
|
167
|
+
length: value.length,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
};
|