@gblikas/querykit 0.3.0 โ†’ 0.5.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,543 @@ 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
+
783
+ ### Raw SQL Expressions
784
+
785
+ For advanced query patterns that can't be expressed as simple field comparisons, virtual fields can return **raw SQL expressions**. This enables database-specific operations like JSONB array membership checks, date calculations, and custom SQL logic.
786
+
787
+ #### Why Raw SQL Expressions?
788
+
789
+ Use raw SQL expressions when you need to:
790
+ - **Check JSONB array membership** - e.g., `my:assigned` where `assignedTo` is a JSONB array
791
+ - **Implement computed/derived fields** - e.g., `priority:high` based on date calculations
792
+ - **Use database-specific functions** - e.g., PostGIS functions, full-text search
793
+ - **Combine multiple conditions** - e.g., custom business logic that requires complex SQL
794
+
795
+ #### Basic Example
796
+
797
+ ```typescript
798
+ import { createQueryKit } from '@gblikas/querykit';
799
+ import { drizzleAdapter } from '@gblikas/querykit/adapters/drizzle';
800
+ import { jsonbContains, dateWithinDays } from '@gblikas/querykit/virtual-fields';
801
+ import { sql } from 'drizzle-orm';
802
+
803
+ interface MyContext extends IQueryContext {
804
+ currentUserId: string;
805
+ }
806
+
807
+ const qk = createQueryKit<typeof schema, MyContext>({
808
+ adapter: drizzleAdapter({ db, schema }),
809
+ schema,
810
+
811
+ virtualFields: {
812
+ // JSONB array contains example
813
+ my: {
814
+ allowedValues: ['assigned'] as const,
815
+ description: 'Filter by your relationship to items',
816
+ resolve: (input, ctx) => {
817
+ if (input.value === 'assigned') {
818
+ return jsonbContains('assigned_to', ctx.currentUserId);
819
+ }
820
+ throw new Error(`Unknown value: ${input.value}`);
821
+ }
822
+ },
823
+
824
+ // Computed priority based on createdAt
825
+ priority: {
826
+ allowedValues: ['high', 'medium', 'low'] as const,
827
+ description: 'Filter by computed priority (based on age)',
828
+ resolve: (input) => {
829
+ const thresholds = { high: 1, medium: 7, low: 30 };
830
+ const days = thresholds[input.value as keyof typeof thresholds];
831
+ return dateWithinDays('created_at', days);
832
+ }
833
+ },
834
+
835
+ // Custom raw SQL example
836
+ custom: {
837
+ allowedValues: ['active'] as const,
838
+ resolve: (input, ctx) => ({
839
+ type: 'raw',
840
+ toSql: () => sql`status = 'active' AND ${sql.identifier('owner_id')} = ${ctx.currentUserId}`
841
+ })
842
+ }
843
+ },
844
+
845
+ createContext: async () => ({
846
+ currentUserId: await getCurrentUserId()
847
+ })
848
+ });
849
+
850
+ // Queries that now work:
851
+ await qk.query('my_table').where('my:assigned').execute();
852
+ await qk.query('my_table').where('priority:high AND status:active').execute();
853
+ ```
854
+
855
+ #### Helper Functions
856
+
857
+ QueryKit provides helper functions for common raw SQL patterns:
858
+
859
+ ##### `jsonbContains(field, value)` - JSONB Array Membership (PostgreSQL)
860
+
861
+ Checks if a JSONB array field contains the given value:
862
+
863
+ ```typescript
864
+ import { jsonbContains } from '@gblikas/querykit/virtual-fields';
865
+
866
+ // Check if assignedTo contains the current user ID
867
+ jsonbContains('assigned_to', ctx.currentUserId)
868
+ // Generates: assigned_to @> '["user123"]'::jsonb
869
+
870
+ // Works with arrays too
871
+ jsonbContains('tags', ['urgent', 'review'])
872
+ // Generates: tags @> '["urgent","review"]'::jsonb
873
+ ```
874
+
875
+ ##### `dateWithinDays(field, days)` - Date Range Check
876
+
877
+ Checks if a timestamp field is within the specified number of days from now:
878
+
879
+ ```typescript
880
+ import { dateWithinDays } from '@gblikas/querykit/virtual-fields';
881
+
882
+ // Check if created within last day
883
+ dateWithinDays('created_at', 1)
884
+ // Generates: created_at >= NOW() - INTERVAL '1 days'
885
+
886
+ // Check if created within last week
887
+ dateWithinDays('created_at', 7)
888
+ // Generates: created_at >= NOW() - INTERVAL '7 days'
889
+ ```
890
+
891
+ #### Custom Raw SQL
892
+
893
+ For complete control, create your own raw SQL expressions:
894
+
895
+ ```typescript
896
+ virtualFields: {
897
+ custom: {
898
+ allowedValues: ['special'] as const,
899
+ resolve: (input, ctx) => ({
900
+ type: 'raw',
901
+ toSql: (context) => {
902
+ // context provides: adapter, tableName, schema
903
+ // Return a Drizzle SQL template
904
+ return sql`
905
+ ${sql.identifier('status')} = 'active'
906
+ AND ${sql.identifier('owner_id')} = ${ctx.currentUserId}
907
+ AND ${sql.identifier('created_at')} >= NOW() - INTERVAL '30 days'
908
+ `;
909
+ }
910
+ })
911
+ }
912
+ }
913
+ ```
914
+
915
+ #### Schema Example with JSONB
916
+
917
+ Here's a complete example with a Drizzle schema that uses JSONB:
918
+
919
+ ```typescript
920
+ import { pgTable, serial, varchar, timestamp, jsonb } from 'drizzle-orm/pg-core';
921
+ import { sql } from 'drizzle-orm';
922
+
923
+ export const tasks = pgTable('tasks', {
924
+ id: serial('id').primaryKey(),
925
+ title: varchar('title', { length: 256 }),
926
+ description: varchar('description', { length: 1024 }),
927
+ createdAt: timestamp('created_at').defaultNow(),
928
+ status: varchar('status', { length: 50 }).default('open'),
929
+ // JSONB array of user IDs
930
+ assignedTo: jsonb('assigned_to')
931
+ .$type<string[]>()
932
+ .default(sql`'[]'`)
933
+ });
934
+
935
+ // QueryKit configuration
936
+ const qk = createQueryKit({
937
+ adapter: drizzleAdapter({ db, schema: { tasks } }),
938
+ schema: { tasks },
939
+
940
+ virtualFields: {
941
+ my: {
942
+ allowedValues: ['assigned'] as const,
943
+ resolve: (input, ctx) => jsonbContains('assigned_to', ctx.currentUserId)
944
+ }
945
+ },
946
+
947
+ createContext: async () => ({
948
+ currentUserId: await getCurrentUserId()
949
+ })
950
+ });
951
+
952
+ // Query: find my assigned tasks that are high priority
953
+ await qk.query('tasks')
954
+ .where('my:assigned AND priority:high')
955
+ .execute();
956
+ ```
957
+
958
+ #### Combining with Standard Fields
959
+
960
+ Raw SQL expressions work seamlessly with standard field queries:
961
+
962
+ ```typescript
963
+ // Mix virtual fields (raw SQL) with standard field queries
964
+ await qk.query('tasks')
965
+ .where('my:assigned AND status:active AND priority:>5')
966
+ .execute();
967
+
968
+ // Complex nested queries
969
+ await qk.query('tasks')
970
+ .where('(my:assigned OR priority:high) AND NOT status:closed')
971
+ .execute();
972
+ ```
973
+
437
974
  ## Roadmap
438
975
 
439
976
  ### Core Parsing Engine and DSL
@@ -457,8 +994,10 @@ function SearchBar({ value, onChange }) {
457
994
  - [ ] CLI tools for testing and debugging
458
995
  - [x] Performance optimizations for SQL generation
459
996
  - [x] Support for complex nested expressions
997
+ - [x] Raw SQL expressions for virtual fields (JSONB, computed fields, custom SQL)
460
998
  - [ ] Custom function support
461
999
  - [ ] Pagination helpers
1000
+ - [x] Virtual fields for context-aware query expansion
462
1001
 
463
1002
  ### Ecosystem Expansion
464
1003
  - [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,