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,439 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import { tokenize, TokenType } from "./lexer";
3
+
4
+ describe("Lexer", () => {
5
+ describe("Basic Token Generation", () => {
6
+ test("generates tokens for single terms", () => {
7
+ expect(tokenize("boots")).toEqual([
8
+ { type: TokenType.STRING, value: "boots", position: 0, length: 5 },
9
+ ]);
10
+
11
+ expect(tokenize("simple-term")).toEqual([
12
+ {
13
+ type: TokenType.STRING,
14
+ value: "simple-term",
15
+ position: 0,
16
+ length: 11,
17
+ },
18
+ ]);
19
+
20
+ expect(tokenize("term_with_underscore")).toEqual([
21
+ {
22
+ type: TokenType.STRING,
23
+ value: "term_with_underscore",
24
+ position: 0,
25
+ length: 20,
26
+ },
27
+ ]);
28
+ });
29
+
30
+ test("handles multiple terms with whitespace", () => {
31
+ expect(tokenize("boots summer")).toEqual([
32
+ { type: TokenType.STRING, value: "boots", position: 0, length: 5 },
33
+ { type: TokenType.STRING, value: "summer", position: 6, length: 6 },
34
+ ]);
35
+
36
+ expect(tokenize(" term1 term2 ")).toEqual([
37
+ { type: TokenType.STRING, value: "term1", position: 2, length: 5 },
38
+ { type: TokenType.STRING, value: "term2", position: 10, length: 5 },
39
+ ]);
40
+ });
41
+
42
+ test("handles logical operators", () => {
43
+ expect(tokenize("boots AND shoes")).toEqual([
44
+ { type: TokenType.STRING, value: "boots", position: 0, length: 5 },
45
+ { type: TokenType.AND, value: "AND", position: 6, length: 3 },
46
+ { type: TokenType.STRING, value: "shoes", position: 10, length: 5 },
47
+ ]);
48
+
49
+ expect(tokenize("boots OR shoes")).toEqual([
50
+ { type: TokenType.STRING, value: "boots", position: 0, length: 5 },
51
+ { type: TokenType.OR, value: "OR", position: 6, length: 2 },
52
+ { type: TokenType.STRING, value: "shoes", position: 9, length: 5 },
53
+ ]);
54
+
55
+ expect(tokenize("NOT test")).toEqual([
56
+ { type: TokenType.NOT, value: "NOT", position: 0, length: 3 },
57
+ { type: TokenType.STRING, value: "test", position: 4, length: 4 },
58
+ ]);
59
+ });
60
+ });
61
+
62
+ describe("Logical Operators", () => {
63
+ test("handles case-insensitive AND operator", () => {
64
+ const variations = ["AND", "and", "And", "aNd"];
65
+ variations.forEach((op) => {
66
+ expect(tokenize(`boots ${op} shoes`)).toEqual([
67
+ { type: TokenType.STRING, value: "boots", position: 0, length: 5 },
68
+ { type: TokenType.AND, value: "AND", position: 6, length: op.length },
69
+ {
70
+ type: TokenType.STRING,
71
+ value: "shoes",
72
+ position: 6 + op.length + 1,
73
+ length: 5,
74
+ },
75
+ ]);
76
+ });
77
+ });
78
+
79
+ test("handles case-insensitive OR operator", () => {
80
+ const variations = ["OR", "or", "Or", "oR"];
81
+ variations.forEach((op) => {
82
+ expect(tokenize(`boots ${op} shoes`)).toEqual([
83
+ { type: TokenType.STRING, value: "boots", position: 0, length: 5 },
84
+ { type: TokenType.OR, value: "OR", position: 6, length: op.length },
85
+ {
86
+ type: TokenType.STRING,
87
+ value: "shoes",
88
+ position: 6 + op.length + 1,
89
+ length: 5,
90
+ },
91
+ ]);
92
+ });
93
+ });
94
+
95
+ test("handles case-insensitive NOT operator", () => {
96
+ const variations = ["NOT", "not", "Not", "nOt"];
97
+ variations.forEach((op) => {
98
+ expect(tokenize(`${op} test`)).toEqual([
99
+ { type: TokenType.NOT, value: "NOT", position: 0, length: op.length },
100
+ {
101
+ type: TokenType.STRING,
102
+ value: "test",
103
+ position: op.length + 1,
104
+ length: 4,
105
+ },
106
+ ]);
107
+ });
108
+ });
109
+
110
+ test("handles operators as field values", () => {
111
+ expect(tokenize("field:and")).toEqual([
112
+ { type: TokenType.STRING, value: "field:and", position: 0, length: 9 },
113
+ ]);
114
+ expect(tokenize("field:or")).toEqual([
115
+ { type: TokenType.STRING, value: "field:or", position: 0, length: 8 },
116
+ ]);
117
+ expect(tokenize('field:"AND"')).toEqual([
118
+ { type: TokenType.QUOTED_STRING, value: 'field:"AND"', position: 0, length: 11 },
119
+ ]);
120
+ });
121
+
122
+ test("handles mixed case in complex queries", () => {
123
+ expect(tokenize("boots AND shoes or sneakers AND sandals")).toEqual([
124
+ { type: TokenType.STRING, value: "boots", position: 0, length: 5 },
125
+ { type: TokenType.AND, value: "AND", position: 6, length: 3 },
126
+ { type: TokenType.STRING, value: "shoes", position: 10, length: 5 },
127
+ { type: TokenType.OR, value: "OR", position: 16, length: 2 },
128
+ { type: TokenType.STRING, value: "sneakers", position: 19, length: 8 },
129
+ { type: TokenType.AND, value: "AND", position: 28, length: 3 },
130
+ { type: TokenType.STRING, value: "sandals", position: 32, length: 7 },
131
+ ]);
132
+ });
133
+ });
134
+
135
+ describe("Quoted Strings", () => {
136
+ test("handles simple quoted strings", () => {
137
+ expect(tokenize('"red shoes"')).toEqual([
138
+ {
139
+ type: TokenType.QUOTED_STRING,
140
+ value: '"red shoes"',
141
+ position: 0,
142
+ length: 11,
143
+ },
144
+ ]);
145
+ });
146
+
147
+ test("handles escaped quotes in quoted strings", () => {
148
+ expect(tokenize('"Nike\\"Air"')).toEqual([
149
+ {
150
+ type: TokenType.QUOTED_STRING,
151
+ value: '"Nike\\"Air"',
152
+ position: 0,
153
+ length: 11,
154
+ },
155
+ ]);
156
+ });
157
+
158
+ test("handles escaped characters in quoted strings", () => {
159
+ expect(tokenize('"path\\\\to\\\\file"')).toEqual([
160
+ {
161
+ type: TokenType.QUOTED_STRING,
162
+ value: '"path\\\\to\\\\file"',
163
+ position: 0,
164
+ length: 16,
165
+ },
166
+ ]);
167
+ });
168
+
169
+ test("throws error for unterminated quotes", () => {
170
+ expect(() => tokenize('"unclosed')).toThrow();
171
+ expect(() => tokenize('"escaped quote\\"')).toThrow();
172
+ });
173
+ });
174
+
175
+ describe("Field:Value Pairs", () => {
176
+ test("handles basic field:value pairs", () => {
177
+ expect(tokenize("color:red")).toEqual([
178
+ { type: TokenType.STRING, value: "color:red", position: 0, length: 9 },
179
+ ]);
180
+
181
+ expect(tokenize("size:42")).toEqual([
182
+ { type: TokenType.STRING, value: "size:42", position: 0, length: 7 },
183
+ ]);
184
+ });
185
+
186
+ test("handles field:value pairs with quoted values", () => {
187
+ expect(tokenize('status:"in progress"')).toEqual([
188
+ {
189
+ type: TokenType.QUOTED_STRING,
190
+ value: 'status:"in progress"',
191
+ position: 0,
192
+ length: 20,
193
+ },
194
+ ]);
195
+ });
196
+
197
+ test("handles field:value pairs with various spacing", () => {
198
+ expect(tokenize("field: value")).toEqual([
199
+ {
200
+ type: TokenType.STRING,
201
+ value: "field:",
202
+ position: 0,
203
+ length: 6,
204
+ },
205
+ {
206
+ type: TokenType.STRING,
207
+ value: "value",
208
+ position: 7,
209
+ length: 5,
210
+ },
211
+ ]);
212
+
213
+ expect(tokenize("field :value")).toEqual([
214
+ {
215
+ type: TokenType.STRING,
216
+ value: "field",
217
+ position: 0,
218
+ length: 5,
219
+ },
220
+ {
221
+ type: TokenType.STRING,
222
+ value: ":value",
223
+ position: 6,
224
+ length: 6,
225
+ },
226
+ ]);
227
+
228
+ expect(tokenize('field:"quoted value"')).toEqual([
229
+ {
230
+ type: TokenType.QUOTED_STRING,
231
+ value: 'field:"quoted value"',
232
+ position: 0,
233
+ length: 20,
234
+ },
235
+ ]);
236
+ });
237
+ });
238
+
239
+ describe("Parentheses", () => {
240
+ test("handles simple parentheses", () => {
241
+ expect(tokenize("(term)")).toEqual([
242
+ { type: TokenType.LPAREN, value: "(", position: 0, length: 1 },
243
+ { type: TokenType.STRING, value: "term", position: 1, length: 4 },
244
+ { type: TokenType.RPAREN, value: ")", position: 5, length: 1 },
245
+ ]);
246
+ });
247
+
248
+ test("handles nested parentheses", () => {
249
+ expect(tokenize("((a))")).toEqual([
250
+ { type: TokenType.LPAREN, value: "(", position: 0, length: 1 },
251
+ { type: TokenType.LPAREN, value: "(", position: 1, length: 1 },
252
+ { type: TokenType.STRING, value: "a", position: 2, length: 1 },
253
+ { type: TokenType.RPAREN, value: ")", position: 3, length: 1 },
254
+ { type: TokenType.RPAREN, value: ")", position: 4, length: 1 },
255
+ ]);
256
+ });
257
+
258
+ test("handles complex expressions with parentheses", () => {
259
+ expect(tokenize("(a AND b) OR c")).toEqual([
260
+ { type: TokenType.LPAREN, value: "(", position: 0, length: 1 },
261
+ { type: TokenType.STRING, value: "a", position: 1, length: 1 },
262
+ { type: TokenType.AND, value: "AND", position: 3, length: 3 },
263
+ { type: TokenType.STRING, value: "b", position: 7, length: 1 },
264
+ { type: TokenType.RPAREN, value: ")", position: 8, length: 1 },
265
+ { type: TokenType.OR, value: "OR", position: 10, length: 2 },
266
+ { type: TokenType.STRING, value: "c", position: 13, length: 1 },
267
+ ]);
268
+ });
269
+ });
270
+
271
+ describe("Complex Queries", () => {
272
+ test("handles complex field:value expressions", () => {
273
+ expect(
274
+ tokenize('category:"winter boots" AND (color:black OR color:brown)')
275
+ ).toEqual([
276
+ {
277
+ type: TokenType.QUOTED_STRING,
278
+ value: 'category:"winter boots"',
279
+ position: 0,
280
+ length: 23,
281
+ },
282
+ { type: TokenType.AND, value: "AND", position: 24, length: 3 },
283
+ { type: TokenType.LPAREN, value: "(", position: 28, length: 1 },
284
+ {
285
+ type: TokenType.STRING,
286
+ value: "color:black",
287
+ position: 29,
288
+ length: 11,
289
+ },
290
+ { type: TokenType.OR, value: "OR", position: 41, length: 2 },
291
+ {
292
+ type: TokenType.STRING,
293
+ value: "color:brown",
294
+ position: 44,
295
+ length: 11,
296
+ },
297
+ { type: TokenType.RPAREN, value: ")", position: 55, length: 1 },
298
+ ]);
299
+ });
300
+
301
+ test("handles nested expressions with multiple operators including NOT", () => {
302
+ expect(tokenize("NOT (a OR b) AND c")).toEqual([
303
+ { type: TokenType.NOT, value: "NOT", position: 0, length: 3 },
304
+ { type: TokenType.LPAREN, value: "(", position: 4, length: 1 },
305
+ { type: TokenType.STRING, value: "a", position: 5, length: 1 },
306
+ { type: TokenType.OR, value: "OR", position: 7, length: 2 },
307
+ { type: TokenType.STRING, value: "b", position: 10, length: 1 },
308
+ { type: TokenType.RPAREN, value: ")", position: 11, length: 1 },
309
+ { type: TokenType.AND, value: "AND", position: 13, length: 3 },
310
+ { type: TokenType.STRING, value: "c", position: 17, length: 1 },
311
+ ]);
312
+ });
313
+
314
+ test("handles mixed terms and operators", () => {
315
+ expect(tokenize('boots AND "red shoes" OR leather')).toEqual([
316
+ { type: TokenType.STRING, value: "boots", position: 0, length: 5 },
317
+ { type: TokenType.AND, value: "AND", position: 6, length: 3 },
318
+ {
319
+ type: TokenType.QUOTED_STRING,
320
+ value: '"red shoes"',
321
+ position: 10,
322
+ length: 11,
323
+ },
324
+ { type: TokenType.OR, value: "OR", position: 22, length: 2 },
325
+ { type: TokenType.STRING, value: "leather", position: 25, length: 7 },
326
+ ]);
327
+ });
328
+
329
+ test("handles empty input", () => {
330
+ expect(tokenize("")).toEqual([]);
331
+ expect(tokenize(" ")).toEqual([]);
332
+ expect(tokenize("\n\t")).toEqual([]);
333
+ });
334
+ });
335
+
336
+ describe("Negative Terms", () => {
337
+ test("handles simple negative terms", () => {
338
+ expect(tokenize("-test")).toEqual([
339
+ { type: TokenType.NOT, value: "NOT", position: 0, length: 1 },
340
+ { type: TokenType.STRING, value: "test", position: 1, length: 4 },
341
+ ]);
342
+ });
343
+
344
+ test("handles negative quoted strings", () => {
345
+ expect(tokenize('-"red shoes"')).toEqual([
346
+ { type: TokenType.NOT, value: "NOT", position: 0, length: 1 },
347
+ {
348
+ type: TokenType.QUOTED_STRING,
349
+ value: '"red shoes"',
350
+ position: 1,
351
+ length: 11,
352
+ },
353
+ ]);
354
+ });
355
+
356
+ test("handles negative field:value pairs", () => {
357
+ expect(tokenize("-status:active")).toEqual([
358
+ { type: TokenType.NOT, value: "NOT", position: 0, length: 1 },
359
+ {
360
+ type: TokenType.STRING,
361
+ value: "status:active",
362
+ position: 1,
363
+ length: 13,
364
+ },
365
+ ]);
366
+ });
367
+
368
+ test("handles negative terms with parentheses", () => {
369
+ expect(tokenize("-(red OR blue)")).toEqual([
370
+ { type: TokenType.NOT, value: "NOT", position: 0, length: 1 },
371
+ { type: TokenType.LPAREN, value: "(", position: 1, length: 1 },
372
+ { type: TokenType.STRING, value: "red", position: 2, length: 3 },
373
+ { type: TokenType.OR, value: "OR", position: 6, length: 2 },
374
+ { type: TokenType.STRING, value: "blue", position: 9, length: 4 },
375
+ { type: TokenType.RPAREN, value: ")", position: 13, length: 1 },
376
+ ]);
377
+ });
378
+
379
+ test("handles mixed positive and negative terms", () => {
380
+ expect(tokenize("boots -leather")).toEqual([
381
+ { type: TokenType.STRING, value: "boots", position: 0, length: 5 },
382
+ { type: TokenType.NOT, value: "NOT", position: 6, length: 1 },
383
+ { type: TokenType.STRING, value: "leather", position: 7, length: 7 },
384
+ ]);
385
+ });
386
+
387
+ test("handles multiple negative terms", () => {
388
+ expect(tokenize("-red -blue")).toEqual([
389
+ { type: TokenType.NOT, value: "NOT", position: 0, length: 1 },
390
+ { type: TokenType.STRING, value: "red", position: 1, length: 3 },
391
+ { type: TokenType.NOT, value: "NOT", position: 5, length: 1 },
392
+ { type: TokenType.STRING, value: "blue", position: 6, length: 4 },
393
+ ]);
394
+ });
395
+
396
+ test("handles hyphens within terms", () => {
397
+ expect(tokenize("pre-owned")).toEqual([
398
+ { type: TokenType.STRING, value: "pre-owned", position: 0, length: 9 },
399
+ ]);
400
+ });
401
+
402
+ test("handles hyphens in field:value pairs", () => {
403
+ expect(tokenize("product-type:pre-owned")).toEqual([
404
+ {
405
+ type: TokenType.STRING,
406
+ value: "product-type:pre-owned",
407
+ position: 0,
408
+ length: 22,
409
+ },
410
+ ]);
411
+ });
412
+ });
413
+
414
+ describe("Adjacent Terms Validation", () => {
415
+ test("rejects adjacent quoted strings", () => {
416
+ expect(() => tokenize('"test""test"')).toThrow(
417
+ "Invalid syntax: Missing operator or whitespace between terms"
418
+ );
419
+ expect(() => tokenize('"test""test""test"')).toThrow(
420
+ "Invalid syntax: Missing operator or whitespace between terms"
421
+ );
422
+ });
423
+
424
+ test("rejects adjacent terms without operators", () => {
425
+ expect(() => tokenize('test"test"')).toThrow(
426
+ "Invalid syntax: Missing operator or whitespace between terms"
427
+ );
428
+ expect(() => tokenize('"test"test')).toThrow(
429
+ "Invalid syntax: Missing operator or whitespace between terms"
430
+ );
431
+ });
432
+
433
+ test("accepts properly separated terms", () => {
434
+ expect(() => tokenize('"test" "test"')).not.toThrow();
435
+ expect(() => tokenize('"test" AND "test"')).not.toThrow();
436
+ expect(() => tokenize('"test" OR "test"')).not.toThrow();
437
+ });
438
+ });
439
+ });