@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
package/.cursor/BUGBOT.md CHANGED
@@ -1,21 +1,84 @@
1
1
  # Project review guidelines
2
2
 
3
- Bellow is a list of generally accepted best-practices to prevent bugs in projects. Not all guidelines may apply to the current project; please make sure to read any README.md files in order to correlate goals for bug detection.
3
+ Below is a list of generally accepted best-practices to prevent bugs in QueryKit. Not all guidelines may apply to every component; please make sure to read the README.md for context on the project's goals.
4
4
 
5
5
  ## Security focus areas
6
6
 
7
7
  - Validate user input in API endpoints
8
8
  - Check for SQL injection vulnerabilities in database queries
9
9
  - Ensure proper authentication on protected routes
10
+ - Validate query inputs using `parseWithContext` with security options
11
+ - Use `allowedFields` and `denyFields` to restrict queryable fields
12
+ - Set `maxQueryDepth` and `maxClauseCount` to prevent DoS attacks
13
+
14
+ ### Query parsing security
15
+
16
+ When using the input parser or `parseWithContext`:
17
+
18
+ 1. **Never trust user-provided queries** - Always validate with security options:
19
+ ```typescript
20
+ const result = parser.parseWithContext(userQuery, {
21
+ securityOptions: {
22
+ allowedFields: ['name', 'status', 'priority'],
23
+ denyFields: ['password', 'secret'],
24
+ maxQueryDepth: 5,
25
+ maxClauseCount: 20
26
+ }
27
+ });
28
+
29
+ if (!result.security?.passed) {
30
+ // Reject query - contains violations
31
+ }
32
+ ```
33
+
34
+ 2. **Schema validation** - Use schema to detect typos and invalid fields early:
35
+ ```typescript
36
+ const result = parser.parseWithContext(userQuery, { schema });
37
+ if (!result.fieldValidation?.valid) {
38
+ // Show user-friendly error with suggestions
39
+ }
40
+ ```
41
+
42
+ 3. **Input parser limitations** - The input parser (`parseQueryInput`, `parseQueryTokens`) is regex-based for performance. It may accept inputs that the main parser rejects. Always validate with `parseWithContext` or `parser.parse()` before executing queries.
10
43
 
11
44
  ## Architecture patterns
12
45
 
13
46
  - Use dependency injection for services
14
47
  - Follow the repository pattern for data access
15
48
  - Implement proper error handling with custom error classes
49
+ - Parser components follow Single Responsibility Principle:
50
+ - `input-parser.ts` - Fast, regex-based tokenization for UI feedback
51
+ - `parser.ts` - Full Liqe-based parsing with AST generation
52
+ - `parseWithContext` - Orchestrates both for rich context
53
+
54
+ ### Parser architecture
55
+
56
+ The parsing system has two tiers:
57
+
58
+ 1. **Input Parser** (`parseQueryInput`, `parseQueryTokens`)
59
+ - Purpose: Real-time UI feedback (highlighting, cursor context)
60
+ - Performance: O(n) regex-based, no AST generation
61
+ - Error handling: Best-effort, never throws
62
+ - Use for: Search bar highlighting, autocomplete triggering
63
+
64
+ 2. **Query Parser** (`parser.parse`, `parseWithContext`)
65
+ - Purpose: Query validation and execution
66
+ - Performance: Full Liqe grammar parsing
67
+ - Error handling: Strict validation, detailed error messages
68
+ - Use for: Query execution, security validation
16
69
 
17
70
  ## Common issues
18
71
 
19
72
  - Memory leaks in React components (check useEffect cleanup)
20
73
  - Missing error boundaries in UI components
21
- - Inconsistent naming conventions (use camelCase for functions)
74
+ - Inconsistent naming conventions (use camelCase for functions)
75
+ - Not checking `result.success` before accessing `result.ast`
76
+ - Using input parser for security validation (use `parseWithContext` instead)
77
+ - Forgetting to provide `cursorPosition` when autocomplete is needed
78
+
79
+ ## Testing guidelines
80
+
81
+ - All parser features require co-located tests
82
+ - Use divergence tests to document differences between input parser and main parser
83
+ - Token consistency tests verify `parseWithContext` tokens match `parseQueryTokens`
84
+ - Security tests should cover field restrictions, depth limits, and value sanitization
package/.husky/pre-commit CHANGED
@@ -14,16 +14,16 @@ echo "Running pre-commit checks..."
14
14
 
