@gblikas/querykit 0.2.0 → 0.4.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/.cursor/BUGBOT.md +65 -2
- package/.husky/pre-commit +3 -3
- package/README.md +510 -1
- package/dist/index.d.ts +36 -3
- package/dist/index.js +20 -3
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.js +1 -0
- package/dist/parser/input-parser.d.ts +215 -0
- package/dist/parser/input-parser.js +493 -0
- package/dist/parser/parser.d.ts +114 -1
- package/dist/parser/parser.js +716 -0
- package/dist/parser/types.d.ts +432 -0
- package/dist/virtual-fields/index.d.ts +5 -0
- package/dist/virtual-fields/index.js +21 -0
- package/dist/virtual-fields/resolver.d.ts +17 -0
- package/dist/virtual-fields/resolver.js +107 -0
- package/dist/virtual-fields/types.d.ts +160 -0
- package/dist/virtual-fields/types.js +5 -0
- package/examples/qk-next/app/page.tsx +190 -86
- package/examples/qk-next/package.json +1 -1
- package/package.json +2 -2
- package/src/adapters/drizzle/index.ts +3 -3
- package/src/index.ts +77 -8
- package/src/parser/divergence.test.ts +357 -0
- package/src/parser/index.ts +2 -1
- package/src/parser/input-parser.test.ts +770 -0
- package/src/parser/input-parser.ts +697 -0
- package/src/parser/parse-with-context-suggestions.test.ts +360 -0
- package/src/parser/parse-with-context-validation.test.ts +447 -0
- package/src/parser/parse-with-context.test.ts +325 -0
- package/src/parser/parser.ts +872 -0
- package/src/parser/token-consistency.test.ts +341 -0
- package/src/parser/types.ts +545 -23
- package/src/virtual-fields/index.ts +6 -0
- package/src/virtual-fields/integration.test.ts +338 -0
- package/src/virtual-fields/resolver.ts +165 -0
- package/src/virtual-fields/types.ts +203 -0
- package/src/virtual-fields/virtual-fields.test.ts +831 -0
- package/examples/qk-next/pnpm-lock.yaml +0 -5623
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests to verify that parseWithContext returns the same token information
|
|
3
|
+
* as the standalone input parser (parseQueryTokens).
|
|
4
|
+
*
|
|
5
|
+
* This ensures the integration is correct and both approaches produce
|
|
6
|
+
* consistent results.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { QueryParser } from './parser';
|
|
10
|
+
import { parseQueryTokens, parseQueryInput } from './input-parser';
|
|
11
|
+
|
|
12
|
+
describe('Token Consistency: parseWithContext vs input parser', () => {
|
|
13
|
+
const parser = new QueryParser();
|
|
14
|
+
|
|
15
|
+
describe('token count consistency', () => {
|
|
16
|
+
const testCases = [
|
|
17
|
+
'status:done',
|
|
18
|
+
'status:done AND priority:high',
|
|
19
|
+
'a:1 OR b:2 OR c:3',
|
|
20
|
+
'status:done AND priority:high OR assigned:me',
|
|
21
|
+
'(a:1 AND b:2)',
|
|
22
|
+
'status:',
|
|
23
|
+
'status:d',
|
|
24
|
+
'',
|
|
25
|
+
' ',
|
|
26
|
+
'hello world',
|
|
27
|
+
'-status:active',
|
|
28
|
+
'name:"John Doe"'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
testCases.forEach(input => {
|
|
32
|
+
it(`should have same token count for: "${input}"`, () => {
|
|
33
|
+
const contextResult = parser.parseWithContext(input);
|
|
34
|
+
const inputParserResult = parseQueryTokens(input);
|
|
35
|
+
|
|
36
|
+
expect(contextResult.tokens.length).toBe(
|
|
37
|
+
inputParserResult.tokens.length
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('token content consistency', () => {
|
|
44
|
+
it('should have identical term tokens', () => {
|
|
45
|
+
const input = 'status:done AND priority:high';
|
|
46
|
+
|
|
47
|
+
const contextResult = parser.parseWithContext(input);
|
|
48
|
+
const inputParserResult = parseQueryTokens(input);
|
|
49
|
+
|
|
50
|
+
// Compare each token
|
|
51
|
+
for (let i = 0; i < contextResult.tokens.length; i++) {
|
|
52
|
+
const ctxToken = contextResult.tokens[i];
|
|
53
|
+
const ipToken = inputParserResult.tokens[i];
|
|
54
|
+
|
|
55
|
+
expect(ctxToken.type).toBe(ipToken.type);
|
|
56
|
+
expect(ctxToken.startPosition).toBe(ipToken.startPosition);
|
|
57
|
+
expect(ctxToken.endPosition).toBe(ipToken.endPosition);
|
|
58
|
+
expect(ctxToken.raw).toBe(ipToken.raw);
|
|
59
|
+
|
|
60
|
+
if (ctxToken.type === 'term' && ipToken.type === 'term') {
|
|
61
|
+
expect(ctxToken.key).toBe(ipToken.key);
|
|
62
|
+
expect(ctxToken.operator).toBe(ipToken.operator);
|
|
63
|
+
expect(ctxToken.value).toBe(ipToken.value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (ctxToken.type === 'operator' && ipToken.type === 'operator') {
|
|
67
|
+
expect(ctxToken.operator).toBe(ipToken.operator);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should have identical operator tokens', () => {
|
|
73
|
+
const input = 'a:1 AND b:2 OR c:3 NOT d:4';
|
|
74
|
+
|
|
75
|
+
const contextResult = parser.parseWithContext(input);
|
|
76
|
+
const inputParserResult = parseQueryTokens(input);
|
|
77
|
+
|
|
78
|
+
const ctxOperators = contextResult.tokens.filter(
|
|
79
|
+
t => t.type === 'operator'
|
|
80
|
+
);
|
|
81
|
+
const ipOperators = inputParserResult.tokens.filter(
|
|
82
|
+
t => t.type === 'operator'
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(ctxOperators.length).toBe(ipOperators.length);
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < ctxOperators.length; i++) {
|
|
88
|
+
expect(ctxOperators[i]).toEqual(ipOperators[i]);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('position consistency', () => {
|
|
94
|
+
const testCases = [
|
|
95
|
+
'status:done',
|
|
96
|
+
'a:1 AND b:2',
|
|
97
|
+
'name:"hello world"',
|
|
98
|
+
'(status:active OR status:pending)',
|
|
99
|
+
'priority:>5 AND count:<=10'
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
testCases.forEach(input => {
|
|
103
|
+
it(`should have identical positions for: "${input}"`, () => {
|
|
104
|
+
const contextResult = parser.parseWithContext(input);
|
|
105
|
+
const inputParserResult = parseQueryTokens(input);
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < contextResult.tokens.length; i++) {
|
|
108
|
+
const ctxToken = contextResult.tokens[i];
|
|
109
|
+
const ipToken = inputParserResult.tokens[i];
|
|
110
|
+
|
|
111
|
+
expect(ctxToken.startPosition).toBe(ipToken.startPosition);
|
|
112
|
+
expect(ctxToken.endPosition).toBe(ipToken.endPosition);
|
|
113
|
+
|
|
114
|
+
// Verify raw text matches the slice
|
|
115
|
+
expect(ctxToken.raw).toBe(
|
|
116
|
+
input.substring(ctxToken.startPosition, ctxToken.endPosition)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('cursor/active token consistency', () => {
|
|
124
|
+
it('should identify same active token at cursor position', () => {
|
|
125
|
+
const input = 'status:done AND priority:high';
|
|
126
|
+
const cursorPosition = 5; // in "status"
|
|
127
|
+
|
|
128
|
+
const contextResult = parser.parseWithContext(input, { cursorPosition });
|
|
129
|
+
const inputParserResult = parseQueryTokens(input, cursorPosition);
|
|
130
|
+
|
|
131
|
+
// Both should identify the same active token
|
|
132
|
+
expect(contextResult.activeTokenIndex).toBe(
|
|
133
|
+
inputParserResult.activeTokenIndex
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (contextResult.activeToken && inputParserResult.activeToken) {
|
|
137
|
+
expect(contextResult.activeToken.type).toBe(
|
|
138
|
+
inputParserResult.activeToken.type
|
|
139
|
+
);
|
|
140
|
+
expect(contextResult.activeToken.startPosition).toBe(
|
|
141
|
+
inputParserResult.activeToken.startPosition
|
|
142
|
+
);
|
|
143
|
+
expect(contextResult.activeToken.endPosition).toBe(
|
|
144
|
+
inputParserResult.activeToken.endPosition
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should identify active token in operator', () => {
|
|
150
|
+
const input = 'status:done AND priority:high';
|
|
151
|
+
const cursorPosition = 13; // in "AND"
|
|
152
|
+
|
|
153
|
+
const contextResult = parser.parseWithContext(input, { cursorPosition });
|
|
154
|
+
const inputParserResult = parseQueryTokens(input, cursorPosition);
|
|
155
|
+
|
|
156
|
+
expect(contextResult.activeToken?.type).toBe('operator');
|
|
157
|
+
expect(inputParserResult.activeToken?.type).toBe('operator');
|
|
158
|
+
expect(contextResult.activeTokenIndex).toBe(
|
|
159
|
+
inputParserResult.activeTokenIndex
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should return same activeTokenIndex for various positions', () => {
|
|
164
|
+
const input = 'a:1 AND b:2 OR c:3';
|
|
165
|
+
const positions = [0, 1, 2, 4, 5, 8, 9, 12, 15, 18];
|
|
166
|
+
|
|
167
|
+
for (const pos of positions) {
|
|
168
|
+
const contextResult = parser.parseWithContext(input, {
|
|
169
|
+
cursorPosition: pos
|
|
170
|
+
});
|
|
171
|
+
const inputParserResult = parseQueryTokens(input, pos);
|
|
172
|
+
|
|
173
|
+
expect(contextResult.activeTokenIndex).toBe(
|
|
174
|
+
inputParserResult.activeTokenIndex
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('parseQueryInput term consistency', () => {
|
|
181
|
+
it('should have consistent term information with parseQueryInput', () => {
|
|
182
|
+
const input = 'status:done AND priority:high';
|
|
183
|
+
|
|
184
|
+
const contextResult = parser.parseWithContext(input);
|
|
185
|
+
const inputResult = parseQueryInput(input);
|
|
186
|
+
|
|
187
|
+
// Get term tokens from parseWithContext
|
|
188
|
+
const ctxTerms = contextResult.tokens.filter(t => t.type === 'term');
|
|
189
|
+
|
|
190
|
+
// Compare with parseQueryInput terms
|
|
191
|
+
expect(ctxTerms.length).toBe(inputResult.terms.length);
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < ctxTerms.length; i++) {
|
|
194
|
+
const ctxTerm = ctxTerms[i];
|
|
195
|
+
const ipTerm = inputResult.terms[i];
|
|
196
|
+
|
|
197
|
+
if (ctxTerm.type === 'term') {
|
|
198
|
+
expect(ctxTerm.key).toBe(ipTerm.key);
|
|
199
|
+
expect(ctxTerm.value).toBe(ipTerm.value);
|
|
200
|
+
expect(ctxTerm.operator).toBe(ipTerm.operator);
|
|
201
|
+
expect(ctxTerm.startPosition).toBe(ipTerm.startPosition);
|
|
202
|
+
expect(ctxTerm.endPosition).toBe(ipTerm.endPosition);
|
|
203
|
+
expect(ctxTerm.raw).toBe(ipTerm.raw);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should have consistent logical operator information', () => {
|
|
209
|
+
const input = 'a:1 AND b:2 OR c:3';
|
|
210
|
+
|
|
211
|
+
const contextResult = parser.parseWithContext(input);
|
|
212
|
+
const inputResult = parseQueryInput(input);
|
|
213
|
+
|
|
214
|
+
// Get operator tokens from parseWithContext
|
|
215
|
+
const ctxOps = contextResult.tokens.filter(t => t.type === 'operator');
|
|
216
|
+
|
|
217
|
+
// Compare with parseQueryInput logical operators
|
|
218
|
+
expect(ctxOps.length).toBe(inputResult.logicalOperators.length);
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < ctxOps.length; i++) {
|
|
221
|
+
const ctxOp = ctxOps[i];
|
|
222
|
+
const ipOp = inputResult.logicalOperators[i];
|
|
223
|
+
|
|
224
|
+
if (ctxOp.type === 'operator') {
|
|
225
|
+
expect(ctxOp.operator).toBe(ipOp.operator);
|
|
226
|
+
expect(ctxOp.startPosition).toBe(ipOp.position);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('edge case consistency', () => {
|
|
233
|
+
it('should handle empty input consistently', () => {
|
|
234
|
+
const input = '';
|
|
235
|
+
|
|
236
|
+
const contextResult = parser.parseWithContext(input);
|
|
237
|
+
const inputParserResult = parseQueryTokens(input);
|
|
238
|
+
|
|
239
|
+
expect(contextResult.tokens).toEqual(inputParserResult.tokens);
|
|
240
|
+
expect(contextResult.tokens.length).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should handle incomplete input consistently', () => {
|
|
244
|
+
const input = 'status:';
|
|
245
|
+
|
|
246
|
+
const contextResult = parser.parseWithContext(input);
|
|
247
|
+
const inputParserResult = parseQueryTokens(input);
|
|
248
|
+
|
|
249
|
+
expect(contextResult.tokens.length).toBe(inputParserResult.tokens.length);
|
|
250
|
+
expect(contextResult.tokens[0]).toEqual(inputParserResult.tokens[0]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should handle invalid input consistently', () => {
|
|
254
|
+
const input = 'status:done AND';
|
|
255
|
+
|
|
256
|
+
const contextResult = parser.parseWithContext(input);
|
|
257
|
+
const inputParserResult = parseQueryTokens(input);
|
|
258
|
+
|
|
259
|
+
expect(contextResult.tokens.length).toBe(inputParserResult.tokens.length);
|
|
260
|
+
|
|
261
|
+
// Both should have the same tokens even though parsing failed
|
|
262
|
+
for (let i = 0; i < contextResult.tokens.length; i++) {
|
|
263
|
+
expect(contextResult.tokens[i]).toEqual(inputParserResult.tokens[i]);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should handle quoted values consistently', () => {
|
|
268
|
+
const input = 'name:"John Doe" AND status:active';
|
|
269
|
+
|
|
270
|
+
const contextResult = parser.parseWithContext(input);
|
|
271
|
+
const inputParserResult = parseQueryTokens(input);
|
|
272
|
+
|
|
273
|
+
expect(contextResult.tokens.length).toBe(inputParserResult.tokens.length);
|
|
274
|
+
|
|
275
|
+
for (let i = 0; i < contextResult.tokens.length; i++) {
|
|
276
|
+
expect(contextResult.tokens[i]).toEqual(inputParserResult.tokens[i]);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should handle comparison operators consistently', () => {
|
|
281
|
+
const input = 'priority:>5 AND count:<=10';
|
|
282
|
+
|
|
283
|
+
const contextResult = parser.parseWithContext(input);
|
|
284
|
+
const inputParserResult = parseQueryTokens(input);
|
|
285
|
+
|
|
286
|
+
expect(contextResult.tokens.length).toBe(inputParserResult.tokens.length);
|
|
287
|
+
|
|
288
|
+
for (let i = 0; i < contextResult.tokens.length; i++) {
|
|
289
|
+
expect(contextResult.tokens[i]).toEqual(inputParserResult.tokens[i]);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should handle negation consistently', () => {
|
|
294
|
+
const input = '-status:inactive';
|
|
295
|
+
|
|
296
|
+
const contextResult = parser.parseWithContext(input);
|
|
297
|
+
const inputParserResult = parseQueryTokens(input);
|
|
298
|
+
|
|
299
|
+
expect(contextResult.tokens.length).toBe(inputParserResult.tokens.length);
|
|
300
|
+
expect(contextResult.tokens[0]).toEqual(inputParserResult.tokens[0]);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('structure vs parseQueryInput consistency', () => {
|
|
305
|
+
it('should have consistent isComplete', () => {
|
|
306
|
+
const testCases = [
|
|
307
|
+
{ input: 'status:done', expectedComplete: true },
|
|
308
|
+
{ input: 'status:', expectedComplete: false },
|
|
309
|
+
{ input: 'status:done AND', expectedComplete: false },
|
|
310
|
+
{ input: '(status:done', expectedComplete: false },
|
|
311
|
+
{ input: 'name:"John', expectedComplete: false }
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
for (const { input, expectedComplete } of testCases) {
|
|
315
|
+
const contextResult = parser.parseWithContext(input);
|
|
316
|
+
|
|
317
|
+
expect(contextResult.structure.isComplete).toBe(expectedComplete);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should have consistent referenced fields', () => {
|
|
322
|
+
const input = 'status:done AND priority:high OR status:pending';
|
|
323
|
+
|
|
324
|
+
const contextResult = parser.parseWithContext(input);
|
|
325
|
+
const inputResult = parseQueryInput(input);
|
|
326
|
+
|
|
327
|
+
// Get unique fields from input parser
|
|
328
|
+
const ipFields = [
|
|
329
|
+
...new Set(
|
|
330
|
+
inputResult.terms
|
|
331
|
+
.filter(t => t.key !== null)
|
|
332
|
+
.map(t => t.key as string)
|
|
333
|
+
)
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
expect(contextResult.structure.referencedFields.sort()).toEqual(
|
|
337
|
+
ipFields.sort()
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|