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,982 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
parseSearchInputQuery,
|
|
5
|
+
type SearchQuery,
|
|
6
|
+
type SearchQueryError,
|
|
7
|
+
stringify,
|
|
8
|
+
ValidationError,
|
|
9
|
+
FieldSchema,
|
|
10
|
+
} from "./parser";
|
|
11
|
+
|
|
12
|
+
describe("Search Query Parser", () => {
|
|
13
|
+
const testValidQuery = (input: string, expected: string) => {
|
|
14
|
+
const result = parseSearchInputQuery(input);
|
|
15
|
+
expect(result.type).toBe("SEARCH_QUERY");
|
|
16
|
+
const query = result as SearchQuery;
|
|
17
|
+
expect(query.expression).toBeTruthy();
|
|
18
|
+
if (query.expression) {
|
|
19
|
+
expect(stringify(query.expression)).toBe(expected);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const testErrorQuery = (input: string, expectedError: ValidationError[]) => {
|
|
24
|
+
const result = parseSearchInputQuery(input);
|
|
25
|
+
expect(result.type).toBe("SEARCH_QUERY_ERROR");
|
|
26
|
+
const error = result as SearchQueryError;
|
|
27
|
+
expect(error.errors).toStrictEqual(expectedError);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const schemas: FieldSchema[] = [
|
|
31
|
+
{ name: "price", type: "number" },
|
|
32
|
+
{ name: "amount", type: "number" },
|
|
33
|
+
{ name: "date", type: "date" },
|
|
34
|
+
{ name: "title", type: "string" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const testSchemaQuery = (input: string, expected: string) => {
|
|
38
|
+
const result = parseSearchInputQuery(input, schemas);
|
|
39
|
+
expect(result.type).toBe("SEARCH_QUERY");
|
|
40
|
+
const query = result as SearchQuery;
|
|
41
|
+
expect(query.expression).toBeTruthy();
|
|
42
|
+
if (query.expression) {
|
|
43
|
+
expect(stringify(query.expression)).toBe(expected);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const testSchemaErrorQuery = (
|
|
48
|
+
input: string,
|
|
49
|
+
expectedError: ValidationError[]
|
|
50
|
+
) => {
|
|
51
|
+
const result = parseSearchInputQuery(input, schemas);
|
|
52
|
+
expect(result.type).toBe("SEARCH_QUERY_ERROR");
|
|
53
|
+
const error = result as SearchQueryError;
|
|
54
|
+
expect(error.errors).toStrictEqual(expectedError);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe("Basic Term Parsing", () => {
|
|
58
|
+
test("parses single terms", () => {
|
|
59
|
+
testValidQuery("boots", "boots");
|
|
60
|
+
testValidQuery('"red shoes"', '"red shoes"');
|
|
61
|
+
testValidQuery("simple-term", "simple-term");
|
|
62
|
+
testValidQuery("term_with_underscore", "term_with_underscore");
|
|
63
|
+
testValidQuery("term123", "term123");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("parses multiple terms with implicit AND", () => {
|
|
67
|
+
testValidQuery("boots summer", "(boots AND summer)");
|
|
68
|
+
testValidQuery("red boots black", "((red AND boots) AND black)");
|
|
69
|
+
testValidQuery("a b c d", "(((a AND b) AND c) AND d)");
|
|
70
|
+
testValidQuery(
|
|
71
|
+
'"quoted term" normal_term',
|
|
72
|
+
'("quoted term" AND normal_term)'
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("handles terms with special characters", () => {
|
|
77
|
+
testValidQuery('"term with-dash"', '"term with-dash"');
|
|
78
|
+
testValidQuery('"term with_underscore"', '"term with_underscore"');
|
|
79
|
+
testValidQuery('"term with.period"', '"term with.period"');
|
|
80
|
+
testValidQuery('"term with@symbol"', '"term with@symbol"');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("Field Value Parsing", () => {
|
|
85
|
+
test("parses simple field:value pairs", () => {
|
|
86
|
+
testValidQuery("color:red", "color:red");
|
|
87
|
+
testValidQuery("size:large", "size:large");
|
|
88
|
+
testValidQuery("price:100", "price:100");
|
|
89
|
+
testValidQuery("date:2024-01-01", "date:2024-01-01");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("parses field values with spaces", () => {
|
|
93
|
+
testValidQuery('status:"pending review"', "status:pending review");
|
|
94
|
+
testValidQuery('category:"winter boots"', "category:winter boots");
|
|
95
|
+
testValidQuery(
|
|
96
|
+
'description:"very long product description"',
|
|
97
|
+
"description:very long product description"
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("handles various field:value spacing", () => {
|
|
102
|
+
testErrorQuery("field: value", [
|
|
103
|
+
{
|
|
104
|
+
length: 6,
|
|
105
|
+
message: "Expected field value",
|
|
106
|
+
position: 0,
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
// testValidQuery("field :value", "field:value");
|
|
110
|
+
testErrorQuery("field: value", [
|
|
111
|
+
{
|
|
112
|
+
length: 6,
|
|
113
|
+
message: "Expected field value",
|
|
114
|
+
position: 0,
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
// testValidQuery("field : value", "field:value");
|
|
118
|
+
testErrorQuery("field : value", [
|
|
119
|
+
{
|
|
120
|
+
length: 1,
|
|
121
|
+
message: "Expected field value",
|
|
122
|
+
position: 6,
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
testValidQuery('field:"quoted value"', "field:quoted value");
|
|
126
|
+
// testValidQuery('field: "quoted value"', "field:quoted value");
|
|
127
|
+
testErrorQuery('field: "quoted value"', [
|
|
128
|
+
{
|
|
129
|
+
length: 6,
|
|
130
|
+
message: "Expected field value",
|
|
131
|
+
position: 0,
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
// testValidQuery('field :"quoted value"', "field:quoted value");
|
|
135
|
+
testErrorQuery('field :"quoted value"', [
|
|
136
|
+
{
|
|
137
|
+
length: 15,
|
|
138
|
+
message: "Missing field name",
|
|
139
|
+
position: 6,
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("handles special characters in field values", () => {
|
|
145
|
+
testValidQuery('brand:"Nike\\Air"', "brand:Nike\\Air");
|
|
146
|
+
testValidQuery('brand:"Nike\\"Air"', 'brand:Nike\\\"Air');
|
|
147
|
+
testValidQuery('path:"C:\\\\Program Files"', "path:C:\\\\Program Files");
|
|
148
|
+
testValidQuery(
|
|
149
|
+
'query:"SELECT * FROM table"',
|
|
150
|
+
"query:SELECT * FROM table"
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("handles fields with numbers and special characters", () => {
|
|
155
|
+
testValidQuery("field2:value", "field2:value");
|
|
156
|
+
testValidQuery("field_name:value", "field_name:value");
|
|
157
|
+
testValidQuery("field-name:value", "field-name:value");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("handles reserved words as identifiers", () => {
|
|
161
|
+
testValidQuery("field:AND", "field:AND");
|
|
162
|
+
testValidQuery("field:OR", "field:OR");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("Logical Operators", () => {
|
|
167
|
+
test("parses AND operator", () => {
|
|
168
|
+
testValidQuery("comfortable AND leather", "(comfortable AND leather)");
|
|
169
|
+
testValidQuery("color:red AND size:large", "(color:red AND size:large)");
|
|
170
|
+
testValidQuery("a AND b AND c", "((a AND b) AND c)");
|
|
171
|
+
testValidQuery(
|
|
172
|
+
'"term one" AND "term two"',
|
|
173
|
+
'("term one" AND "term two")'
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("parses OR operator", () => {
|
|
178
|
+
testValidQuery("leather OR suede", "(leather OR suede)");
|
|
179
|
+
testValidQuery(
|
|
180
|
+
"color:black OR color:brown",
|
|
181
|
+
"(color:black OR color:brown)"
|
|
182
|
+
);
|
|
183
|
+
testValidQuery("a OR b OR c", "((a OR b) OR c)");
|
|
184
|
+
testValidQuery('"term one" OR "term two"', '("term one" OR "term two")');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("handles operator precedence correctly", () => {
|
|
188
|
+
testValidQuery("a AND b OR c", "((a AND b) OR c)");
|
|
189
|
+
testValidQuery("a OR b AND c", "(a OR (b AND c))");
|
|
190
|
+
testValidQuery("a OR b OR c AND d", "((a OR b) OR (c AND d))");
|
|
191
|
+
testValidQuery("a AND b AND c OR d", "(((a AND b) AND c) OR d)");
|
|
192
|
+
testValidQuery("a OR b AND c AND d", "(a OR ((b AND c) AND d))");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("handles mixed implicit and explicit operators", () => {
|
|
196
|
+
testValidQuery("a b OR c", "((a AND b) OR c)");
|
|
197
|
+
testValidQuery("a OR b c", "(a OR (b AND c))");
|
|
198
|
+
testValidQuery("a b c OR d", "(((a AND b) AND c) OR d)");
|
|
199
|
+
testValidQuery("a OR b c d", "(a OR ((b AND c) AND d))");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("NOT Expression Support", () => {
|
|
204
|
+
test("parses simple NOT expressions", () => {
|
|
205
|
+
testValidQuery("NOT test", "NOT (test)");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("parses NOT with parentheses", () => {
|
|
209
|
+
testValidQuery("NOT (test)", "NOT (test)");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("parses NOT with field:value", () => {
|
|
213
|
+
testValidQuery("NOT status:active", "NOT (status:active)");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("parses NOT with quoted strings", () => {
|
|
217
|
+
testValidQuery('NOT "red shoes"', 'NOT ("red shoes")');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("parses complex expressions with NOT", () => {
|
|
221
|
+
testValidQuery("boots AND NOT leather", "(boots AND NOT (leather))");
|
|
222
|
+
testValidQuery("NOT (leather OR suede)", "NOT ((leather OR suede))");
|
|
223
|
+
testValidQuery(
|
|
224
|
+
"category:boots AND NOT (color:brown OR color:black)",
|
|
225
|
+
"(category:boots AND NOT ((color:brown OR color:black)))"
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("Negative Term Support", () => {
|
|
231
|
+
test("parses simple negative terms", () => {
|
|
232
|
+
testValidQuery("-test", "NOT (test)");
|
|
233
|
+
testValidQuery('-"red shoes"', 'NOT ("red shoes")');
|
|
234
|
+
testValidQuery("-status:active", "NOT (status:active)");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("parses multiple negative terms", () => {
|
|
238
|
+
testValidQuery("-red -blue", "(NOT (red) AND NOT (blue))");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("parses mixed positive and negative terms", () => {
|
|
242
|
+
testValidQuery("boots -leather", "(boots AND NOT (leather))");
|
|
243
|
+
testValidQuery(
|
|
244
|
+
"category:shoes -color:brown",
|
|
245
|
+
"(category:shoes AND NOT (color:brown))"
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("parses negative terms with parentheses", () => {
|
|
250
|
+
testValidQuery("-(red OR blue)", "NOT ((red OR blue))");
|
|
251
|
+
testValidQuery(
|
|
252
|
+
"shoes -(color:red OR color:blue)",
|
|
253
|
+
"(shoes AND NOT ((color:red OR color:blue)))"
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("parses complex expressions with negative terms", () => {
|
|
258
|
+
testValidQuery(
|
|
259
|
+
"boots -color:brown -(brand:nike OR brand:adidas)",
|
|
260
|
+
"((boots AND NOT (color:brown)) AND NOT ((brand:nike OR brand:adidas)))"
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("Parentheses Grouping", () => {
|
|
266
|
+
test("parses simple parenthesized expressions", () => {
|
|
267
|
+
testValidQuery(
|
|
268
|
+
"(winter OR summer) AND boots",
|
|
269
|
+
"((winter OR summer) AND boots)"
|
|
270
|
+
);
|
|
271
|
+
testValidQuery("(leather OR suede)", "(leather OR suede)");
|
|
272
|
+
testValidQuery("((a OR b))", "(a OR b)");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("parses nested parentheses", () => {
|
|
276
|
+
testValidQuery(
|
|
277
|
+
'"red shoes" OR ((blue OR purple) AND sneakers)',
|
|
278
|
+
'("red shoes" OR ((blue OR purple) AND sneakers))'
|
|
279
|
+
);
|
|
280
|
+
testValidQuery("((a AND b) OR c) AND d", "(((a AND b) OR c) AND d)");
|
|
281
|
+
testValidQuery("(a AND (b OR (c AND d)))", "(a AND (b OR (c AND d)))");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("handles complex parentheses combinations", () => {
|
|
285
|
+
testValidQuery(
|
|
286
|
+
"(a AND b) OR (c AND d) OR (e AND f)",
|
|
287
|
+
"(((a AND b) OR (c AND d)) OR (e AND f))"
|
|
288
|
+
);
|
|
289
|
+
testValidQuery(
|
|
290
|
+
"((a OR b) AND (c OR d)) OR ((e OR f) AND (g OR h))",
|
|
291
|
+
"(((a OR b) AND (c OR d)) OR ((e OR f) AND (g OR h)))"
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("handles other parentheses", () => {
|
|
296
|
+
testValidQuery("((test))", "test");
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("Complex Queries", () => {
|
|
301
|
+
test("parses complex field and term combinations", () => {
|
|
302
|
+
testValidQuery(
|
|
303
|
+
'category:"winter boots" AND (color:black OR color:brown)',
|
|
304
|
+
"(category:winter boots AND (color:black OR color:brown))"
|
|
305
|
+
);
|
|
306
|
+
testValidQuery(
|
|
307
|
+
"size:large color:red status:available",
|
|
308
|
+
"((size:large AND color:red) AND status:available)"
|
|
309
|
+
);
|
|
310
|
+
testValidQuery(
|
|
311
|
+
'type:"running shoes" AND (color:red OR color:blue) AND size:42 AND in_stock:true',
|
|
312
|
+
"(((type:running shoes AND (color:red OR color:blue)) AND size:42) AND in_stock:true)"
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("handles mixed quoted and unquoted values", () => {
|
|
317
|
+
testValidQuery(
|
|
318
|
+
'category:shoes AND brand:"Nike Air" OR category:boots AND brand:Timberland',
|
|
319
|
+
"((category:shoes AND brand:Nike Air) OR (category:boots AND brand:Timberland))"
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("handles deeply nested expressions", () => {
|
|
324
|
+
testValidQuery(
|
|
325
|
+
'(category:"winter gear" AND (type:boots OR type:shoes)) OR (category:"summer gear" AND (type:sandals OR type:slippers)) AND in_stock:true',
|
|
326
|
+
"((category:winter gear AND (type:boots OR type:shoes)) OR ((category:summer gear AND (type:sandals OR type:slippers)) AND in_stock:true))"
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe("Range Query Support", () => {
|
|
332
|
+
test("parses comparison operators", () => {
|
|
333
|
+
testSchemaQuery("price:>100", "price:>100");
|
|
334
|
+
testSchemaQuery("price:>=100", "price:>=100");
|
|
335
|
+
testSchemaQuery("price:<50", "price:<50");
|
|
336
|
+
testSchemaQuery("price:<=50", "price:<=50");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("parses between ranges", () => {
|
|
340
|
+
testSchemaQuery("price:10..20", "price:10..20");
|
|
341
|
+
testSchemaQuery("amount:50.99..100.50", "amount:50.99..100.50");
|
|
342
|
+
testSchemaQuery(
|
|
343
|
+
"date:2024-01-01..2024-12-31",
|
|
344
|
+
"date:2024-01-01..2024-12-31"
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("parses open-ended ranges", () => {
|
|
349
|
+
testSchemaQuery("price:10..", "price:>=10");
|
|
350
|
+
testSchemaQuery("price:..20", "price:<=20");
|
|
351
|
+
testSchemaQuery("date:2024-01-01..", "date:>=2024-01-01");
|
|
352
|
+
testSchemaQuery("date:..2024-12-31", "date:<=2024-12-31");
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("complex expressions with ranges", () => {
|
|
356
|
+
testSchemaQuery(
|
|
357
|
+
"price:>100 AND amount:<50",
|
|
358
|
+
"(price:>100 AND amount:<50)"
|
|
359
|
+
);
|
|
360
|
+
testSchemaQuery(
|
|
361
|
+
"price:10..20 OR amount:>=100",
|
|
362
|
+
"(price:10..20 OR amount:>=100)"
|
|
363
|
+
);
|
|
364
|
+
testSchemaQuery(
|
|
365
|
+
"(price:>100 AND amount:<50) OR date:>=2024-01-01",
|
|
366
|
+
"((price:>100 AND amount:<50) OR date:>=2024-01-01)"
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("mixes ranges with regular fields", () => {
|
|
371
|
+
testSchemaQuery(
|
|
372
|
+
"title:shoes AND price:10..20",
|
|
373
|
+
"(title:shoes AND price:10..20)"
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("only applies range parsing to numeric and date fields", () => {
|
|
378
|
+
const result = parseSearchInputQuery("title:>100", schemas);
|
|
379
|
+
expect(result.type).toBe("SEARCH_QUERY");
|
|
380
|
+
const query = result as SearchQuery;
|
|
381
|
+
expect(stringify(query.expression!)).toBe("title:>100");
|
|
382
|
+
expect(query.expression!.type).toBe("FIELD_VALUE");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("parses decimal numbers in ranges", () => {
|
|
386
|
+
testSchemaQuery("price:10.5..20.99", "price:10.5..20.99");
|
|
387
|
+
testSchemaQuery("amount:50.99..100.50", "amount:50.99..100.50");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("parses negative numbers in ranges", () => {
|
|
391
|
+
testSchemaQuery("price:-10..10", "price:-10..10");
|
|
392
|
+
testSchemaQuery("amount:-50.99..-10.50", "amount:-50.99..-10.50");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("parses date format ranges", () => {
|
|
396
|
+
testSchemaQuery(
|
|
397
|
+
"date:2024-01-01..2024-12-31",
|
|
398
|
+
"date:2024-01-01..2024-12-31"
|
|
399
|
+
);
|
|
400
|
+
testSchemaQuery(
|
|
401
|
+
"date:2023-12-31..2024-01-01",
|
|
402
|
+
"date:2023-12-31..2024-01-01"
|
|
403
|
+
);
|
|
404
|
+
});
|
|
405
|
+
describe("Wildcard Pattern Support", () => {
|
|
406
|
+
test("parses simple wildcard patterns", () => {
|
|
407
|
+
testValidQuery("test*", "test*");
|
|
408
|
+
testValidQuery('"test*"', '"test*"');
|
|
409
|
+
testValidQuery("field:test*", "field:test*");
|
|
410
|
+
testValidQuery('field:"test*"', "field:test*");
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("parses wildcards in quoted strings", () => {
|
|
414
|
+
testValidQuery('"test * test"', '"test * test"');
|
|
415
|
+
testValidQuery('"test * test"*', '"test * test"*');
|
|
416
|
+
testValidQuery('field:"test * test"', "field:test * test");
|
|
417
|
+
testValidQuery('field:"test * test"*', 'field:"test * test"*');
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe("Error Cases", () => {
|
|
423
|
+
test("handles empty input", () => {
|
|
424
|
+
const result = parseSearchInputQuery("");
|
|
425
|
+
expect(result.type).toBe("SEARCH_QUERY");
|
|
426
|
+
expect((result as SearchQuery).expression).toBeNull();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("handles whitespace-only input", () => {
|
|
430
|
+
const result = parseSearchInputQuery(" \t\n ");
|
|
431
|
+
expect(result.type).toBe("SEARCH_QUERY");
|
|
432
|
+
expect((result as SearchQuery).expression).toBeNull();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("handles invalid field syntax", () => {
|
|
436
|
+
testErrorQuery("field:", [
|
|
437
|
+
{
|
|
438
|
+
length: 6,
|
|
439
|
+
message: "Expected field value",
|
|
440
|
+
position: 0,
|
|
441
|
+
},
|
|
442
|
+
]);
|
|
443
|
+
testErrorQuery(":value", [
|
|
444
|
+
{
|
|
445
|
+
length: 6,
|
|
446
|
+
message: "Missing field name",
|
|
447
|
+
position: 0,
|
|
448
|
+
},
|
|
449
|
+
]);
|
|
450
|
+
testErrorQuery(":", [
|
|
451
|
+
{
|
|
452
|
+
length: 1,
|
|
453
|
+
message: "Expected field value",
|
|
454
|
+
position: 0,
|
|
455
|
+
},
|
|
456
|
+
]);
|
|
457
|
+
testErrorQuery("field::", [
|
|
458
|
+
{
|
|
459
|
+
length: 6,
|
|
460
|
+
message: "Expected field value",
|
|
461
|
+
position: 0,
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
length: 1,
|
|
465
|
+
message: "Expected field value",
|
|
466
|
+
position: 6,
|
|
467
|
+
},
|
|
468
|
+
]);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("handles reserved words as identifiers", () => {
|
|
472
|
+
testErrorQuery("AND:value", [
|
|
473
|
+
{
|
|
474
|
+
length: 3,
|
|
475
|
+
message: "AND is a reserved word",
|
|
476
|
+
position: 0,
|
|
477
|
+
},
|
|
478
|
+
]);
|
|
479
|
+
testErrorQuery("OR:test", [
|
|
480
|
+
{
|
|
481
|
+
length: 2,
|
|
482
|
+
message: "OR is a reserved word",
|
|
483
|
+
position: 0,
|
|
484
|
+
},
|
|
485
|
+
]);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test("handles malformed parentheses", () => {
|
|
489
|
+
testErrorQuery("()", [
|
|
490
|
+
{
|
|
491
|
+
length: 1,
|
|
492
|
+
message: 'Unexpected ")"',
|
|
493
|
+
position: 1,
|
|
494
|
+
},
|
|
495
|
+
]);
|
|
496
|
+
testErrorQuery("(test))", [
|
|
497
|
+
{
|
|
498
|
+
length: 1,
|
|
499
|
+
message: 'Unexpected ")"',
|
|
500
|
+
position: 6,
|
|
501
|
+
},
|
|
502
|
+
]);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("handles unterminated quoted strings", () => {
|
|
506
|
+
testErrorQuery('brand:"Nike', [
|
|
507
|
+
{
|
|
508
|
+
length: 6,
|
|
509
|
+
message: "Unterminated quoted string",
|
|
510
|
+
position: 6,
|
|
511
|
+
},
|
|
512
|
+
]);
|
|
513
|
+
testErrorQuery('brand:"Nike\\', [
|
|
514
|
+
{
|
|
515
|
+
length: 7,
|
|
516
|
+
message: "Unterminated quoted string",
|
|
517
|
+
position: 6,
|
|
518
|
+
},
|
|
519
|
+
]);
|
|
520
|
+
testErrorQuery('brand:"Nike\\"', [
|
|
521
|
+
{
|
|
522
|
+
length: 8,
|
|
523
|
+
message: "Unterminated quoted string",
|
|
524
|
+
position: 6,
|
|
525
|
+
},
|
|
526
|
+
]);
|
|
527
|
+
testErrorQuery('"unclosed quote', [
|
|
528
|
+
{
|
|
529
|
+
length: 16,
|
|
530
|
+
message: "Unterminated quoted string",
|
|
531
|
+
position: 0,
|
|
532
|
+
},
|
|
533
|
+
]);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("handles invalid operator usage", () => {
|
|
537
|
+
testErrorQuery("AND term", [
|
|
538
|
+
{
|
|
539
|
+
length: 3,
|
|
540
|
+
message: "AND is a reserved word",
|
|
541
|
+
position: 0,
|
|
542
|
+
},
|
|
543
|
+
]);
|
|
544
|
+
testErrorQuery("OR term", [
|
|
545
|
+
{
|
|
546
|
+
length: 2,
|
|
547
|
+
message: "OR is a reserved word",
|
|
548
|
+
position: 0,
|
|
549
|
+
},
|
|
550
|
+
]);
|
|
551
|
+
testErrorQuery("term AND", [
|
|
552
|
+
{
|
|
553
|
+
length: 3,
|
|
554
|
+
message: "Unexpected token: AND",
|
|
555
|
+
position: 5,
|
|
556
|
+
},
|
|
557
|
+
]);
|
|
558
|
+
testErrorQuery("term OR", [
|
|
559
|
+
{
|
|
560
|
+
length: 2,
|
|
561
|
+
message: "Unexpected token: OR",
|
|
562
|
+
position: 5,
|
|
563
|
+
},
|
|
564
|
+
]);
|
|
565
|
+
testErrorQuery("AND AND", [
|
|
566
|
+
{
|
|
567
|
+
length: 3,
|
|
568
|
+
message: "AND is a reserved word",
|
|
569
|
+
position: 0,
|
|
570
|
+
},
|
|
571
|
+
]);
|
|
572
|
+
testErrorQuery("OR OR", [
|
|
573
|
+
{
|
|
574
|
+
length: 2,
|
|
575
|
+
message: "OR is a reserved word",
|
|
576
|
+
position: 0,
|
|
577
|
+
},
|
|
578
|
+
]);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("handle multiple errors", () => {
|
|
582
|
+
testErrorQuery(
|
|
583
|
+
'category:"winter boots" AND (value: OR color:) AND size:',
|
|
584
|
+
[
|
|
585
|
+
{
|
|
586
|
+
length: 6,
|
|
587
|
+
message: "Expected field value",
|
|
588
|
+
position: 29,
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
length: 6,
|
|
592
|
+
message: "Expected field value",
|
|
593
|
+
position: 39,
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
length: 5,
|
|
597
|
+
message: "Expected field value",
|
|
598
|
+
position: 51,
|
|
599
|
+
},
|
|
600
|
+
]
|
|
601
|
+
);
|
|
602
|
+
testErrorQuery(
|
|
603
|
+
'category:"winter boots" AND (value: OR color:) AND size: AND AND',
|
|
604
|
+
[
|
|
605
|
+
{
|
|
606
|
+
length: 3,
|
|
607
|
+
message: "AND is a reserved word",
|
|
608
|
+
position: 61,
|
|
609
|
+
},
|
|
610
|
+
]
|
|
611
|
+
);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test("handles malformed ranges", () => {
|
|
615
|
+
testSchemaErrorQuery("amount:>", [
|
|
616
|
+
{
|
|
617
|
+
message: "Expected range value",
|
|
618
|
+
position: 8,
|
|
619
|
+
length: 0,
|
|
620
|
+
},
|
|
621
|
+
]);
|
|
622
|
+
|
|
623
|
+
testSchemaErrorQuery("price:>=>100", [
|
|
624
|
+
{
|
|
625
|
+
message: "Invalid range operator",
|
|
626
|
+
position: 6,
|
|
627
|
+
length: 3,
|
|
628
|
+
},
|
|
629
|
+
]);
|
|
630
|
+
|
|
631
|
+
testSchemaErrorQuery("price:>..", [
|
|
632
|
+
{
|
|
633
|
+
message: "Invalid numeric value",
|
|
634
|
+
position: 6,
|
|
635
|
+
length: 1,
|
|
636
|
+
},
|
|
637
|
+
]);
|
|
638
|
+
|
|
639
|
+
testSchemaErrorQuery("price:...", [
|
|
640
|
+
{
|
|
641
|
+
message: "Invalid range format",
|
|
642
|
+
position: 6,
|
|
643
|
+
length: 3,
|
|
644
|
+
},
|
|
645
|
+
]);
|
|
646
|
+
|
|
647
|
+
testSchemaErrorQuery("price:100...", [
|
|
648
|
+
{
|
|
649
|
+
message: "Invalid range format",
|
|
650
|
+
position: 6,
|
|
651
|
+
length: 6,
|
|
652
|
+
},
|
|
653
|
+
]);
|
|
654
|
+
|
|
655
|
+
testSchemaErrorQuery("price:...200", [
|
|
656
|
+
{
|
|
657
|
+
message: "Invalid range format",
|
|
658
|
+
position: 6,
|
|
659
|
+
length: 6,
|
|
660
|
+
},
|
|
661
|
+
]);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test("handles invalid numeric ranges", () => {
|
|
665
|
+
testSchemaErrorQuery("price:abc..def", [
|
|
666
|
+
{
|
|
667
|
+
message: "Invalid numeric value",
|
|
668
|
+
position: 6,
|
|
669
|
+
length: 3,
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
message: "Invalid numeric value",
|
|
673
|
+
position: 11,
|
|
674
|
+
length: 3,
|
|
675
|
+
},
|
|
676
|
+
]);
|
|
677
|
+
|
|
678
|
+
testSchemaErrorQuery("amount:>abc", [
|
|
679
|
+
{
|
|
680
|
+
message: "Invalid numeric value",
|
|
681
|
+
position: 8,
|
|
682
|
+
length: 3,
|
|
683
|
+
},
|
|
684
|
+
]);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("handles invalid date ranges", () => {
|
|
688
|
+
testSchemaErrorQuery("date:>not-a-date", [
|
|
689
|
+
{
|
|
690
|
+
message: "Invalid date format",
|
|
691
|
+
position: 5,
|
|
692
|
+
length: 11,
|
|
693
|
+
},
|
|
694
|
+
]);
|
|
695
|
+
|
|
696
|
+
testSchemaErrorQuery("date:2024-13-01..2024-12-31", [
|
|
697
|
+
{
|
|
698
|
+
message: "Invalid date format",
|
|
699
|
+
position: 5,
|
|
700
|
+
length: 22,
|
|
701
|
+
},
|
|
702
|
+
]);
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
describe("Wildcard Error Cases", () => {
|
|
707
|
+
test("rejects multiple wildcards in unquoted strings", () => {
|
|
708
|
+
testErrorQuery("test*test", [
|
|
709
|
+
{
|
|
710
|
+
message: "Wildcard (*) can only appear at the end of a term",
|
|
711
|
+
position: 4,
|
|
712
|
+
length: 1,
|
|
713
|
+
},
|
|
714
|
+
]);
|
|
715
|
+
|
|
716
|
+
testErrorQuery("te*st*", [
|
|
717
|
+
{
|
|
718
|
+
message: "Wildcard (*) can only appear at the end of a term",
|
|
719
|
+
position: 2,
|
|
720
|
+
length: 1,
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
message: "Only one trailing wildcard (*) is allowed",
|
|
724
|
+
position: 5,
|
|
725
|
+
length: 1,
|
|
726
|
+
},
|
|
727
|
+
]);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test("rejects multiple trailing wildcards", () => {
|
|
731
|
+
testErrorQuery("test**", [
|
|
732
|
+
{
|
|
733
|
+
message: "Only one trailing wildcard (*) is allowed",
|
|
734
|
+
position: 5,
|
|
735
|
+
length: 1,
|
|
736
|
+
},
|
|
737
|
+
]);
|
|
738
|
+
|
|
739
|
+
testErrorQuery('"test"**', [
|
|
740
|
+
{
|
|
741
|
+
message: "Only one trailing wildcard (*) is allowed",
|
|
742
|
+
position: 7,
|
|
743
|
+
length: 1,
|
|
744
|
+
},
|
|
745
|
+
]);
|
|
746
|
+
|
|
747
|
+
testErrorQuery('field:"test"**', [
|
|
748
|
+
{
|
|
749
|
+
message: "Only one trailing wildcard (*) is allowed",
|
|
750
|
+
position: 13,
|
|
751
|
+
length: 1,
|
|
752
|
+
},
|
|
753
|
+
]);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
test("rejects wildcards in field names", () => {
|
|
757
|
+
testErrorQuery("fie*ld:value", [
|
|
758
|
+
{
|
|
759
|
+
message: "Invalid characters in field name",
|
|
760
|
+
position: 0,
|
|
761
|
+
length: 6,
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
message: "Wildcard (*) can only appear at the end of a term",
|
|
765
|
+
position: 3,
|
|
766
|
+
length: 1,
|
|
767
|
+
},
|
|
768
|
+
]);
|
|
769
|
+
|
|
770
|
+
testErrorQuery("field*:value", [
|
|
771
|
+
{
|
|
772
|
+
message: "Invalid characters in field name",
|
|
773
|
+
position: 0,
|
|
774
|
+
length: 6,
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
message: "Wildcard (*) can only appear at the end of a term",
|
|
778
|
+
position: 5,
|
|
779
|
+
length: 1,
|
|
780
|
+
},
|
|
781
|
+
]);
|
|
782
|
+
|
|
783
|
+
testErrorQuery("f*:value", [
|
|
784
|
+
{
|
|
785
|
+
message: "Invalid characters in field name",
|
|
786
|
+
position: 0,
|
|
787
|
+
length: 2,
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
message: "Wildcard (*) can only appear at the end of a term",
|
|
791
|
+
position: 1,
|
|
792
|
+
length: 1,
|
|
793
|
+
},
|
|
794
|
+
]);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test("complex wildcard error cases", () => {
|
|
798
|
+
testErrorQuery('field*:"test"', [
|
|
799
|
+
{
|
|
800
|
+
message: "Invalid characters in field name",
|
|
801
|
+
position: 0,
|
|
802
|
+
length: 6,
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
message: "Wildcard (*) can only appear at the end of a term",
|
|
806
|
+
position: 5,
|
|
807
|
+
length: 1,
|
|
808
|
+
},
|
|
809
|
+
]);
|
|
810
|
+
|
|
811
|
+
testErrorQuery('field:"test * test"**', [
|
|
812
|
+
{
|
|
813
|
+
message: "Only one trailing wildcard (*) is allowed",
|
|
814
|
+
position: 20,
|
|
815
|
+
length: 1,
|
|
816
|
+
},
|
|
817
|
+
]);
|
|
818
|
+
|
|
819
|
+
testErrorQuery("test* field*:value", [
|
|
820
|
+
{
|
|
821
|
+
message: "Invalid characters in field name",
|
|
822
|
+
position: 6,
|
|
823
|
+
length: 6,
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
message: "Wildcard (*) can only appear at the end of a term",
|
|
827
|
+
position: 11,
|
|
828
|
+
length: 1,
|
|
829
|
+
},
|
|
830
|
+
]);
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// In parser.test.ts
|
|
835
|
+
|
|
836
|
+
describe("IN Query Support", () => {
|
|
837
|
+
test("parses simple IN queries", () => {
|
|
838
|
+
testValidQuery("status:IN(active,pending)", "status:IN(active,pending)");
|
|
839
|
+
testValidQuery("type:IN(a,b,c)", "type:IN(a,b,c)");
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
test("parses IN queries with quoted values", () => {
|
|
843
|
+
testValidQuery(
|
|
844
|
+
'category:IN("winter boots","summer shoes")',
|
|
845
|
+
'category:IN("winter boots","summer shoes")'
|
|
846
|
+
);
|
|
847
|
+
testValidQuery(
|
|
848
|
+
'status:IN(active,"in progress",completed)',
|
|
849
|
+
'status:IN(active,"in progress",completed)'
|
|
850
|
+
);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
test("parses IN queries with special characters", () => {
|
|
854
|
+
testValidQuery(
|
|
855
|
+
'tags:IN("high-priority","low-priority")',
|
|
856
|
+
'tags:IN("high-priority","low-priority")'
|
|
857
|
+
);
|
|
858
|
+
testValidQuery(
|
|
859
|
+
"category:IN(mens_shoes,womens_shoes)",
|
|
860
|
+
"category:IN(mens_shoes,womens_shoes)"
|
|
861
|
+
);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test("handles IN queries with schema validation", () => {
|
|
865
|
+
const schemas: FieldSchema[] = [
|
|
866
|
+
{ name: "price", type: "number" },
|
|
867
|
+
{ name: "date", type: "date" },
|
|
868
|
+
{ name: "status", type: "string" },
|
|
869
|
+
];
|
|
870
|
+
|
|
871
|
+
// Valid number IN
|
|
872
|
+
const numResult = parseSearchInputQuery("price:IN(10,20,30)", schemas);
|
|
873
|
+
expect(numResult.type).toBe("SEARCH_QUERY");
|
|
874
|
+
expect((numResult as SearchQuery).expression).toBeTruthy();
|
|
875
|
+
|
|
876
|
+
// // Valid date IN
|
|
877
|
+
// const dateResult = parseSearchInputQuery(
|
|
878
|
+
// "date:IN(2024-01-01,2024-02-01)",
|
|
879
|
+
// schemas
|
|
880
|
+
// );
|
|
881
|
+
// expect(dateResult.type).toBe("SEARCH_QUERY");
|
|
882
|
+
// expect((dateResult as SearchQuery).expression).toBeTruthy();
|
|
883
|
+
|
|
884
|
+
// Invalid number IN
|
|
885
|
+
const invalidNumResult = parseSearchInputQuery(
|
|
886
|
+
"price:IN(abc,def)",
|
|
887
|
+
schemas
|
|
888
|
+
);
|
|
889
|
+
expect(invalidNumResult.type).toBe("SEARCH_QUERY_ERROR");
|
|
890
|
+
|
|
891
|
+
// should be fully equal to [{"length": 3, "message": "Invalid numeric value", "position": 9}, {"length": 3, "message": "Invalid numeric value", "position": 13}]
|
|
892
|
+
expect((invalidNumResult as SearchQueryError).errors).toStrictEqual([
|
|
893
|
+
{
|
|
894
|
+
message: "Invalid numeric value",
|
|
895
|
+
position: 9,
|
|
896
|
+
length: 3,
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
message: "Invalid numeric value",
|
|
900
|
+
position: 13,
|
|
901
|
+
length: 3,
|
|
902
|
+
},
|
|
903
|
+
]);
|
|
904
|
+
|
|
905
|
+
// Invalid date IN
|
|
906
|
+
// const invalidDateResult = parseSearchInputQuery(
|
|
907
|
+
// "date:IN(2024-01-01,invalid-date)",
|
|
908
|
+
// schemas
|
|
909
|
+
// );
|
|
910
|
+
// expect(invalidDateResult.type).toBe("SEARCH_QUERY_ERROR");
|
|
911
|
+
// expect((invalidDateResult as SearchQueryError).errors).toContainEqual(
|
|
912
|
+
// expect.objectContaining({
|
|
913
|
+
// message: "Invalid date format in IN expression",
|
|
914
|
+
// })
|
|
915
|
+
// );
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
test("parses complex queries with IN", () => {
|
|
919
|
+
testValidQuery(
|
|
920
|
+
'category:IN("winter boots","summer shoes") AND status:active',
|
|
921
|
+
'(category:IN("winter boots","summer shoes") AND status:active)'
|
|
922
|
+
);
|
|
923
|
+
testValidQuery(
|
|
924
|
+
'status:IN(active,pending) OR (category:"boots" AND price:>100)',
|
|
925
|
+
"(status:IN(active,pending) OR (category:boots AND price:>100))"
|
|
926
|
+
);
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
describe("IN Error Cases", () => {
|
|
930
|
+
test("handles empty IN lists", () => {
|
|
931
|
+
testErrorQuery("status:IN()", [
|
|
932
|
+
{
|
|
933
|
+
message: "IN operator requires at least one value",
|
|
934
|
+
position: 10,
|
|
935
|
+
length: 1,
|
|
936
|
+
},
|
|
937
|
+
]);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
test("handles missing closing parenthesis", () => {
|
|
941
|
+
testErrorQuery("status:IN(active,pending", [
|
|
942
|
+
{
|
|
943
|
+
message: "Expected ',' or ')' after IN value",
|
|
944
|
+
position: 5,
|
|
945
|
+
length: 1,
|
|
946
|
+
},
|
|
947
|
+
]);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
test("handles invalid field names", () => {
|
|
951
|
+
const schemas: FieldSchema[] = [{ name: "status", type: "string" }];
|
|
952
|
+
const result = parseSearchInputQuery("invalid:IN(a,b)", schemas);
|
|
953
|
+
expect(result.type).toBe("SEARCH_QUERY_ERROR");
|
|
954
|
+
expect((result as SearchQueryError).errors).toContainEqual(
|
|
955
|
+
expect.objectContaining({
|
|
956
|
+
message: 'Invalid field: "invalid"',
|
|
957
|
+
})
|
|
958
|
+
);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
test("handles invalid value separators", () => {
|
|
962
|
+
testErrorQuery("status:IN(active pending)", [
|
|
963
|
+
{
|
|
964
|
+
message: "Expected ',' or ')' after IN value",
|
|
965
|
+
position: 17,
|
|
966
|
+
length: 1,
|
|
967
|
+
},
|
|
968
|
+
]);
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
test("handles unterminated quoted strings in IN list", () => {
|
|
972
|
+
testErrorQuery('status:IN("unclosed', [
|
|
973
|
+
{
|
|
974
|
+
message: "Unterminated quoted string",
|
|
975
|
+
position: 10,
|
|
976
|
+
length: 10,
|
|
977
|
+
},
|
|
978
|
+
]);
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
});
|