15
15
  # Run lint-staged to process only changed files
16
16
  echo "🔍 Running lint-staged..."
17
- pnpm lint-staged || { echo "❌ Linting failed"; exit 1; }
17
+ npx lint-staged || { echo "❌ Linting failed"; exit 1; }
18
18
 
19
19
  # Check TypeScript compilation
20
20
  echo "🔍 Checking TypeScript compilation..."
21
- pnpm exec tsc --noEmit || { echo "❌ TypeScript check failed"; exit 1; }
21
+ npx tsc --noEmit || { echo "❌ TypeScript check failed"; exit 1; }
22
22
 
23
23
  # Run tests
24
24
  if [ "$RUN_TESTS" = "true" ]; then
25
25
  echo "🧪 Running tests..."
26
- pnpm test || { echo "❌ Tests failed"; exit 1; }
26
+ npm test || { echo "❌ Tests failed"; exit 1; }
27
27
  fi
28
28
 
29
29
  # If everything passes, allow the commit
package/README.md CHANGED
@@ -275,6 +275,511 @@ const publicSearchKit = createQueryKit({
275
275
  });
276
276
  ```
277
277
 
278
+ ## Input Parsing for Search UIs
279
+
280
+ QueryKit provides utilities for building rich search bar experiences with real-time feedback, including key:value highlighting, autocomplete suggestions, and error recovery hints.
281
+
282
+ ### Real-Time Token Parsing
283
+
284
+ Use `parseQueryInput` and `parseQueryTokens` for lightweight, real-time parsing as users type:
285
+
286
+ ```typescript
287
+ import { parseQueryInput, parseQueryTokens } from '@gblikas/querykit';
288
+
289
+ // Parse input to get terms and cursor context
290
+ const input = 'status:done AND priority:';
291
+ const result = parseQueryInput(input, { cursorPosition: 25 });
292
+
293
+ // result.terms contains parsed terms:
294
+ // [{ key: 'status', value: 'done', ... }, { key: 'priority', value: null, ... }]
295
+
296
+ // result.cursorContext tells you where the cursor is: 'key', 'value', or 'operator'
297
+ console.log(result.cursorContext); // 'value' (cursor is after 'priority:')
298
+
299
+ // Get interleaved tokens (terms + operators) for highlighting
300
+ const tokens = parseQueryTokens(input);
301
+ // [
302
+ // { type: 'term', key: 'status', value: 'done', startPosition: 0, endPosition: 11 },
303
+ // { type: 'operator', operator: 'AND', startPosition: 12, endPosition: 15 },
304
+ // { type: 'term', key: 'priority', value: null, startPosition: 16, endPosition: 25 }
305
+ // ]
306
+ ```
307
+
308
+ ### Rich Context with parseWithContext
309
+
310
+ For comprehensive parsing with schema validation, autocomplete, and error recovery:
311
+
312
+ ```typescript
313
+ import { QueryParser } from '@gblikas/querykit';
314
+
315
+ const parser = new QueryParser();
316
+
317
+ // Define your schema for validation and autocomplete
318
+ const schema = {
319
+ status: {
320
+ type: 'string',
321
+ allowedValues: ['todo', 'doing', 'done'],
322
+ description: 'Task status'
323
+ },
324
+ priority: { type: 'number', description: 'Priority level (1-5)' },
325
+ assignee: { type: 'string', description: 'Assigned user' }
326
+ };
327
+
328
+ const result = parser.parseWithContext('status:do', {
329
+ cursorPosition: 9,
330
+ schema,
331
+ securityOptions: { maxClauseCount: 10 }
332
+ });
333
+
334
+ // Always returns a result object (never throws)
335
+ console.log(result.success); // true/false - whether parsing succeeded
336
+ console.log(result.tokens); // Tokenized input (always available)
337
+ console.log(result.structure); // Query structure analysis
338
+ console.log(result.ast); // AST (if successful)
339
+ console.log(result.error); // Error details (if failed)
340
+
341
+ // Autocomplete suggestions based on cursor position
342
+ console.log(result.suggestions);
343
+ // {
344
+ // context: 'value',
345
+ // currentField: 'status',
346
+ // values: [
347
+ // { value: 'doing', score: 80 },
348
+ // { value: 'done', score: 80 }
349
+ // ]
350
+ // }
351
+
352
+ // Schema validation results
353
+ console.log(result.fieldValidation);
354
+ // { valid: true, fields: [...], unknownFields: [] }
355
+
356
+ // Security pre-check
357
+ console.log(result.security);
358
+ // { passed: true, violations: [], warnings: [] }
359
+ ```
360
+
361
+ ### Error Recovery
362
+
363
+ When parsing fails, `parseWithContext` provides helpful recovery hints:
364
+
365
+ ```typescript
366
+ const result = parser.parseWithContext('status:"incomplete');
367
+
368
+ console.log(result.recovery);
369
+ // {
370
+ // issue: 'unclosed_quote',
371
+ // message: 'Unclosed double quote detected',
372
+ // suggestion: 'Add a closing " to complete the quoted value',
373
+ // autofix: 'status:"incomplete"',
374
+ // position: 7
375
+ // }
376
+ ```
377
+
378
+ Error types detected:
379
+ - `unclosed_quote` - Missing closing quote (with autofix)
380
+ - `unclosed_parenthesis` - Unbalanced parentheses (with autofix)
381
+ - `trailing_operator` - Query ends with AND/OR/NOT (with autofix)
382
+ - `missing_value` - Field has colon but no value
383
+ - `syntax_error` - Generic syntax issue
384
+
385
+ ### Building a Search Bar with Highlighting
386
+
387
+ Here's a React example using the input parser for highlighting:
388
+
389
+ ```tsx
390
+ import { parseQueryTokens } from '@gblikas/querykit';
391
+
392
+ function SearchBar({ value, onChange }) {
393
+ const tokens = parseQueryTokens(value);
394
+
395
+ const renderHighlightedQuery = () => {
396
+ if (!value) return null;
397
+
398
+ return tokens.map((token, idx) => {
399
+ const text = value.slice(token.startPosition, token.endPosition);
400
+
401
+ if (token.type === 'operator') {
402
+ return <span key={idx} className="text-purple-500">{text}</span>;
403
+ }
404
+
405
+ // Term token - highlight key and value differently
406
+ if (token.key && token.operator) {
407
+ const keyEnd = token.startPosition + token.key.length;
408
+ const opEnd = keyEnd + token.operator.length;
409
+ return (
410
+ <span key={idx}>
411
+ <span className="text-orange-400">{token.key}</span>
412
+ <span className="text-gray-500">{token.operator}</span>
413
+ <span className="text-blue-400">{value.slice(opEnd, token.endPosition)}</span>
414
+ </span>
415
+ );
416
+ }
417
+
418
+ return <span key={idx}>{text}</span>;
419
+ });
420
+ };
421
+
422
+ return (
423
+ <div className="relative">
424
+ <div className="absolute inset-0 pointer-events-none">
425
+ {renderHighlightedQuery()}
426
+ </div>
427
+ <input
428
+ value={value}
429
+ onChange={(e) => onChange(e.target.value)}
430
+ className="bg-transparent text-transparent caret-black"
431
+ />
432
+ </div>
433
+ );
434
+ }
435
+ ```
436
+
437
+ ## Virtual Fields
438
+
439
+ Virtual fields enable powerful shortcuts in your queries that expand to real schema fields at query execution time based on runtime context. This allows you to support queries like `my:assigned` which expands to `assignee_id == <current_user_id>` using the currently logged-in user's ID.
440
+
441
+ ### Why Virtual Fields?
442
+
443
+ Virtual fields are useful when:
444
+ - You want to provide user-friendly shortcuts (e.g., `my:assigned` instead of `assignee_id:123`)
445
+ - The query depends on runtime context (current user, permissions, tenant, etc.)
446
+ - You want to abstract complex field mappings from end users
447
+ - You need consistent query shortcuts across your application
448
+
449
+ ### Basic Usage
450
+
451
+ Define virtual fields when creating your QueryKit instance:
452
+
453
+ ```typescript
454
+ import { createQueryKit } from '@gblikas/querykit';
455
+ import { drizzleAdapter } from '@gblikas/querykit/adapters/drizzle';
456
+
457
+ const qk = createQueryKit({
458
+ adapter: drizzleAdapter,
459
+ schema: { tasks, users },
460
+
461
+ // Define virtual fields
462
+ virtualFields: {
463
+ my: {
464
+ allowedValues: ['assigned', 'created', 'watching'] as const,
465
+ description: 'Filter by your relationship to items',
466
+
467
+ resolve: (input, ctx, { fields }) => {
468
+ // Map virtual values to real schema fields
469
+ const fieldMap = fields({
470
+ assigned: 'assignee_id',
471
+ created: 'creator_id',
472
+ watching: 'watcher_ids'
473
+ });
474
+
475
+ return {
476
+ type: 'comparison',
477
+ field: fieldMap[input.value],
478
+ operator: '==',
479
+ value: ctx.currentUserId
480
+ };
481
+ }
482
+ }
483
+ },
484
+
485
+ // Provide runtime context
486
+ createContext: async () => ({
487
+ currentUserId: await getCurrentUserId(),
488
+ currentUserTeamIds: await getUserTeamIds()
489
+ })
490
+ });
491
+
492
+ // Use virtual fields in queries
493
+ const myTasks = await qk
494
+ .query('tasks')
495
+ .where('my:assigned AND status:active')
496
+ .execute();
497
+ ```
498
+
499
+ ### Configuration Options
500
+
501
+ Each virtual field definition supports:
502
+
503
+ ```typescript
504
+ {
505
+ // Required: allowed values for this virtual field
506
+ allowedValues: ['value1', 'value2'] as const,
507
+
508
+ // Optional: allow comparison operators (>, <, >=, <=)
509
+ // Default: false (only equality ":" is allowed)
510
+ allowOperators?: boolean,
511
+
512
+ // Required: resolver function
513
+ resolve: (input, context, helpers) => {
514
+ // Return a query expression that replaces the virtual field
515
+ return {
516
+ type: 'comparison',
517
+ field: 'real_field',
518
+ operator: '==',
519
+ value: context.someValue
520
+ };
521
+ },
522
+
523
+ // Optional: human-readable description
524
+ description?: string,
525
+
526
+ // Optional: descriptions for each value
527
+ valueDescriptions?: {
528
+ value1: 'Description of value1',
529
+ value2: 'Description of value2'
530
+ }
531
+ }
532
+ ```
533
+
534
+ ### Type-Safe Field Mapping
535
+
536
+ The `fields()` helper provides compile-time validation that all mapped fields exist in your schema:
537
+
538
+ ```typescript
539
+ virtualFields: {
540
+ my: {
541
+ allowedValues: ['assigned', 'created'] as const,
542
+ resolve: (input, ctx, { fields }) => {
543
+ // TypeScript validates:
544
+ // 1. All allowedValues keys are mapped
545
+ // 2. All field values exist in the schema
546
+ const fieldMap = fields({
547
+ assigned: 'assignee_id', // ✓ Valid schema field
548
+ created: 'creator_id' // ✓ Valid schema field
549
+ // Missing 'watching' → TypeScript error!
550
+ // assigned: 'invalid_field' → TypeScript error!
551
+ });
552
+
553
+ return {
554
+ type: 'comparison',
555
+ field: fieldMap[input.value],
556
+ operator: '==',
557
+ value: ctx.currentUserId
558
+ };
559
+ }
560
+ }
561
+ }
562
+ ```
563
+
564
+ ### Context Factory
565
+
566
+ The `createContext` function is called once per query execution to provide runtime values:
567
+
568
+ ```typescript
569
+ createContext: async () => {
570
+ const user = await getCurrentUser();
571
+ const permissions = await getUserPermissions(user.id);
572
+
573
+ return {
574
+ currentUserId: user.id,
575
+ currentUserTeamIds: user.teamIds,
576
+ canSeeArchived: permissions.includes('view:archived')
577
+ };
578
+ }
579
+ ```
580
+
581
+ Context is type-safe and can include any data your resolvers need:
582
+
583
+ ```typescript
584
+ interface MyQueryContext extends IQueryContext {
585
+ currentUserId: number;
586
+ currentUserTeamIds: number[];
587
+ canSeeArchived: boolean;
588
+ }
589
+
590
+ const qk = createQueryKit<typeof schema, MyQueryContext>({
591
+ // ... configuration
592
+ });
593
+ ```
594
+
595
+ ### Complex Resolvers
596
+
597
+ Virtual fields can return logical expressions for more complex scenarios:
598
+
599
+ ```typescript
600
+ virtualFields: {
601
+ myItems: {
602
+ allowedValues: ['all'] as const,
603
+ resolve: (input, ctx) => ({
604
+ // Return a logical OR expression
605
+ type: 'logical',
606
+ operator: 'OR',
607
+ left: {
608
+ type: 'comparison',
609
+ field: 'assignee_id',
610
+ operator: '==',
611
+ value: ctx.currentUserId
612
+ },
613
+ right: {
614
+ type: 'comparison',
615
+ field: 'creator_id',
616
+ operator: '==',
617
+ value: ctx.currentUserId
618
+ }
619
+ })
620
+ }
621
+ }
622
+
623
+ // Expands to: (assignee_id == currentUserId OR creator_id == currentUserId)
624
+ await qk.query('tasks').where('myItems:all').execute();
625
+ ```
626
+
627
+ ### Allowing Comparison Operators
628
+
629
+ By default, only equality (`:`) is allowed. Enable other operators with `allowOperators: true`:
630
+
631
+ ```typescript
632
+ virtualFields: {
633
+ priority: {
634
+ allowedValues: ['high', 'low'] as const,
635
+ allowOperators: true, // Enable >, <, etc.
636
+
637
+ resolve: (input, ctx) => {
638
+ const threshold = input.value === 'high' ? 7 : 3;
639
+
640
+ return {
641
+ type: 'comparison',
642
+ field: 'priority',
643
+ operator: input.operator, // Use the operator from the query
644
+ value: threshold
645
+ };
646
+ }
647
+ }
648
+ }
649
+
650
+ // Both work:
651
+ qk.query('tasks').where('priority:high') // priority == 7
652
+ qk.query('tasks').where('priority:>high') // priority > 7
653
+ ```
654
+
655
+ ### Error Handling
656
+
657
+ QueryKit throws `QueryParseError` for invalid virtual field usage:
658
+
659
+ ```typescript
660
+ // Invalid value
661
+ qk.query('tasks').where('my:invalid')
662
+ // Error: Invalid value "invalid" for virtual field "my".
663
+ // Allowed values: "assigned", "created", "watching"
664
+
665
+ // Operator not allowed (when allowOperators: false)
666
+ qk.query('tasks').where('my:>assigned')
667
+ // Error: Virtual field "my" does not allow comparison operators.
668
+ // Only equality (":") is permitted.
669
+ ```
670
+
671
+ ### Complete Example
672
+
673
+ Here's a full example with multiple virtual fields:
674
+
675
+ ```typescript
676
+ import { createQueryKit, IQueryContext, ComparisonOperator } from '@gblikas/querykit';
677
+ import { drizzleAdapter } from '@gblikas/querykit/adapters/drizzle';
678
+
679
+ // Define your context type
680
+ interface TaskQueryContext extends IQueryContext {
681
+ currentUserId: number;
682
+ currentUserTeamIds: number[];
683
+ currentTenantId: string;
684
+ }
685
+
686
+ // Create QueryKit with virtual fields
687
+ const qk = createQueryKit<typeof schema, TaskQueryContext>({
688
+ adapter: drizzleAdapter,
689
+ schema: { tasks, users },
690
+
691
+ virtualFields: {
692
+ // User relationship shortcuts
693
+ my: {
694
+ allowedValues: ['assigned', 'created', 'watching'] as const,
695
+ description: 'Filter by your relationship to tasks',
696
+ valueDescriptions: {
697
+ assigned: 'Tasks assigned to you',
698
+ created: 'Tasks you created',
699
+ watching: 'Tasks you are watching'
700
+ },
701
+ resolve: (input, ctx, { fields }) => {
702
+ const fieldMap = fields({
703
+ assigned: 'assignee_id',
704
+ created: 'creator_id',
705
+ watching: 'watcher_ids'
706
+ });
707
+ return {
708
+ type: 'comparison',
709
+ field: fieldMap[input.value],
710
+ operator: '==',
711
+ value: ctx.currentUserId
712
+ };
713
+ }
714
+ },
715
+
716
+ // Team shortcuts
717
+ team: {
718
+ allowedValues: ['assigned', 'owned'] as const,
719
+ description: 'Filter by team relationship',
720
+ resolve: (input, ctx, { fields }) => {
721
+ const fieldMap = fields({
722
+ assigned: 'assignee_id',
723
+ owned: 'owner_id'
724
+ });
725
+ return {
726
+ type: 'comparison',
727
+ field: fieldMap[input.value],
728
+ operator: 'IN',
729
+ value: ctx.currentUserTeamIds
730
+ };
731
+ }
732
+ },
733
+
734
+ // Priority shortcuts with operators
735
+ priority: {
736
+ allowedValues: ['critical', 'high', 'normal', 'low'] as const,
737
+ allowOperators: true,
738
+ description: 'Filter by priority level',
739
+ resolve: (input) => {
740
+ const priorityMap = {
741
+ critical: 10,
742
+ high: 7,
743
+ normal: 5,
744
+ low: 3
745
+ };
746
+ return {
747
+ type: 'comparison',
748
+ field: 'priority',
749
+ operator: input.operator as ComparisonOperator,
750
+ value: priorityMap[input.value as keyof typeof priorityMap]
751
+ };
752
+ }
753
+ }
754
+ },
755
+
756
+ // Context factory
757
+ createContext: async () => {
758
+ const user = await getCurrentUser();
759
+ const teams = await getUserTeams(user.id);
760
+
761
+ return {
762
+ currentUserId: user.id,
763
+ currentUserTeamIds: teams.map(t => t.id),
764
+ currentTenantId: user.tenantId
765
+ };
766
+ }
767
+ });
768
+
769
+ // Example queries using virtual fields
770
+ // "my:assigned AND status:active"
771
+ // "team:assigned OR my:created"
772
+ // "priority:>high AND my:watching"
773
+ // "(my:assigned OR team:assigned) AND status:active"
774
+
775
+ const results = await qk
776
+ .query('tasks')
777
+ .where('my:assigned AND priority:>high')
778
+ .orderBy('created_at', 'desc')
779
+ .limit(10)
780
+ .execute();
781
+ ```
782
+
278
783
  ## Roadmap
279
784
 
280
785
  ### Core Parsing Engine and DSL
@@ -283,6 +788,9 @@ const publicSearchKit = createQueryKit({
283
788
  - [x] Develop internal AST representation
284
789
  - [x] Implement consistent syntax for logical operators (AND, OR, NOT)
285
790
  - [x] Support standard comparison operators (==, !=, >, >=, <, <=)
791
+ - [x] Real-time input parsing for search UIs
792
+ - [x] Autocomplete suggestions with schema awareness
793
+ - [x] Error recovery hints with autofix
286
794
 
287
795
  ### First Adapters
288
796
  - [x] Drizzle ORM integration
@@ -297,9 +805,10 @@ const publicSearchKit = createQueryKit({
297
805
  - [x] Support for complex nested expressions
298
806
  - [ ] Custom function support
299
807
  - [ ] Pagination helpers
808
+ - [x] Virtual fields for context-aware query expansion
300
809
 
301
810
  ### Ecosystem Expansion
302
- - [ ] Frontend query builder components
811
+ - [x] Frontend query builder components (input parser)
303
812
  - [ ] Additional ORM adapters
304
813
  - [ ] Server middleware for Express/Fastify
305
814
  - [ ] TypeScript SDK generation