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.
Files changed (58) hide show
  1. package/dist/cjs/first-pass-parser.js +77 -0
  2. package/dist/cjs/lexer.js +322 -0
  3. package/dist/cjs/parse-in-values.js +65 -0
  4. package/dist/cjs/parse-primary.js +154 -0
  5. package/dist/cjs/parse-range-expression.js +174 -0
  6. package/dist/cjs/parser.js +85 -0
  7. package/dist/cjs/search-query-to-sql.js +346 -0
  8. package/dist/cjs/transform-to-expression.js +130 -0
  9. package/dist/cjs/validate-expression-fields.js +244 -0
  10. package/dist/cjs/validate-in-expression.js +33 -0
  11. package/dist/cjs/validate-string.js +65 -0
  12. package/dist/cjs/validate-wildcard.js +40 -0
  13. package/dist/cjs/validator.js +34 -0
  14. package/dist/esm/first-pass-parser.js +73 -0
  15. package/dist/esm/lexer.js +315 -0
  16. package/dist/esm/parse-in-values.js +61 -0
  17. package/dist/esm/parse-primary.js +147 -0
  18. package/dist/esm/parse-range-expression.js +170 -0
  19. package/dist/esm/parser.js +81 -0
  20. package/dist/esm/search-query-to-sql.js +341 -0
  21. package/dist/esm/transform-to-expression.js +126 -0
  22. package/dist/esm/validate-expression-fields.js +240 -0
  23. package/dist/esm/validate-in-expression.js +29 -0
  24. package/dist/esm/validate-string.js +61 -0
  25. package/dist/esm/validate-wildcard.js +36 -0
  26. package/dist/esm/validator.js +30 -0
  27. package/dist/types/first-pass-parser.d.ts +40 -0
  28. package/dist/types/lexer.d.ts +27 -0
  29. package/dist/types/parse-in-values.d.ts +3 -0
  30. package/dist/types/parse-primary.d.ts +6 -0
  31. package/dist/types/parse-range-expression.d.ts +2 -0
  32. package/dist/types/parser.d.ts +68 -0
  33. package/dist/types/search-query-to-sql.d.ts +18 -0
  34. package/dist/types/transform-to-expression.d.ts +3 -0
  35. package/dist/types/validate-expression-fields.d.ts +4 -0
  36. package/dist/types/validate-in-expression.d.ts +3 -0
  37. package/dist/types/validate-string.d.ts +3 -0
  38. package/dist/types/validate-wildcard.d.ts +3 -0
  39. package/dist/types/validator.d.ts +8 -0
  40. package/package.json +52 -0
  41. package/src/first-pass-parser.test.ts +441 -0
  42. package/src/first-pass-parser.ts +144 -0
  43. package/src/lexer.test.ts +439 -0
  44. package/src/lexer.ts +387 -0
  45. package/src/parse-in-values.ts +74 -0
  46. package/src/parse-primary.ts +179 -0
  47. package/src/parse-range-expression.ts +187 -0
  48. package/src/parser.test.ts +982 -0
  49. package/src/parser.ts +219 -0
  50. package/src/search-query-to-sql.test.ts +503 -0
  51. package/src/search-query-to-sql.ts +506 -0
  52. package/src/transform-to-expression.ts +153 -0
  53. package/src/validate-expression-fields.ts +296 -0
  54. package/src/validate-in-expression.ts +36 -0
  55. package/src/validate-string.ts +73 -0
  56. package/src/validate-wildcard.ts +45 -0
  57. package/src/validator.test.ts +192 -0
  58. 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
+ };