@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
package/dist/parser/types.d.ts
CHANGED
|
@@ -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,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
|
+
}
|