@gblikas/querykit 0.3.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/.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
@@ -434,6 +434,352 @@ function SearchBar({ value, onChange }) {
434
434
  }
435
435
  ```
436
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
+
437
783
  ## Roadmap
438
784
 
439
785
  ### Core Parsing Engine and DSL
@@ -459,6 +805,7 @@ function SearchBar({ value, onChange }) {
459
805
  - [x] Support for complex nested expressions
460
806
  - [ ] Custom function support
461
807
  - [ ] Pagination helpers
808
+ - [x] Virtual fields for context-aware query expansion
462
809
 
463
810
  ### Ecosystem Expansion
464
811
  - [x] Frontend query builder components (input parser)
package/dist/index.d.ts CHANGED
@@ -10,9 +10,11 @@ import { QueryParser, IParserOptions } from './parser';
10
10
  import { SqlTranslator } from './translators/sql';
11
11
  import { ISecurityOptions } from './security';
12
12
  import { IAdapter, IAdapterOptions } from './adapters';
13
+ import { IQueryContext, VirtualFieldsConfig } from './virtual-fields';
13
14
  export { QueryParser, IParserOptions, QueryBuilder, IQueryBuilderOptions, SqlTranslator };
14
15
  export * from './translators';
15
16
  export * from './adapters';
17
+ export * from './virtual-fields';
16
18
  /**
17
19
  * Create a new QueryBuilder instance
18
20
  */
@@ -24,7 +26,7 @@ export declare function createQueryParser(options?: IParserOptions): QueryParser
24
26
  /**
25
27
  * Options for creating a new QueryKit instance
26
28
  */
27
- export interface IQueryKitOptions<TSchema extends Record<string, object> = Record<string, Record<string, unknown>>> {
29
+ export interface IQueryKitOptions<TSchema extends Record<string, object> = Record<string, Record<string, unknown>>, TContext extends IQueryContext = IQueryContext> {
28
30
  /**
29
31
  * The adapter to use for database connections
30
32
  */
@@ -43,6 +45,37 @@ export interface IQueryKitOptions<TSchema extends Record<string, object> = Recor
43
45
  adapterOptions?: IAdapterOptions & {
44
46
  [key: string]: unknown;
45
47
  };
48
+ /**
49
+ * Virtual field definitions for context-aware query expansion.
50
+ * Virtual fields allow shortcuts like `my:assigned` that expand to
51
+ * real schema fields at query execution time.
52
+ *
53
+ * @example
54
+ * virtualFields: {
55
+ * my: {
56
+ * allowedValues: ['assigned', 'created'] as const,
57
+ * resolve: (input, ctx, { fields }) => ({
58
+ * type: 'comparison',
59
+ * field: fields({ assigned: 'assignee_id', created: 'creator_id' })[input.value],
60
+ * operator: '==',
61
+ * value: ctx.currentUserId
62
+ * })
63
+ * }
64
+ * }
65
+ */
66
+ virtualFields?: VirtualFieldsConfig<TSchema, TContext>;
67
+ /**
68
+ * Factory function to create query execution context.
69
+ * Called once per query execution to provide runtime values
70
+ * for virtual field resolution.
71
+ *
72
+ * @example
73
+ * createContext: async () => ({
74
+ * currentUserId: await getCurrentUserId(),
75
+ * currentUserTeamIds: await getUserTeamIds()
76
+ * })
77
+ */
78
+ createContext?: () => TContext | Promise<TContext>;
46
79
  }
47
80
  export interface IQueryExecutor<TResult> {
48
81
  execute(): Promise<TResult[]>;
@@ -66,10 +99,10 @@ export type QueryKit<TSchema extends Record<string, object>, TRows extends {
66
99
  /**
67
100
  * Create a new QueryKit instance
68
101
  */
69
- export declare function createQueryKit<TSchema extends Record<string, object>, TRows extends {
102
+ export declare function createQueryKit<TSchema extends Record<string, object>, TContext extends IQueryContext = IQueryContext, TRows extends {
70
103
  [K in keyof TSchema & string]: unknown;
71
104
  } = {
72
105
  [K in keyof TSchema & string]: unknown;
73
- }>(options: IQueryKitOptions<TSchema>): QueryKit<TSchema, TRows>;
106
+ }>(options: IQueryKitOptions<TSchema, TContext>): QueryKit<TSchema, TRows>;
74
107
  export * from './parser';
75
108
  export * from './security';
package/dist/index.js CHANGED
@@ -33,9 +33,11 @@ Object.defineProperty(exports, "QueryParser", { enumerable: true, get: function
33
33
  const sql_1 = require("./translators/sql");
34
34
  Object.defineProperty(exports, "SqlTranslator", { enumerable: true, get: function () { return sql_1.SqlTranslator; } });
35
35
  const security_1 = require("./security");
36
+ const virtual_fields_1 = require("./virtual-fields");
36
37
  // Re-export from modules
37
38
  __exportStar(require("./translators"), exports);
38
39
  __exportStar(require("./adapters"), exports);
40
+ __exportStar(require("./virtual-fields"), exports);
39
41
  /**
40
42
  * Create a new QueryBuilder instance
41
43
  */
@@ -75,9 +77,8 @@ function createQueryKit(options) {
75
77
  query: (table) => {
76
78
  return {
77
79
  where: (queryString) => {
78
- // Parse and validate the query
80
+ // Parse the query
79
81
  const expressionAst = parser.parse(queryString);
80
- securityValidator.validate(expressionAst, options.schema);
81
82
  // Execution state accumulated via fluent calls
82
83
  let orderByState = {};
83
84
  let limitState;
@@ -96,8 +97,24 @@ function createQueryKit(options) {
96
97
  return executor;
97
98
  },
98
99
  execute: async () => {
100
+ // Validate that if virtual fields are configured, createContext must also be provided
101
+ if (options.virtualFields && !options.createContext) {
102
+ throw new Error('createContext must be provided when virtualFields is configured');
103
+ }
104
+ // Get context if virtual fields are configured
105
+ let context;
106
+ if (options.virtualFields && options.createContext) {
107
+ context = await options.createContext();
108
+ }
109
+ // Resolve virtual fields if configured and context is available
110
+ let resolvedExpression = expressionAst;
111
+ if (options.virtualFields && context) {
112
+ resolvedExpression = (0, virtual_fields_1.resolveVirtualFields)(expressionAst, options.virtualFields, context);
113
+ }
114
+ // Validate the resolved query
115
+ securityValidator.validate(resolvedExpression, options.schema);
99
116
  // Delegate to adapter
100
- const results = await options.adapter.execute(table, expressionAst, {
117
+ const results = await options.adapter.execute(table, resolvedExpression, {
101
118
  orderBy: Object.keys(orderByState).length > 0
102
119
  ? orderByState
103
120
  : undefined,
@@ -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
+ }