@gblikas/querykit 0.2.0 → 0.3.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.
@@ -0,0 +1,325 @@
1
+ import { QueryParser } from './parser';
2
+
3
+ describe('QueryParser.parseWithContext', () => {
4
+ let parser: QueryParser;
5
+
6
+ beforeEach(() => {
7
+ parser = new QueryParser();
8
+ });
9
+
10
+ describe('successful parsing', () => {
11
+ it('should return success=true for valid query', () => {
12
+ const result = parser.parseWithContext('status:done');
13
+
14
+ expect(result.success).toBe(true);
15
+ expect(result.ast).toBeDefined();
16
+ expect(result.error).toBeUndefined();
17
+ });
18
+
19
+ it('should include AST for valid query', () => {
20
+ const result = parser.parseWithContext('status:done');
21
+
22
+ expect(result.ast).toMatchObject({
23
+ type: 'comparison',
24
+ field: 'status',
25
+ value: 'done'
26
+ });
27
+ });
28
+
29
+ it('should parse complex valid query', () => {
30
+ const result = parser.parseWithContext('status:done AND priority:high');
31
+
32
+ expect(result.success).toBe(true);
33
+ expect(result.ast?.type).toBe('logical');
34
+ });
35
+ });
36
+
37
+ describe('failed parsing', () => {
38
+ it('should return success=false for invalid query', () => {
39
+ const result = parser.parseWithContext('status:');
40
+
41
+ expect(result.success).toBe(false);
42
+ expect(result.ast).toBeUndefined();
43
+ expect(result.error).toBeDefined();
44
+ });
45
+
46
+ it('should include error message', () => {
47
+ const result = parser.parseWithContext('status:');
48
+
49
+ expect(result.error?.message).toBeDefined();
50
+ expect(result.error?.message.length).toBeGreaterThan(0);
51
+ });
52
+
53
+ it('should still return tokens for invalid query', () => {
54
+ const result = parser.parseWithContext('status:');
55
+
56
+ expect(result.tokens).toHaveLength(1);
57
+ expect(result.tokens[0]).toMatchObject({
58
+ type: 'term',
59
+ key: 'status',
60
+ value: null
61
+ });
62
+ });
63
+
64
+ it('should handle trailing operator', () => {
65
+ const result = parser.parseWithContext('status:done AND');
66
+
67
+ expect(result.success).toBe(false);
68
+ expect(result.tokens.length).toBeGreaterThanOrEqual(2);
69
+ });
70
+ });
71
+
72
+ describe('tokens', () => {
73
+ it('should always return tokens array', () => {
74
+ const validResult = parser.parseWithContext('status:done');
75
+ const invalidResult = parser.parseWithContext('status:');
76
+
77
+ expect(Array.isArray(validResult.tokens)).toBe(true);
78
+ expect(Array.isArray(invalidResult.tokens)).toBe(true);
79
+ });
80
+
81
+ it('should return interleaved tokens for compound query', () => {
82
+ const result = parser.parseWithContext('a:1 AND b:2 OR c:3');
83
+
84
+ expect(result.tokens).toHaveLength(5);
85
+ expect(result.tokens.map(t => t.type)).toEqual([
86
+ 'term',
87
+ 'operator',
88
+ 'term',
89
+ 'operator',
90
+ 'term'
91
+ ]);
92
+ });
93
+
94
+ it('should include position information', () => {
95
+ const result = parser.parseWithContext('status:done');
96
+
97
+ expect(result.tokens[0].startPosition).toBe(0);
98
+ expect(result.tokens[0].endPosition).toBe(11);
99
+ expect(result.tokens[0].raw).toBe('status:done');
100
+ });
101
+ });
102
+
103
+ describe('active token (cursor awareness)', () => {
104
+ it('should identify active token when cursor position provided', () => {
105
+ const result = parser.parseWithContext('status:done AND priority:high', {
106
+ cursorPosition: 5
107
+ });
108
+
109
+ expect(result.activeToken).toBeDefined();
110
+ expect(result.activeToken?.type).toBe('term');
111
+ expect(result.activeTokenIndex).toBe(0);
112
+ });
113
+
114
+ it('should identify cursor in operator', () => {
115
+ const result = parser.parseWithContext('status:done AND priority:high', {
116
+ cursorPosition: 13 // in "AND"
117
+ });
118
+
119
+ expect(result.activeToken?.type).toBe('operator');
120
+ expect(result.activeTokenIndex).toBe(1);
121
+ });
122
+
123
+ it('should return activeTokenIndex=-1 when no cursor provided', () => {
124
+ const result = parser.parseWithContext('status:done');
125
+
126
+ expect(result.activeTokenIndex).toBe(-1);
127
+ });
128
+ });
129
+
130
+ describe('structure analysis', () => {
131
+ it('should analyze simple query structure', () => {
132
+ const result = parser.parseWithContext('status:done');
133
+
134
+ expect(result.structure).toMatchObject({
135
+ depth: 1,
136
+ clauseCount: 1,
137
+ operatorCount: 0,
138
+ hasBalancedParentheses: true,
139
+ hasBalancedQuotes: true,
140
+ isComplete: true,
141
+ complexity: 'simple'
142
+ });
143
+ });
144
+
145
+ it('should count clauses correctly', () => {
146
+ const result = parser.parseWithContext('a:1 AND b:2 AND c:3');
147
+
148
+ expect(result.structure.clauseCount).toBe(3);
149
+ expect(result.structure.operatorCount).toBe(2);
150
+ });
151
+
152
+ it('should detect unbalanced parentheses', () => {
153
+ const result = parser.parseWithContext('(status:done');
154
+
155
+ expect(result.structure.hasBalancedParentheses).toBe(false);
156
+ expect(result.structure.isComplete).toBe(false);
157
+ });
158
+
159
+ it('should detect unbalanced quotes', () => {
160
+ const result = parser.parseWithContext('name:"John');
161
+
162
+ expect(result.structure.hasBalancedQuotes).toBe(false);
163
+ expect(result.structure.isComplete).toBe(false);
164
+ });
165
+
166
+ it('should calculate depth for nested query', () => {
167
+ const result = parser.parseWithContext('(a:1 AND (b:2 OR c:3))');
168
+
169
+ expect(result.structure.depth).toBe(2);
170
+ });
171
+
172
+ it('should extract referenced fields', () => {
173
+ const result = parser.parseWithContext('status:done AND priority:high');
174
+
175
+ expect(result.structure.referencedFields).toContain('status');
176
+ expect(result.structure.referencedFields).toContain('priority');
177
+ expect(result.structure.referencedFields).toHaveLength(2);
178
+ });
179
+
180
+ it('should not duplicate fields in referencedFields', () => {
181
+ const result = parser.parseWithContext('status:done OR status:pending');
182
+
183
+ expect(result.structure.referencedFields).toEqual(['status']);
184
+ });
185
+
186
+ it('should classify simple queries', () => {
187
+ expect(parser.parseWithContext('a:1').structure.complexity).toBe(
188
+ 'simple'
189
+ );
190
+ expect(parser.parseWithContext('a:1 AND b:2').structure.complexity).toBe(
191
+ 'simple'
192
+ );
193
+ });
194
+
195
+ it('should classify moderate queries', () => {
196
+ const result = parser.parseWithContext('a:1 AND b:2 AND c:3');
197
+
198
+ expect(result.structure.complexity).toBe('moderate');
199
+ });
200
+
201
+ it('should classify complex queries', () => {
202
+ const result = parser.parseWithContext(
203
+ 'a:1 AND b:2 AND c:3 AND d:4 AND e:5 AND f:6'
204
+ );
205
+
206
+ expect(result.structure.complexity).toBe('complex');
207
+ });
208
+
209
+ it('should classify deeply nested as complex', () => {
210
+ const result = parser.parseWithContext(
211
+ '(((a:1 AND b:2) OR c:3) AND d:4)'
212
+ );
213
+
214
+ // Depth > 3 should be complex
215
+ expect(result.structure.depth).toBeGreaterThanOrEqual(3);
216
+ });
217
+ });
218
+
219
+ describe('input preservation', () => {
220
+ it('should preserve original input', () => {
221
+ const input = 'status:done AND priority:high';
222
+ const result = parser.parseWithContext(input);
223
+
224
+ expect(result.input).toBe(input);
225
+ });
226
+ });
227
+
228
+ describe('edge cases', () => {
229
+ it('should handle empty input', () => {
230
+ const result = parser.parseWithContext('');
231
+
232
+ expect(result.success).toBe(false);
233
+ expect(result.tokens).toHaveLength(0);
234
+ expect(result.structure.clauseCount).toBe(0);
235
+ expect(result.structure.depth).toBe(0);
236
+ });
237
+
238
+ it('should handle whitespace-only input', () => {
239
+ const result = parser.parseWithContext(' ');
240
+
241
+ expect(result.success).toBe(false);
242
+ expect(result.tokens).toHaveLength(0);
243
+ });
244
+
245
+ it('should handle query with only logical operators', () => {
246
+ const result = parser.parseWithContext('AND OR');
247
+
248
+ expect(result.success).toBe(false);
249
+ // Should still tokenize the operators
250
+ expect(result.tokens.length).toBeGreaterThanOrEqual(1);
251
+ });
252
+
253
+ it('should handle very long query', () => {
254
+ const clauses = Array.from(
255
+ { length: 20 },
256
+ (_, i) => `field${i}:value${i}`
257
+ );
258
+ const query = clauses.join(' AND ');
259
+ const result = parser.parseWithContext(query);
260
+
261
+ expect(result.success).toBe(true);
262
+ expect(result.structure.clauseCount).toBe(20);
263
+ expect(result.structure.complexity).toBe('complex');
264
+ });
265
+ });
266
+
267
+ describe('integration with parser options', () => {
268
+ it('should respect caseInsensitiveFields option', () => {
269
+ const caseInsensitiveParser = new QueryParser({
270
+ caseInsensitiveFields: true
271
+ });
272
+
273
+ const result = caseInsensitiveParser.parseWithContext('STATUS:done');
274
+
275
+ expect(result.success).toBe(true);
276
+ expect(result.ast).toMatchObject({
277
+ field: 'status' // lowercased
278
+ });
279
+ });
280
+
281
+ it('should respect fieldMappings option', () => {
282
+ const mappedParser = new QueryParser({
283
+ fieldMappings: { s: 'status' }
284
+ });
285
+
286
+ const result = mappedParser.parseWithContext('s:done');
287
+
288
+ expect(result.success).toBe(true);
289
+ expect(result.ast).toMatchObject({
290
+ field: 'status' // mapped
291
+ });
292
+ });
293
+ });
294
+
295
+ describe('never throws', () => {
296
+ it('should never throw, even for malformed input', () => {
297
+ const badInputs = [
298
+ '',
299
+ ' ',
300
+ ':::',
301
+ '(((',
302
+ '))))',
303
+ 'AND AND AND',
304
+ '""""""',
305
+ '\n\t\r',
306
+ 'field:>>>>>',
307
+ 'a'.repeat(10000)
308
+ ];
309
+
310
+ for (const input of badInputs) {
311
+ expect(() => parser.parseWithContext(input)).not.toThrow();
312
+ }
313
+ });
314
+
315
+ it('should return a valid result object for any input', () => {
316
+ const result = parser.parseWithContext('totally {{invalid}} query!!!');
317
+
318
+ expect(result).toHaveProperty('success');
319
+ expect(result).toHaveProperty('input');
320
+ expect(result).toHaveProperty('tokens');
321
+ expect(result).toHaveProperty('structure');
322
+ expect(result).toHaveProperty('activeTokenIndex');
323
+ });
324
+ });
325
+ });