@gblikas/querykit 0.0.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 (118) hide show
  1. package/.cursor/BUGBOT.md +21 -0
  2. package/.cursor/rules/01-project-structure.mdc +77 -0
  3. package/.cursor/rules/02-typescript-standards.mdc +105 -0
  4. package/.cursor/rules/03-testing-standards.mdc +78 -0
  5. package/.cursor/rules/04-query-language.mdc +79 -0
  6. package/.cursor/rules/05-solid-principles.mdc +118 -0
  7. package/.cursor/rules/liqe-readme-docs.mdc +438 -0
  8. package/.devcontainer/devcontainer.json +25 -0
  9. package/.eslintignore +1 -0
  10. package/.eslintrc.js +39 -0
  11. package/.github/dependabot.yml +12 -0
  12. package/.github/workflows/ci.yml +114 -0
  13. package/.github/workflows/publish.yml +61 -0
  14. package/.husky/pre-commit +30 -0
  15. package/.prettierrc +10 -0
  16. package/CONTRIBUTING.md +187 -0
  17. package/LICENSE +674 -0
  18. package/README.md +237 -0
  19. package/dist/adapters/drizzle/index.d.ts +122 -0
  20. package/dist/adapters/drizzle/index.js +166 -0
  21. package/dist/adapters/index.d.ts +7 -0
  22. package/dist/adapters/index.js +25 -0
  23. package/dist/adapters/types.d.ts +60 -0
  24. package/dist/adapters/types.js +8 -0
  25. package/dist/index.d.ts +75 -0
  26. package/dist/index.js +118 -0
  27. package/dist/parser/index.d.ts +2 -0
  28. package/dist/parser/index.js +18 -0
  29. package/dist/parser/parser.d.ts +51 -0
  30. package/dist/parser/parser.js +201 -0
  31. package/dist/parser/types.d.ts +68 -0
  32. package/dist/parser/types.js +5 -0
  33. package/dist/query/builder.d.ts +61 -0
  34. package/dist/query/builder.js +188 -0
  35. package/dist/query/index.d.ts +2 -0
  36. package/dist/query/index.js +18 -0
  37. package/dist/query/types.d.ts +79 -0
  38. package/dist/query/types.js +2 -0
  39. package/dist/security/index.d.ts +2 -0
  40. package/dist/security/index.js +18 -0
  41. package/dist/security/types.d.ts +181 -0
  42. package/dist/security/types.js +43 -0
  43. package/dist/security/validator.d.ts +191 -0
  44. package/dist/security/validator.js +344 -0
  45. package/dist/translators/drizzle/index.d.ts +73 -0
  46. package/dist/translators/drizzle/index.js +260 -0
  47. package/dist/translators/index.d.ts +8 -0
  48. package/dist/translators/index.js +27 -0
  49. package/dist/translators/sql/index.d.ts +108 -0
  50. package/dist/translators/sql/index.js +252 -0
  51. package/dist/translators/types.d.ts +39 -0
  52. package/dist/translators/types.js +8 -0
  53. package/examples/qk-next/README.md +35 -0
  54. package/examples/qk-next/app/favicon.ico +0 -0
  55. package/examples/qk-next/app/globals.css +122 -0
  56. package/examples/qk-next/app/layout.tsx +121 -0
  57. package/examples/qk-next/app/page.tsx +813 -0
  58. package/examples/qk-next/app/providers.tsx +80 -0
  59. package/examples/qk-next/components/aurora-background.tsx +12 -0
  60. package/examples/qk-next/components/github-stars.tsx +51 -0
  61. package/examples/qk-next/components/mode-toggle.tsx +27 -0
  62. package/examples/qk-next/components/reactbits/blocks/Backgrounds/Aurora/Aurora.tsx +217 -0
  63. package/examples/qk-next/components/reactbits/blocks/Backgrounds/LightRays/LightRays.tsx +474 -0
  64. package/examples/qk-next/components/theme-provider.tsx +11 -0
  65. package/examples/qk-next/components/ui/card.tsx +92 -0
  66. package/examples/qk-next/components/ui/command.tsx +184 -0
  67. package/examples/qk-next/components/ui/dialog.tsx +143 -0
  68. package/examples/qk-next/components/ui/drawer.tsx +135 -0
  69. package/examples/qk-next/components/ui/hover-card.tsx +44 -0
  70. package/examples/qk-next/components/ui/icons.tsx +148 -0
  71. package/examples/qk-next/components/ui/sonner.tsx +26 -0
  72. package/examples/qk-next/components/ui/table.tsx +117 -0
  73. package/examples/qk-next/components.json +21 -0
  74. package/examples/qk-next/eslint.config.mjs +21 -0
  75. package/examples/qk-next/jsrepo.json +13 -0
  76. package/examples/qk-next/lib/utils.ts +6 -0
  77. package/examples/qk-next/next.config.ts +8 -0
  78. package/examples/qk-next/package.json +48 -0
  79. package/examples/qk-next/pnpm-lock.yaml +5558 -0
  80. package/examples/qk-next/postcss.config.mjs +5 -0
  81. package/examples/qk-next/public/file.svg +1 -0
  82. package/examples/qk-next/public/globe.svg +1 -0
  83. package/examples/qk-next/public/next.svg +1 -0
  84. package/examples/qk-next/public/vercel.svg +1 -0
  85. package/examples/qk-next/public/window.svg +1 -0
  86. package/examples/qk-next/tsconfig.json +42 -0
  87. package/examples/qk-next/types/sonner.d.ts +3 -0
  88. package/jest.config.js +26 -0
  89. package/package.json +51 -0
  90. package/src/adapters/drizzle/drizzle-adapter.test.ts +115 -0
  91. package/src/adapters/drizzle/index.ts +299 -0
  92. package/src/adapters/index.ts +11 -0
  93. package/src/adapters/types.ts +72 -0
  94. package/src/index.ts +194 -0
  95. package/src/integration.test.ts +202 -0
  96. package/src/parser/index.ts +2 -0
  97. package/src/parser/parser.test.ts +1056 -0
  98. package/src/parser/parser.ts +268 -0
  99. package/src/parser/types.ts +97 -0
  100. package/src/query/builder.test.ts +272 -0
  101. package/src/query/builder.ts +274 -0
  102. package/src/query/index.ts +2 -0
  103. package/src/query/types.ts +107 -0
  104. package/src/security/index.ts +2 -0
  105. package/src/security/types.ts +210 -0
  106. package/src/security/validator.test.ts +459 -0
  107. package/src/security/validator.ts +395 -0
  108. package/src/security.test.ts +366 -0
  109. package/src/translators/drizzle/drizzle-translator.test.ts +128 -0
  110. package/src/translators/drizzle/index.test.ts +45 -0
  111. package/src/translators/drizzle/index.ts +346 -0
  112. package/src/translators/index.ts +14 -0
  113. package/src/translators/sql/index.test.ts +45 -0
  114. package/src/translators/sql/index.ts +331 -0
  115. package/src/translators/sql/sql-translator.test.ts +419 -0
  116. package/src/translators/types.ts +44 -0
  117. package/src/types/sonner.d.ts +3 -0
  118. package/tsconfig.json +34 -0
