@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.
Files changed (39) hide show
  1. package/.cursor/BUGBOT.md +65 -2
  2. package/.husky/pre-commit +3 -3
  3. package/README.md +510 -1
  4. package/dist/index.d.ts +36 -3
  5. package/dist/index.js +20 -3
  6. package/dist/parser/index.d.ts +1 -0
  7. package/dist/parser/index.js +1 -0
  8. package/dist/parser/input-parser.d.ts +215 -0
  9. package/dist/parser/input-parser.js +493 -0
  10. package/dist/parser/parser.d.ts +114 -1
  11. package/dist/parser/parser.js +716 -0
  12. package/dist/parser/types.d.ts +432 -0
  13. package/dist/virtual-fields/index.d.ts +5 -0
  14. package/dist/virtual-fields/index.js +21 -0
  15. package/dist/virtual-fields/resolver.d.ts +17 -0
  16. package/dist/virtual-fields/resolver.js +107 -0
  17. package/dist/virtual-fields/types.d.ts +160 -0
  18. package/dist/virtual-fields/types.js +5 -0
  19. package/examples/qk-next/app/page.tsx +190 -86
  20. package/examples/qk-next/package.json +1 -1
  21. package/package.json +2 -2
  22. package/src/adapters/drizzle/index.ts +3 -3
  23. package/src/index.ts +77 -8
  24. package/src/parser/divergence.test.ts +357 -0
  25. package/src/parser/index.ts +2 -1
  26. package/src/parser/input-parser.test.ts +770 -0
  27. package/src/parser/input-parser.ts +697 -0
  28. package/src/parser/parse-with-context-suggestions.test.ts +360 -0
  29. package/src/parser/parse-with-context-validation.test.ts +447 -0
  30. package/src/parser/parse-with-context.test.ts +325 -0
  31. package/src/parser/parser.ts +872 -0
  32. package/src/parser/token-consistency.test.ts +341 -0
  33. package/src/parser/types.ts +545 -23
  34. package/src/virtual-fields/index.ts +6 -0
  35. package/src/virtual-fields/integration.test.ts +338 -0
  36. package/src/virtual-fields/resolver.ts +165 -0
  37. package/src/virtual-fields/types.ts +203 -0
  38. package/src/virtual-fields/virtual-fields.test.ts +831 -0
  39. package/examples/qk-next/pnpm-lock.yaml +0 -5623
@@ -65,4 +65,436 @@ export interface IQueryParser {
65
65
  * @returns true if the query is valid, false otherwise
66
66
  */
67
67
  validate(query: string): boolean;
68
+ /**
69
+ * Parse a query string with full context information
70
+ * @param query The query string to parse
71
+ * @param options Options for context parsing
72
+ * @returns Rich parse result with tokens, AST, structure, and more
73
+ */
74
+ parseWithContext(query: string, options?: IParseWithContextOptions): IQueryParseResult;
75
+ }
76
+ /**
77
+ * Options for parseWithContext
78
+ */
79
+ export interface IParseWithContextOptions {
80
+ /**
81
+ * Cursor position in the input (for cursor-aware features)
82
+ */
83
+ cursorPosition?: number;
84
+ /**
85
+ * Schema to validate fields against.
86
+ * Keys are field names, values describe the field type.
87
+ * When provided, enables field validation in the result.
88
+ */
89
+ schema?: Record<string, IFieldSchema>;
90
+ /**
91
+ * Security options for pre-validation.
92
+ * When provided, enables security pre-check in the result.
93
+ */
94
+ securityOptions?: ISecurityOptionsForContext;
95
+ }
96
+ /**
97
+ * Field schema definition for validation
98
+ */
99
+ export interface IFieldSchema {
100
+ /**
101
+ * The type of the field
102
+ */
103
+ type: 'string' | 'number' | 'boolean' | 'date' | 'array' | 'unknown';
104
+ /**
105
+ * Whether the field is required (for documentation purposes)
106
+ */
107
+ required?: boolean;
108
+ /**
109
+ * Allowed values for the field (for enums)
110
+ */
111
+ allowedValues?: Array<string | number | boolean>;
112
+ /**
113
+ * Human-readable description of the field
114
+ */
115
+ description?: string;
116
+ }
117
+ /**
118
+ * Security options for parseWithContext (subset of full security options)
119
+ */
120
+ export interface ISecurityOptionsForContext {
121
+ /**
122
+ * List of fields that are allowed to be queried
123
+ */
124
+ allowedFields?: string[];
125
+ /**
126
+ * List of fields that are denied from being queried
127
+ */
128
+ denyFields?: string[];
129
+ /**
130
+ * Maximum query depth allowed
131
+ */
132
+ maxQueryDepth?: number;
133
+ /**
134
+ * Maximum number of clauses allowed
135
+ */
136
+ maxClauseCount?: number;
137
+ /**
138
+ * Whether to allow dot notation in field names
139
+ */
140
+ allowDotNotation?: boolean;
141
+ }
142
+ /**
143
+ * Structural analysis of a query
144
+ */
145
+ export interface IQueryStructure {
146
+ /**
147
+ * Maximum nesting depth of the query
148
+ * e.g., "a:1 AND (b:2 OR c:3)" has depth 2
149
+ */
150
+ depth: number;
151
+ /**
152
+ * Total number of comparison clauses
153
+ * e.g., "a:1 AND b:2 OR c:3" has 3 clauses
154
+ */
155
+ clauseCount: number;
156
+ /**
157
+ * Number of logical operators (AND, OR, NOT)
158
+ */
159
+ operatorCount: number;
160
+ /**
161
+ * Whether parentheses are balanced
162
+ */
163
+ hasBalancedParentheses: boolean;
164
+ /**
165
+ * Whether all quotes are closed
166
+ */
167
+ hasBalancedQuotes: boolean;
168
+ /**
169
+ * Whether the query appears structurally complete
170
+ * (no trailing operators, no unclosed constructs)
171
+ */
172
+ isComplete: boolean;
173
+ /**
174
+ * List of fields referenced in the query
175
+ */
176
+ referencedFields: string[];
177
+ /**
178
+ * Complexity classification
179
+ */
180
+ complexity: 'simple' | 'moderate' | 'complex';
181
+ }
182
+ /**
183
+ * Error information when parsing fails
184
+ */
185
+ export interface IQueryParseErrorInfo {
186
+ /**
187
+ * Error message
188
+ */
189
+ message: string;
190
+ /**
191
+ * Position in the input where the error occurred (if known)
192
+ */
193
+ position?: number;
194
+ /**
195
+ * The portion of input that caused the error (if identifiable)
196
+ */
197
+ problematicText?: string;
198
+ }
199
+ /**
200
+ * Result of parseWithContext - provides rich context about the query
201
+ */
202
+ export interface IQueryParseResult {
203
+ /**
204
+ * Whether parsing succeeded
205
+ */
206
+ success: boolean;
207
+ /**
208
+ * The original input string
209
+ */
210
+ input: string;
211
+ /**
212
+ * Parsed AST (only present if success is true)
213
+ */
214
+ ast?: QueryExpression;
215
+ /**
216
+ * Error information (only present if success is false)
217
+ */
218
+ error?: IQueryParseErrorInfo;
219
+ /**
220
+ * Tokenized representation of the input
221
+ * Always present, even if parsing failed
222
+ */
223
+ tokens: IQueryToken[];
224
+ /**
225
+ * The token at the cursor position (if cursorPosition was provided)
226
+ */
227
+ activeToken?: IQueryToken;
228
+ /**
229
+ * Index of the active token (-1 if none)
230
+ */
231
+ activeTokenIndex: number;
232
+ /**
233
+ * Structural analysis of the query
234
+ */
235
+ structure: IQueryStructure;
236
+ /**
237
+ * Field validation results (only present if schema was provided in options)
238
+ */
239
+ fieldValidation?: IFieldValidationResult;
240
+ /**
241
+ * Security pre-check results (only present if securityOptions was provided)
242
+ */
243
+ security?: ISecurityCheckResult;
244
+ /**
245
+ * Autocomplete suggestions based on cursor position
246
+ * Only present if cursorPosition was provided in options
247
+ */
248
+ suggestions?: IAutocompleteSuggestions;
249
+ /**
250
+ * Error recovery hints (only present if parsing failed)
251
+ */
252
+ recovery?: IErrorRecovery;
253
+ }
254
+ /**
255
+ * Autocomplete suggestions based on cursor context
256
+ */
257
+ export interface IAutocompleteSuggestions {
258
+ /**
259
+ * The context where the cursor is positioned
260
+ */
261
+ context: 'field' | 'operator' | 'value' | 'logical_operator' | 'empty';
262
+ /**
263
+ * The current field being edited (if in value context)
264
+ */
265
+ currentField?: string;
266
+ /**
267
+ * Suggested field names (when in field context)
268
+ */
269
+ fields?: IFieldSuggestion[];
270
+ /**
271
+ * Suggested values (when in value context and schema has allowedValues)
272
+ */
273
+ values?: IValueSuggestion[];
274
+ /**
275
+ * Suggested operators (when in operator context)
276
+ */
277
+ operators?: IOperatorSuggestion[];
278
+ /**
279
+ * Suggested logical operators (AND, OR, NOT)
280
+ */
281
+ logicalOperators?: string[];
282
+ /**
283
+ * The text that would be replaced by the suggestion
284
+ */
285
+ replaceText?: string;
286
+ /**
287
+ * Position range that would be replaced
288
+ */
289
+ replaceRange?: {
290
+ start: number;
291
+ end: number;
292
+ };
293
+ }
294
+ /**
295
+ * A field suggestion for autocomplete
296
+ */
297
+ export interface IFieldSuggestion {
298
+ /**
299
+ * The field name
300
+ */
301
+ field: string;
302
+ /**
303
+ * The field type (from schema)
304
+ */
305
+ type?: string;
306
+ /**
307
+ * Description of the field (from schema)
308
+ */
309
+ description?: string;
310
+ /**
311
+ * Match score (higher is better match)
312
+ */
313
+ score: number;
314
+ }
315
+ /**
316
+ * A value suggestion for autocomplete
317
+ */
318
+ export interface IValueSuggestion {
319
+ /**
320
+ * The suggested value
321
+ */
322
+ value: string | number | boolean;
323
+ /**
324
+ * Display label (may differ from value)
325
+ */
326
+ label?: string;
327
+ /**
328
+ * Match score (higher is better match)
329
+ */
330
+ score: number;
331
+ }
332
+ /**
333
+ * An operator suggestion for autocomplete
334
+ */
335
+ export interface IOperatorSuggestion {
336
+ /**
337
+ * The operator symbol
338
+ */
339
+ operator: string;
340
+ /**
341
+ * Human-readable description
342
+ */
343
+ description: string;
344
+ /**
345
+ * Whether this operator is applicable to the current field type
346
+ */
347
+ applicable: boolean;
348
+ }
349
+ /**
350
+ * Error recovery suggestions
351
+ */
352
+ export interface IErrorRecovery {
353
+ /**
354
+ * The type of issue detected
355
+ */
356
+ issue: 'unclosed_quote' | 'unclosed_parenthesis' | 'trailing_operator' | 'missing_value' | 'missing_operator' | 'syntax_error';
357
+ /**
358
+ * Human-readable description of the issue
359
+ */
360
+ message: string;
361
+ /**
362
+ * Suggested fix description
363
+ */
364
+ suggestion: string;
365
+ /**
366
+ * The corrected query (if auto-fix is possible)
367
+ */
368
+ autofix?: string;
369
+ /**
370
+ * Position where the issue was detected
371
+ */
372
+ position?: number;
373
+ }
374
+ /**
375
+ * Result of field validation against schema
376
+ */
377
+ export interface IFieldValidationResult {
378
+ /**
379
+ * Whether all fields passed validation
380
+ */
381
+ valid: boolean;
382
+ /**
383
+ * Validation details for each field
384
+ */
385
+ fields: IFieldValidationDetail[];
386
+ /**
387
+ * Fields that were referenced but not found in schema
388
+ */
389
+ unknownFields: string[];
390
+ }
391
+ /**
392
+ * Validation detail for a single field
393
+ */
394
+ export interface IFieldValidationDetail {
395
+ /**
396
+ * The field name
397
+ */
398
+ field: string;
399
+ /**
400
+ * Whether this field is valid
401
+ */
402
+ valid: boolean;
403
+ /**
404
+ * The expected type from schema (if known)
405
+ */
406
+ expectedType?: string;
407
+ /**
408
+ * Reason for validation failure (if invalid)
409
+ */
410
+ reason?: 'unknown_field' | 'type_mismatch' | 'invalid_value' | 'denied';
411
+ /**
412
+ * Suggested correction (for typos)
413
+ */
414
+ suggestion?: string;
415
+ /**
416
+ * Allowed values (if field has enum constraint)
417
+ */
418
+ allowedValues?: Array<string | number | boolean>;
419
+ }
420
+ /**
421
+ * Result of security pre-check
422
+ */
423
+ export interface ISecurityCheckResult {
424
+ /**
425
+ * Whether the query passes all security checks
426
+ */
427
+ passed: boolean;
428
+ /**
429
+ * List of security violations found
430
+ */
431
+ violations: ISecurityViolation[];
432
+ /**
433
+ * Warnings that don't block execution but should be noted
434
+ */
435
+ warnings: ISecurityWarning[];
436
+ }
437
+ /**
438
+ * A security violation that blocks query execution
439
+ */
440
+ export interface ISecurityViolation {
441
+ /**
442
+ * Type of violation
443
+ */
444
+ type: 'denied_field' | 'depth_exceeded' | 'clause_limit' | 'dot_notation' | 'field_not_allowed';
445
+ /**
446
+ * Human-readable message
447
+ */
448
+ message: string;
449
+ /**
450
+ * The field that caused the violation (if applicable)
451
+ */
452
+ field?: string;
453
+ }
454
+ /**
455
+ * A security warning (doesn't block execution)
456
+ */
457
+ export interface ISecurityWarning {
458
+ /**
459
+ * Type of warning
460
+ */
461
+ type: 'approaching_depth_limit' | 'approaching_clause_limit' | 'complex_query';
462
+ /**
463
+ * Human-readable message
464
+ */
465
+ message: string;
466
+ /**
467
+ * Current value
468
+ */
469
+ current?: number;
470
+ /**
471
+ * Limit value
472
+ */
473
+ limit?: number;
474
+ }
475
+ /**
476
+ * A token in the parsed query (term or operator)
477
+ */
478
+ export type IQueryToken = IQueryTermToken | IQueryOperatorToken;
479
+ /**
480
+ * A term token (field:value pair or bare value)
481
+ */
482
+ export interface IQueryTermToken {
483
+ type: 'term';
484
+ key: string | null;
485
+ operator: string | null;
486
+ value: string | null;
487
+ startPosition: number;
488
+ endPosition: number;
489
+ raw: string;
490
+ }
491
+ /**
492
+ * A logical operator token (AND, OR, NOT)
493
+ */
494
+ export interface IQueryOperatorToken {
495
+ type: 'operator';
496
+ operator: 'AND' | 'OR' | 'NOT';
497
+ startPosition: number;
498
+ endPosition: number;
499
+ raw: string;
68
500
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Virtual Fields module exports
3
+ */
4
+ export * from './types';
5
+ export * from './resolver';
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /**
3
+ * Virtual Fields module exports
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
17
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ __exportStar(require("./types"), exports);
21
+ __exportStar(require("./resolver"), exports);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Virtual field resolution logic
3
+ */
4
+ import { QueryExpression } from '../parser/types';
5
+ import { IQueryContext, VirtualFieldsConfig } from './types';
6
+ /**
7
+ * Resolve virtual fields in a query expression.
8
+ * Recursively walks the AST and replaces virtual field references with
9
+ * their resolved expressions based on the provided context.
10
+ *
11
+ * @param expr - The query expression to resolve
12
+ * @param virtualFields - Virtual field configuration
13
+ * @param context - Runtime context for resolution
14
+ * @returns The resolved query expression
15
+ * @throws {QueryParseError} If a virtual field value is invalid or operator is not allowed
16
+ */
17
+ export declare function resolveVirtualFields<TSchema extends Record<string, object>, TContext extends IQueryContext>(expr: QueryExpression, virtualFields: VirtualFieldsConfig<TSchema, TContext>, context: TContext): QueryExpression;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ /**
3
+ * Virtual field resolution logic
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveVirtualFields = resolveVirtualFields;
7
+ const parser_1 = require("../parser/parser");
8
+ /**
9
+ * Resolve virtual fields in a query expression.
10
+ * Recursively walks the AST and replaces virtual field references with
11
+ * their resolved expressions based on the provided context.
12
+ *
13
+ * @param expr - The query expression to resolve
14
+ * @param virtualFields - Virtual field configuration
15
+ * @param context - Runtime context for resolution
16
+ * @returns The resolved query expression
17
+ * @throws {QueryParseError} If a virtual field value is invalid or operator is not allowed
18
+ */
19
+ function resolveVirtualFields(expr, virtualFields, context) {
20
+ // Base case: comparison expression
21
+ if (expr.type === 'comparison') {
22
+ return resolveComparisonExpression(expr, virtualFields, context);
23
+ }
24
+ // Recursive case: logical expression
25
+ if (expr.type === 'logical') {
26
+ return resolveLogicalExpression(expr, virtualFields, context);
27
+ }
28
+ // Unknown expression type, return as-is
29
+ return expr;
30
+ }
31
+ /**
32
+ * Resolve a comparison expression.
33
+ * If the field is a virtual field, resolve it using the configuration.
34
+ * Otherwise, return the expression unchanged.
35
+ */
36
+ function resolveComparisonExpression(expr, virtualFields, context) {
37
+ const fieldName = expr.field;
38
+ const virtualFieldDef = virtualFields[fieldName];
39
+ // Not a virtual field, return as-is
40
+ if (!virtualFieldDef) {
41
+ return expr;
42
+ }
43
+ // Validate the value is a string (virtual fields require string values)
44
+ if (typeof expr.value !== 'string') {
45
+ const valueType = Array.isArray(expr.value)
46
+ ? `array (${JSON.stringify(expr.value)})`
47
+ : typeof expr.value === 'object'
48
+ ? `object (${JSON.stringify(expr.value)})`
49
+ : typeof expr.value;
50
+ throw new parser_1.QueryParseError(`Virtual field "${fieldName}" requires a string value, got ${valueType}`);
51
+ }
52
+ const value = expr.value;
53
+ // Validate the value is in allowedValues
54
+ if (!virtualFieldDef.allowedValues.includes(value)) {
55
+ const allowedValuesStr = virtualFieldDef.allowedValues
56
+ .map(v => `"${v}"`)
57
+ .join(', ');
58
+ throw new parser_1.QueryParseError(`Invalid value "${value}" for virtual field "${fieldName}". Allowed values: ${allowedValuesStr}`);
59
+ }
60
+ // Validate operator usage
61
+ const allowOperators = virtualFieldDef.allowOperators ?? false;
62
+ if (!allowOperators && expr.operator !== '==') {
63
+ throw new parser_1.QueryParseError(`Virtual field "${fieldName}" does not allow comparison operators. Only equality (":") is permitted.`);
64
+ }
65
+ // Create the input for the resolver
66
+ const input = {
67
+ field: fieldName,
68
+ operator: expr.operator,
69
+ value: value
70
+ };
71
+ // Create the helpers object with type-safe fields() helper
72
+ // The fields() method is generic at the method level, allowing TypeScript to
73
+ // infer TValues from the mapping object at call-time without needing type assertions
74
+ const helpers = {
75
+ fields: (mapping) => {
76
+ // Validate that all keys in the mapping are in the virtual field's allowed values
77
+ const mappingKeys = Object.keys(mapping);
78
+ const allowedValues = virtualFieldDef.allowedValues;
79
+ for (const key of mappingKeys) {
80
+ if (!allowedValues.includes(key)) {
81
+ throw new parser_1.QueryParseError(`Invalid key "${key}" in field mapping for virtual field "${fieldName}". ` +
82
+ `Allowed keys are: ${allowedValues.map(v => `"${v}"`).join(', ')}`);
83
+ }
84
+ }
85
+ // Runtime: this is just an identity function
86
+ // Compile-time: TypeScript validates the mapping structure
87
+ return mapping;
88
+ }
89
+ };
90
+ // Resolve the virtual field - no type assertions needed!
91
+ const resolved = virtualFieldDef.resolve(input, context, helpers);
92
+ return resolved;
93
+ }
94
+ /**
95
+ * Resolve a logical expression.
96
+ * Recursively resolve both left and right sides.
97
+ */
98
+ function resolveLogicalExpression(expr, virtualFields, context) {
99
+ return {
100
+ type: 'logical',
101
+ operator: expr.operator,
102
+ left: resolveVirtualFields(expr.left, virtualFields, context),
103
+ right: expr.right
104
+ ? resolveVirtualFields(expr.right, virtualFields, context)
105
+ : undefined
106
+ };
107
+ }