@@ -0,0 +1,1056 @@
1
+ import { QueryParser, QueryParseError } from './parser';
2
+ import { QueryExpression } from './types';
3
+
4
+ // Replace the type definition with this approach
5
+ type QueryParserPrivate = {
6
+ convertLiqeAst(node: unknown): QueryExpression;
7
+ convertLiqeOperator(operator: string): string;
8
+ convertLiqeValue(
9
+ value: unknown
10
+ ): string | number | boolean | null | unknown[];
11
+ normalizeFieldName(field: string): string;
12
+ };
13
+
14
+ describe('QueryParser', () => {
15
+ let parser: QueryParser;
16
+
17
+ beforeEach(() => {
18
+ parser = new QueryParser();
19
+ });
20
+
21
+ describe('parse', () => {
22
+ it('should parse simple comparison expressions', () => {
23
+ const query = 'priority:>2';
24
+ const expected: QueryExpression = {
25
+ type: 'comparison',
26
+ field: 'priority',
27
+ operator: '>',
28
+ value: 2
29
+ };
30
+
31
+ expect(parser.parse(query)).toEqual(expected);
32
+ });
33
+
34
+ it('should parse equality comparison expressions', () => {
35
+ const query = 'priority:2';
36
+ const expected: QueryExpression = {
37
+ type: 'comparison',
38
+ field: 'priority',
39
+ operator: '==',
40
+ value: 2
41
+ };
42
+
43
+ expect(parser.parse(query)).toEqual(expected);
44
+ });
45
+
46
+ it('should parse less than comparison expressions', () => {
47
+ const query = 'priority:<2';
48
+ const expected: QueryExpression = {
49
+ type: 'comparison',
50
+ field: 'priority',
51
+ operator: '<',
52
+ value: 2
53
+ };
54
+
55
+ expect(parser.parse(query)).toEqual(expected);
56
+ });
57
+
58
+ it('should parse less than or equal comparison expressions', () => {
59
+ const query = 'priority:<=2';
60
+ const expected: QueryExpression = {
61
+ type: 'comparison',
62
+ field: 'priority',
63
+ operator: '<=',
64
+ value: 2
65
+ };
66
+
67
+ expect(parser.parse(query)).toEqual(expected);
68
+ });
69
+
70
+ it('should parse greater than or equal comparison expressions', () => {
71
+ const query = 'priority:>=2';
72
+ const expected: QueryExpression = {
73
+ type: 'comparison',
74
+ field: 'priority',
75
+ operator: '>=',
76
+ value: 2
77
+ };
78
+
79
+ expect(parser.parse(query)).toEqual(expected);
80
+ });
81
+
82
+ it('should parse IN operator expressions', () => {
83
+ const query = 'status:"active" OR status:"pending"';
84
+ const expected: QueryExpression = {
85
+ type: 'logical',
86
+ operator: 'OR',
87
+ left: {
88
+ type: 'comparison',
89
+ field: 'status',
90
+ operator: '==',
91
+ value: 'active'
92
+ },
93
+ right: {
94
+ type: 'comparison',
95
+ field: 'status',
96
+ operator: '==',
97
+ value: 'pending'
98
+ }
99
+ };
100
+
101
+ expect(parser.parse(query)).toEqual(expected);
102
+ });
103
+
104
+ it('should parse NOT IN operator expressions', () => {
105
+ const query = 'NOT (status:"inactive" OR status:"closed")';
106
+ const expected: QueryExpression = {
107
+ type: 'logical',
108
+ operator: 'NOT',
109
+ left: {
110
+ type: 'logical',
111
+ operator: 'OR',
112
+ left: {
113
+ type: 'comparison',
114
+ field: 'status',
115
+ operator: '==',
116
+ value: 'inactive'
117
+ },
118
+ right: {
119
+ type: 'comparison',
120
+ field: 'status',
121
+ operator: '==',
122
+ value: 'closed'
123
+ }
124
+ }
125
+ };
126
+
127
+ expect(parser.parse(query)).toEqual(expected);
128
+ });
129
+
130
+ it('should parse wildcard expressions', () => {
131
+ const query = 'name:"John*"';
132
+ const expected: QueryExpression = {
133
+ type: 'comparison',
134
+ field: 'name',
135
+ operator: 'LIKE',
136
+ value: 'John*'
137
+ };
138
+
139
+ expect(parser.parse(query)).toEqual(expected);
140
+ });
141
+
142
+ it('should parse logical AND expressions', () => {
143
+ const query = 'priority:>2 AND status:active';
144
+ const expected: QueryExpression = {
145
+ type: 'logical',
146
+ operator: 'AND',
147
+ left: {
148
+ type: 'comparison',
149
+ field: 'priority',
150
+ operator: '>',
151
+ value: 2
152
+ },
153
+ right: {
154
+ type: 'comparison',
155
+ field: 'status',
156
+ operator: '==',
157
+ value: 'active'
158
+ }
159
+ };
160
+
161
+ expect(parser.parse(query)).toEqual(expected);
162
+ });
163
+
164
+ it('should parse logical OR expressions', () => {
165
+ const query = 'status:active OR status:pending';
166
+ const expected: QueryExpression = {
167
+ type: 'logical',
168
+ operator: 'OR',
169
+ left: {
170
+ type: 'comparison',
171
+ field: 'status',
172
+ operator: '==',
173
+ value: 'active'
174
+ },
175
+ right: {
176
+ type: 'comparison',
177
+ field: 'status',
178
+ operator: '==',
179
+ value: 'pending'
180
+ }
181
+ };
182
+
183
+ expect(parser.parse(query)).toEqual(expected);
184
+ });
185
+
186
+ it('should parse NOT expressions', () => {
187
+ const query = 'NOT status:inactive';
188
+ const expected: QueryExpression = {
189
+ type: 'logical',
190
+ operator: 'NOT',
191
+ left: {
192
+ type: 'comparison',
193
+ field: 'status',
194
+ operator: '==',
195
+ value: 'inactive'
196
+ }
197
+ };
198
+
199
+ expect(parser.parse(query)).toEqual(expected);
200
+ });
201
+
202
+ it('should parse parenthesized expressions', () => {
203
+ const query = '(status:active OR status:pending) AND priority:>2';
204
+ const expected: QueryExpression = {
205
+ type: 'logical',
206
+ operator: 'AND',
207
+ left: {
208
+ type: 'logical',
209
+ operator: 'OR',
210
+ left: {
211
+ type: 'comparison',
212
+ field: 'status',
213
+ operator: '==',
214
+ value: 'active'
215
+ },
216
+ right: {
217
+ type: 'comparison',
218
+ field: 'status',
219
+ operator: '==',
220
+ value: 'pending'
221
+ }
222
+ },
223
+ right: {
224
+ type: 'comparison',
225
+ field: 'priority',
226
+ operator: '>',
227
+ value: 2
228
+ }
229
+ };
230
+
231
+ expect(parser.parse(query)).toEqual(expected);
232
+ });
233
+
234
+ it('should handle case-insensitive fields when enabled', () => {
235
+ const parser = new QueryParser({ caseInsensitiveFields: true });
236
+ const query = 'STATUS:active';
237
+ const expected: QueryExpression = {
238
+ type: 'comparison',
239
+ field: 'status',
240
+ operator: '==',
241
+ value: 'active'
242
+ };
243
+
244
+ expect(parser.parse(query)).toEqual(expected);
245
+ });
246
+
247
+ it('should apply field mappings when provided', () => {
248
+ const parser = new QueryParser({
249
+ fieldMappings: { user_name: 'username' }
250
+ });
251
+ const query = 'user_name:john';
252
+ const expected: QueryExpression = {
253
+ type: 'comparison',
254
+ field: 'username',
255
+ operator: '==',
256
+ value: 'john'
257
+ };
258
+
259
+ expect(parser.parse(query)).toEqual(expected);
260
+ });
261
+
262
+ it('should throw QueryParseError for invalid queries', () => {
263
+ const invalidQueries = ['', 'invalid:query:format', 'field:', ':value'];
264
+
265
+ invalidQueries.forEach(query => {
266
+ expect(() => parser.parse(query)).toThrow(QueryParseError);
267
+ });
268
+ });
269
+
270
+ it('should throw QueryParseError for unsupported operators', () => {
271
+ const query = 'priority:^2';
272
+ expect(() => parser.parse(query)).toThrow(QueryParseError);
273
+ });
274
+
275
+ it('should throw QueryParseError for invalid AST nodes', () => {
276
+ // This test simulates an internal error in the parser
277
+ const mockLiqeAst = {} as unknown;
278
+ expect(() =>
279
+ parser['convertLiqeAst'](mockLiqeAst as import('liqe').LiqeQuery)
280
+ ).toThrow(QueryParseError);
281
+ });
282
+
283
+ // New test cases start here
284
+ it('should parse quoted string values', () => {
285
+ const query = 'name:"John Doe"';
286
+ const expected: QueryExpression = {
287
+ type: 'comparison',
288
+ field: 'name',
289
+ operator: '==',
290
+ value: 'John Doe'
291
+ };
292
+
293
+ expect(parser.parse(query)).toEqual(expected);
294
+ });
295
+
296
+ it('should parse boolean values', () => {
297
+ const query = 'isActive:true';
298
+ const expected: QueryExpression = {
299
+ type: 'comparison',
300
+ field: 'isActive',
301
+ operator: '==',
302
+ value: true
303
+ };
304
+
305
+ expect(parser.parse(query)).toEqual(expected);
306
+ });
307
+
308
+ it('should parse null values', () => {
309
+ const query = 'lastName:null';
310
+ const expected: QueryExpression = {
311
+ type: 'comparison',
312
+ field: 'lastName',
313
+ operator: '==',
314
+ value: null
315
+ };
316
+
317
+ expect(parser.parse(query)).toEqual(expected);
318
+ });
319
+
320
+ it('should parse complex nested expressions with multiple operators', () => {
321
+ const query =
322
+ '(name:"John" OR name:"Jane") AND (priority:>2 AND (isActive:true OR role:"admin"))';
323
+ const expected: QueryExpression = {
324
+ type: 'logical',
325
+ operator: 'AND',
326
+ left: {
327
+ type: 'logical',
328
+ operator: 'OR',
329
+ left: {
330
+ type: 'comparison',
331
+ field: 'name',
332
+ operator: '==',
333
+ value: 'John'
334
+ },
335
+ right: {
336
+ type: 'comparison',
337
+ field: 'name',
338
+ operator: '==',
339
+ value: 'Jane'
340
+ }
341
+ },
342
+ right: {
343
+ type: 'logical',
344
+ operator: 'AND',
345
+ left: {
346
+ type: 'comparison',
347
+ field: 'priority',
348
+ operator: '>',
349
+ value: 2
350
+ },
351
+ right: {
352
+ type: 'logical',
353
+ operator: 'OR',
354
+ left: {
355
+ type: 'comparison',
356
+ field: 'isActive',
357
+ operator: '==',
358
+ value: true
359
+ },
360
+ right: {
361
+ type: 'comparison',
362
+ field: 'role',
363
+ operator: '==',
364
+ value: 'admin'
365
+ }
366
+ }
367
+ }
368
+ };
369
+
370
+ expect(parser.parse(query)).toEqual(expected);
371
+ });
372
+
373
+ it('should apply complex field mappings', () => {
374
+ const parser = new QueryParser({
375
+ fieldMappings: {
376
+ user_name: 'username',
377
+ user_priority: 'priority',
378
+ user_active: 'isActive'
379
+ }
380
+ });
381
+
382
+ const query = 'user_name:john AND user_priority:>2 AND user_active:true';
383
+ const expected: QueryExpression = {
384
+ type: 'logical',
385
+ operator: 'AND',
386
+ left: {
387
+ type: 'logical',
388
+ operator: 'AND',
389
+ left: {
390
+ type: 'comparison',
391
+ field: 'username',
392
+ operator: '==',
393
+ value: 'john'
394
+ },
395
+ right: {
396
+ type: 'comparison',
397
+ field: 'priority',
398
+ operator: '>',
399
+ value: 2
400
+ }
401
+ },
402
+ right: {
403
+ type: 'comparison',
404
+ field: 'isActive',
405
+ operator: '==',
406
+ value: true
407
+ }
408
+ };
409
+
410
+ expect(parser.parse(query)).toEqual(expected);
411
+ });
412
+
413
+ it('should throw QueryParseError for invalid logical operator', () => {
414
+ // Simulating an internal error when converting logical operators
415
+ const mockLiqeAst = {
416
+ type: 'LogicalExpression',
417
+ operator: { operator: 'INVALID' },
418
+ left: {
419
+ type: 'Tag',
420
+ field: { name: 'field' },
421
+ expression: { value: 'value' },
422
+ operator: { operator: ':' }
423
+ }
424
+ } as unknown;
425
+
426
+ expect(() =>
427
+ parser['convertLiqeAst'](
428
+ mockLiqeAst as unknown as import('liqe').LiqeQuery
429
+ )
430
+ ).toThrow(QueryParseError);
431
+ });
432
+
433
+ it('should throw QueryParseError for invalid field or expression in Tag node', () => {
434
+ // Simulating an internal error where field or expression is missing in a Tag node
435
+ const mockLiqeAst = {
436
+ type: 'Tag',
437
+ operator: { operator: ':' }
438
+ } as unknown;
439
+
440
+ expect(() =>
441
+ parser['convertLiqeAst'](
442
+ mockLiqeAst as unknown as import('liqe').LiqeQuery
443
+ )
444
+ ).toThrow(QueryParseError);
445
+ });
446
+
447
+ it('should throw QueryParseError for invalid empty expression', () => {
448
+ // Simulating an internal error for an invalid empty expression
449
+ const mockLiqeAst = {
450
+ type: 'EmptyExpression'
451
+ // Missing 'left' property
452
+ } as unknown;
453
+
454
+ expect(() =>
455
+ parser['convertLiqeAst'](
456
+ mockLiqeAst as unknown as import('liqe').LiqeQuery
457
+ )
458
+ ).toThrow(QueryParseError);
459
+ });
460
+
461
+ it('should throw QueryParseError for invalid parenthesized expression', () => {
462
+ // Simulating an internal error for an invalid parenthesized expression
463
+ const mockLiqeAst = {
464
+ type: 'ParenthesizedExpression'
465
+ // Missing 'expression' property
466
+ } as unknown;
467
+
468
+ expect(() =>
469
+ parser['convertLiqeAst'](
470
+ mockLiqeAst as unknown as import('liqe').LiqeQuery
471
+ )
472
+ ).toThrow(QueryParseError);
473
+ });
474
+
475
+ it('should throw QueryParseError for unsupported value type', () => {
476
+ // Create a mock Tag node with an unsupported value type (object)
477
+ const mockLiqeAst = {
478
+ type: 'Tag',
479
+ field: { name: 'field' },
480
+ operator: { operator: ':' },
481
+ expression: { value: { unsupported: 'object' } }
482
+ } as unknown;
483
+
484
+ expect(() =>
485
+ parser['convertLiqeAst'](mockLiqeAst as import('liqe').LiqeQuery)
486
+ ).toThrow(QueryParseError);
487
+ });
488
+
489
+ it('should parse multiple field mappings in a single query', () => {
490
+ const parser = new QueryParser({
491
+ fieldMappings: {
492
+ first_name: 'firstName',
493
+ last_name: 'lastName'
494
+ }
495
+ });
496
+
497
+ const query = 'first_name:John AND last_name:Doe';
498
+ const expected: QueryExpression = {
499
+ type: 'logical',
500
+ operator: 'AND',
501
+ left: {
502
+ type: 'comparison',
503
+ field: 'firstName',
504
+ operator: '==',
505
+ value: 'John'
506
+ },
507
+ right: {
508
+ type: 'comparison',
509
+ field: 'lastName',
510
+ operator: '==',
511
+ value: 'Doe'
512
+ }
513
+ };
514
+
515
+ expect(parser.parse(query)).toEqual(expected);
516
+ });
517
+
518
+ // New test cases to improve coverage
519
+ it('should handle IN operator expressions with array values', () => {
520
+ // This test is to handle the IN operator cases in convertLiqeOperator
521
+ const mockLiqeAst = {
522
+ type: 'Tag',
523
+ field: { name: 'status' },
524
+ operator: { operator: 'in' },
525
+ expression: { value: ['active', 'pending'] }
526
+ } as unknown;
527
+
528
+ const result = parser['convertLiqeAst'](
529
+ mockLiqeAst as import('liqe').LiqeQuery
530
+ );
531
+ expect(result).toEqual({
532
+ type: 'comparison',
533
+ field: 'status',
534
+ operator: 'IN',
535
+ value: ['active', 'pending']
536
+ });
537
+ });
538
+
539
+ it('should handle NOT IN operator expressions with array values', () => {
540
+ // This test is to handle the NOT IN operator cases in convertLiqeOperator
541
+ const mockLiqeAst = {
542
+ type: 'Tag',
543
+ field: { name: 'status' },
544
+ operator: { operator: 'not in' },
545
+ expression: { value: ['inactive', 'deleted'] }
546
+ } as unknown;
547
+
548
+ const result = parser['convertLiqeAst'](
549
+ mockLiqeAst as import('liqe').LiqeQuery
550
+ );
551
+ expect(result).toEqual({
552
+ type: 'comparison',
553
+ field: 'status',
554
+ operator: 'NOT IN',
555
+ value: ['inactive', 'deleted']
556
+ });
557
+ });
558
+
559
+ it('should handle equality operator (=) expressions', () => {
560
+ // Test for the '=' case in convertLiqeOperator
561
+ const mockLiqeAst = {
562
+ type: 'Tag',
563
+ field: { name: 'priority' },
564
+ operator: { operator: '=' },
565
+ expression: { value: 2 }
566
+ } as unknown;
567
+
568
+ const result = parser['convertLiqeAst'](
569
+ mockLiqeAst as import('liqe').LiqeQuery
570
+ );
571
+ expect(result).toEqual({
572
+ type: 'comparison',
573
+ field: 'priority',
574
+ operator: '==',
575
+ value: 2
576
+ });
577
+ });
578
+
579
+ it('should handle array values in queries', () => {
580
+ // Testing array handling in convertLiqeValue
581
+ const mockLiqeAst = {
582
+ type: 'Tag',
583
+ field: { name: 'tags' },
584
+ operator: { operator: ':' },
585
+ expression: { value: ['important', 'urgent', 'critical'] }
586
+ } as unknown;
587
+
588
+ const result = parser['convertLiqeAst'](
589
+ mockLiqeAst as import('liqe').LiqeQuery
590
+ );
591
+ expect(result).toEqual({
592
+ type: 'comparison',
593
+ field: 'tags',
594
+ operator: '==',
595
+ value: ['important', 'urgent', 'critical']
596
+ });
597
+ });
598
+
599
+ it('should handle EmptyExpression with left property', () => {
600
+ // Testing the EmptyExpression branch with a left property
601
+ const mockLiqeAst = {
602
+ type: 'EmptyExpression',
603
+ left: {
604
+ type: 'Tag',
605
+ field: { name: 'status' },
606
+ operator: { operator: ':' },
607
+ expression: { value: 'active' }
608
+ }
609
+ } as unknown;
610
+
611
+ const result = parser['convertLiqeAst'](
612
+ mockLiqeAst as import('liqe').LiqeQuery
613
+ );
614
+ expect(result).toEqual({
615
+ type: 'comparison',
616
+ field: 'status',
617
+ operator: '==',
618
+ value: 'active'
619
+ });
620
+ });
621
+
622
+ it('should handle error details in QueryParseError', () => {
623
+ try {
624
+ // Force a specific error to test error handling in parse method
625
+ parser['convertLiqeAst'] = (): QueryExpression => {
626
+ const error = new Error('Test specific error message');
627
+ throw error;
628
+ };
629
+
630
+ parser.parse('status:active');
631
+ fail('Should have thrown an error');
632
+ } catch (error) {
633
+ expect(error).toBeInstanceOf(QueryParseError);
634
+ expect((error as Error).message).toContain(
635
+ 'Test specific error message'
636
+ );
637
+ } finally {
638
+ // Restore original method
639
+ parser['convertLiqeAst'] = jest.fn();
640
+ }
641
+ });
642
+
643
+ it('should throw QueryParseError for non-Error exceptions', () => {
644
+ try {
645
+ // Force a non-Error exception to test that branch
646
+ parser['convertLiqeAst'] = (): QueryExpression => {
647
+ throw 'String exception'; // Not an Error object
648
+ };
649
+
650
+ parser.parse('status:active');
651
+ fail('Should have thrown an error');
652
+ } catch (error) {
653
+ expect(error).toBeInstanceOf(QueryParseError);
654
+ expect((error as Error).message).toContain('String exception');
655
+ } finally {
656
+ // Restore original method
657
+ parser['convertLiqeAst'] = jest.fn();
658
+ }
659
+ });
660
+
661
+ // Additional tests to improve coverage further
662
+ it('should handle explicit case when node type is not recognized', () => {
663
+ // Testing line 78: unsupported node type branch
664
+ const mockLiqeAst = {
665
+ type: 'UnsupportedNodeType' // This should trigger the default case
666
+ } as unknown;
667
+
668
+ expect(() =>
669
+ parser['convertLiqeAst'](mockLiqeAst as import('liqe').LiqeQuery)
670
+ ).toThrow(QueryParseError);
671
+ });
672
+
673
+ it('should handle more complex field names in normalizeFieldName', () => {
674
+ // Create a parser with both case insensitivity and field mappings
675
+ // to exercise line 207 in normalizeFieldName
676
+ const parser = new QueryParser({
677
+ caseInsensitiveFields: true,
678
+ fieldMappings: {
679
+ 'user_profile.name': 'userName',
680
+ 'user_profile.priority': 'userPriority'
681
+ }
682
+ });
683
+
684
+ const mockLiqeAst = {
685
+ type: 'Tag',
686
+ field: { name: 'USER_PROFILE.NAME' }, // Upper case to test case insensitivity
687
+ operator: { operator: ':' },
688
+ expression: { value: 'John' }
689
+ } as unknown;
690
+
691
+ const result = parser['convertLiqeAst'](
692
+ mockLiqeAst as import('liqe').LiqeQuery
693
+ );
694
+ expect(result).toEqual({
695
+ type: 'comparison',
696
+ field: 'userName', // Should be mapped and case-normalized
697
+ operator: '==',
698
+ value: 'John'
699
+ });
700
+ });
701
+
702
+ it('should handle invalid parenthesized expression with undefined expression', () => {
703
+ // This tests line 142 - when a parenthesized expression has an undefined expression
704
+ const mockLiqeAst = {
705
+ type: 'ParenthesizedExpression',
706
+ expression: undefined
707
+ } as unknown;
708
+
709
+ expect(() =>
710
+ parser['convertLiqeAst'](mockLiqeAst as import('liqe').LiqeQuery)
711
+ ).toThrow(QueryParseError);
712
+ });
713
+
714
+ // Target line 78 specifically with correct typing
715
+ it('should throw QueryParseError for unknown node types', () => {
716
+ // We need to make sure line 78 executes by creating an object with a type property but not a known type
717
+ const mockUnknownNode = {
718
+ type: 'UnknownType'
719
+ } as unknown as import('liqe').LiqeQuery;
720
+
721
+ expect(() => parser['convertLiqeAst'](mockUnknownNode)).toThrowError(
722
+ 'Unsupported node type: UnknownType'
723
+ );
724
+ });
725
+
726
+ // Target line 142 specifically - ParenthesizedExpression with undefined expression
727
+ it('should throw QueryParseError for ParenthesizedExpression with undefined expression', () => {
728
+ const mockParenNode = {
729
+ type: 'ParenthesizedExpression',
730
+ // Explicitly define expression as undefined to target line 142
731
+ expression: undefined
732
+ } as unknown as import('liqe').LiqeQuery;
733
+
734
+ expect(() => parser['convertLiqeAst'](mockParenNode)).toThrowError(
735
+ 'Invalid parenthesized expression'
736
+ );
737
+ });
738
+
739
+ // Target line 207 - the fallback case in normalizeFieldName
740
+ it('should return the original field name when no mapping exists', () => {
741
+ // Create a parser with field mappings that don't include the field we're testing
742
+ const parser = new QueryParser({
743
+ fieldMappings: {
744
+ known_field: 'mappedField'
745
+ }
746
+ });
747
+
748
+ // This should test line 207 by using an unmapped field
749
+ const unmappedField = 'unmapped_field';
750
+ const result = parser['normalizeFieldName'](unmappedField);
751
+
752
+ // Should return the original field since there is no mapping
753
+ expect(result).toBe(unmappedField);
754
+ });
755
+
756
+ it('should handle all cases in the switch statement of convertLiqeAst', () => {
757
+ // Testing line 78: the last case in the switch statement
758
+ // Create a direct mock of a node with an unknown type
759
+ const mockNode = { type: 'CompletelyUnknownType' };
760
+
761
+ try {
762
+ // Need to call the method directly
763
+ (parser as unknown as QueryParserPrivate).convertLiqeAst(mockNode);
764
+ fail('Should have thrown an error');
765
+ } catch (error) {
766
+ // Verify we hit the default case
767
+ expect(error).toBeInstanceOf(QueryParseError);
768
+ expect((error as QueryParseError).message).toBe(
769
+ 'Unsupported node type: CompletelyUnknownType'
770
+ );
771
+ }
772
+ });
773
+
774
+ it('should handle invalid ParenthesizedExpression node', () => {
775
+ // Testing line 142: the throw in the ParenthesizedExpression case
776
+ // Create a direct mock of a ParenthesizedExpression without an expression property
777
+ const mockNode = {
778
+ type: 'ParenthesizedExpression'
779
+ // Intentionally not adding an expression property
780
+ };
781
+
782
+ try {
783
+ // Need to call the method directly
784
+ (parser as unknown as QueryParserPrivate).convertLiqeAst(mockNode);
785
+ fail('Should have thrown an error');
786
+ } catch (error) {
787
+ // Verify we hit the right case
788
+ expect(error).toBeInstanceOf(QueryParseError);
789
+ expect((error as QueryParseError).message).toBe(
790
+ 'Invalid parenthesized expression'
791
+ );
792
+ }
793
+ });
794
+
795
+ it('should handle multiple wildcards in a pattern', () => {
796
+ const query = 'title:"*product*"';
797
+ const expected: QueryExpression = {
798
+ type: 'comparison',
799
+ field: 'title',
800
+ operator: 'LIKE',
801
+ value: '*product*'
802
+ };
803
+
804
+ expect(parser.parse(query)).toEqual(expected);
805
+ });
806
+
807
+ it('should parse prefix wildcard pattern (foo*)', () => {
808
+ const query = 'name:foo*';
809
+ const expected: QueryExpression = {
810
+ type: 'comparison',
811
+ field: 'name',
812
+ operator: 'LIKE',
813
+ value: 'foo*'
814
+ };
815
+
816
+ expect(parser.parse(query)).toEqual(expected);
817
+ });
818
+
819
+ it('should parse combined wildcard pattern (foo*bar)', () => {
820
+ const query = 'name:foo*bar';
821
+ const expected: QueryExpression = {
822
+ type: 'comparison',
823
+ field: 'name',
824
+ operator: 'LIKE',
825
+ value: 'foo*bar'
826
+ };
827
+
828
+ expect(parser.parse(query)).toEqual(expected);
829
+ });
830
+
831
+ it('should parse single character wildcard (foo?)', () => {
832
+ const query = 'name:foo?';
833
+ const expected: QueryExpression = {
834
+ type: 'comparison',
835
+ field: 'name',
836
+ operator: 'LIKE',
837
+ value: 'foo?'
838
+ };
839
+
840
+ expect(parser.parse(query)).toEqual(expected);
841
+ });
842
+
843
+ it('should parse combined single character wildcard (foo?bar)', () => {
844
+ const query = 'name:foo?bar';
845
+ const expected: QueryExpression = {
846
+ type: 'comparison',
847
+ field: 'name',
848
+ operator: 'LIKE',
849
+ value: 'foo?bar'
850
+ };
851
+
852
+ expect(parser.parse(query)).toEqual(expected);
853
+ });
854
+
855
+ it('should parse mixed wildcard patterns (f*o?bar*)', () => {
856
+ const query = 'name:f*o?bar*';
857
+ const expected: QueryExpression = {
858
+ type: 'comparison',
859
+ field: 'name',
860
+ operator: 'LIKE',
861
+ value: 'f*o?bar*'
862
+ };
863
+
864
+ expect(parser.parse(query)).toEqual(expected);
865
+ });
866
+ });
867
+
868
+ describe('validate', () => {
869
+ it('should return true for valid queries', () => {
870
+ const validQueries = [
871
+ 'priority:>2',
872
+ 'status:active',
873
+ 'priority:>2 AND status:active',
874
+ 'status:active OR status:pending',
875
+ 'NOT status:inactive'
876
+ ];
877
+
878
+ validQueries.forEach(query => {
879
+ expect(parser.validate(query)).toBe(true);
880
+ });
881
+ });
882
+
883
+ it('should return false for invalid queries', () => {
884
+ const invalidQueries = ['', 'invalid:query:format', 'field:', ':value'];
885
+
886
+ invalidQueries.forEach(query => {
887
+ expect(parser.validate(query)).toBe(false);
888
+ });
889
+ });
890
+
891
+ // New validation tests
892
+ it('should return true for complex valid queries', () => {
893
+ const validComplexQueries = [
894
+ 'name:"John Doe" AND (priority:>2 OR role:"admin")',
895
+ '(status:active OR status:pending) AND NOT isDeleted:true',
896
+ 'tags:"important*" OR (priority:>3 AND assignee:"john@example.com")'
897
+ ];
898
+
899
+ validComplexQueries.forEach(query => {
900
+ expect(parser.validate(query)).toBe(true);
901
+ });
902
+ });
903
+
904
+ it('should return false for additional invalid queries', () => {
905
+ const additionalInvalidQueries = [
906
+ 'field:value:extra', // Too many colons
907
+ 'AND field:value', // Starting with operator
908
+ 'field:value AND', // Ending with operator
909
+ '(field:value', // Unclosed parenthesis
910
+ 'field:value)', // Extra closing parenthesis
911
+ 'OR' // Lone operator
912
+ ];
913
+
914
+ additionalInvalidQueries.forEach(query => {
915
+ expect(parser.validate(query)).toBe(false);
916
+ });
917
+ });
918
+ });
919
+
920
+ // Test additional constructor options
921
+ describe('parser options', () => {
922
+ it('should apply default options when none provided', () => {
923
+ const parser = new QueryParser();
924
+
925
+ // Case sensitive by default
926
+ const query = 'STATUS:active';
927
+ const parsedQuery = parser.parse(query);
928
+ expect(parsedQuery).toEqual({
929
+ type: 'comparison',
930
+ field: 'STATUS', // Not lowercased
931
+ operator: '==',
932
+ value: 'active'
933
+ });
934
+ });
935
+
936
+ it('should apply multiple options when provided', () => {
937
+ const parser = new QueryParser({
938
+ caseInsensitiveFields: true,
939
+ fieldMappings: { user_name: 'username' }
940
+ });
941
+
942
+ // Test both options working together
943
+ const query = 'USER_NAME:john';
944
+ const parsedQuery = parser.parse(query);
945
+ expect(parsedQuery).toEqual({
946
+ type: 'comparison',
947
+ field: 'username', // Lowercased and mapped
948
+ operator: '==',
949
+ value: 'john'
950
+ });
951
+ });
952
+ });
953
+
954
+ // Add a specific test suite for direct private method testing
955
+ describe('Private method testing for coverage', () => {
956
+ let parser: QueryParser;
957
+
958
+ beforeEach(() => {
959
+ parser = new QueryParser();
960
+ });
961
+
962
+ it('should directly test convertLiqeOperator with various inputs', () => {
963
+ // Testing convertLiqeOperator with various inputs for coverage
964
+ // Access private method using type assertion
965
+ const convertLiqeOperator = (
966
+ parser as unknown as QueryParserPrivate
967
+ ).convertLiqeOperator.bind(parser);
968
+
969
+ // Test basic cases
970
+ expect(convertLiqeOperator(':')).toBe('==');
971
+ expect(convertLiqeOperator('=')).toBe('==');
972
+ expect(convertLiqeOperator('!=')).toBe('!=');
973
+ expect(convertLiqeOperator('>')).toBe('>');
974
+ expect(convertLiqeOperator('>=')).toBe('>=');
975
+ expect(convertLiqeOperator('<')).toBe('<');
976
+ expect(convertLiqeOperator('<=')).toBe('<=');
977
+
978
+ // Test specific cases for coverage
979
+ expect(convertLiqeOperator('in')).toBe('IN');
980
+ expect(convertLiqeOperator('not in')).toBe('NOT IN');
981
+
982
+ // Test with colon prefix
983
+ expect(convertLiqeOperator(':>')).toBe('>');
984
+
985
+ // Test error case
986
+ expect(() => convertLiqeOperator('unknown')).toThrow(QueryParseError);
987
+ });
988
+
989
+ it('should directly test convertLiqeValue with various inputs', () => {
990
+ // Access private method using type assertion
991
+ const convertLiqeValue = (
992
+ parser as unknown as QueryParserPrivate
993
+ ).convertLiqeValue.bind(parser);
994
+
995
+ // Test various value types
996
+ expect(convertLiqeValue('string')).toBe('string');
997
+ expect(convertLiqeValue(123)).toBe(123);
998
+ expect(convertLiqeValue(true)).toBe(true);
999
+ expect(convertLiqeValue(null)).toBe(null);
1000
+ expect(convertLiqeValue([1, 2, 3])).toEqual([1, 2, 3]);
1001
+
1002
+ // Test error case
1003
+ expect(() => convertLiqeValue({ key: 'value' })).toThrow(QueryParseError);
1004
+ });
1005
+
1006
+ it('should directly test normalizeFieldName', () => {
1007
+ // Test with default options
1008
+ expect(
1009
+ (parser as unknown as QueryParserPrivate).normalizeFieldName(
1010
+ 'fieldName'
1011
+ )
1012
+ ).toBe('fieldName');
1013
+
1014
+ // Test with case insensitivity
1015
+ const caseInsensitiveParser = new QueryParser({
1016
+ caseInsensitiveFields: true
1017
+ });
1018
+ expect(
1019
+ (
1020
+ caseInsensitiveParser as unknown as QueryParserPrivate
1021
+ ).normalizeFieldName('FieldName')
1022
+ ).toBe('fieldname');
1023
+
1024
+ // Test with field mappings
1025
+ const mappingParser = new QueryParser({
1026
+ fieldMappings: { old_name: 'newName' }
1027
+ });
1028
+ expect(
1029
+ (mappingParser as unknown as QueryParserPrivate).normalizeFieldName(
1030
+ 'old_name'
1031
+ )
1032
+ ).toBe('newName');
1033
+ expect(
1034
+ (mappingParser as unknown as QueryParserPrivate).normalizeFieldName(
1035
+ 'unmapped'
1036
+ )
1037
+ ).toBe('unmapped');
1038
+
1039
+ // Test with both options
1040
+ const combinedParser = new QueryParser({
1041
+ caseInsensitiveFields: true,
1042
+ fieldMappings: { old_name: 'newName' }
1043
+ });
1044
+ expect(
1045
+ (combinedParser as unknown as QueryParserPrivate).normalizeFieldName(
1046
+ 'OLD_NAME'
1047
+ )
1048
+ ).toBe('newName');
1049
+ expect(
1050
+ (combinedParser as unknown as QueryParserPrivate).normalizeFieldName(
1051
+ 'Unmapped'
1052
+ )
1053
+ ).toBe('unmapped');
1054
+ });
1055
+ });
1056
+ });