@gblikas/querykit 0.4.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.
@@ -45,6 +45,11 @@ export function resolveVirtualFields<
45
45
  return resolveLogicalExpression(expr, virtualFields, context);
46
46
  }
47
47
 
48
+ // Pass through raw expressions
49
+ if (expr.type === 'raw') {
50
+ return expr;
51
+ }
52
+
48
53
  // Unknown expression type, return as-is
49
54
  return expr;
50
55
  }
@@ -5,9 +5,28 @@
5
5
  import {
6
6
  QueryExpression,
7
7
  IComparisonExpression,
8
- ComparisonOperator
8
+ ComparisonOperator,
9
+ IRawSqlExpression
9
10
  } from '../parser/types';
10
11
 
12
+ /**
13
+ * Context provided to raw SQL generators for adapter-specific SQL generation.
14
+ */
15
+ export interface IRawSqlContext {
16
+ /**
17
+ * The database adapter identifier (e.g., 'drizzle')
18
+ */
19
+ adapter: string;
20
+ /**
21
+ * The table name being queried
22
+ */
23
+ tableName: string;
24
+ /**
25
+ * Access to the schema for field references
26
+ */
27
+ schema: Record<string, unknown>;
28
+ }
29
+
11
30
  /**
12
31
  * Base interface for query context.
13
32
  * Users can extend this interface with their own context properties.
@@ -113,10 +132,11 @@ export interface ITypedComparisonExpression<
113
132
 
114
133
  /**
115
134
  * Schema-constrained query expression.
116
- * Can be a comparison or logical expression with typed fields.
135
+ * Can be a comparison, logical expression with typed fields, or a raw SQL expression.
117
136
  */
118
137
  export type ITypedQueryExpression<TFields extends string = string> =
119
138
  | ITypedComparisonExpression<TFields>
139
+ | IRawSqlExpression
120
140
  | QueryExpression;
121
141
 
122
142
  /**
@@ -0,0 +1,182 @@
1
+ /**
2
+ * End-to-end integration test demonstrating the user's example from the issue
3
+ */
4
+
5
+ import { sql } from 'drizzle-orm';
6
+ import type { SQL } from 'drizzle-orm';
7
+ import { jsonbContains, dateWithinDays } from '../virtual-fields';
8
+ import { QueryParser } from '../parser';
9
+ import { resolveVirtualFields } from '../virtual-fields/resolver';
10
+ import { IQueryContext, VirtualFieldsConfig } from '../virtual-fields/types';
11
+ import { DrizzleTranslator } from '../translators/drizzle';
12
+
13
+ // Mock schema similar to the user's example
14
+ type UserSchema = {
15
+ my_table: {
16
+ id: number;
17
+ title: string;
18
+ description: string;
19
+ created_at: Date;
20
+ assigned_to: string[];
21
+ status: string;
22
+ };
23
+ };
24
+
25
+ interface IMyContext extends IQueryContext {
26
+ currentUserId: string;
27
+ }
28
+
29
+ describe('Raw SQL Expression - User Example Integration', () => {
30
+ it('should parse and resolve my:assigned JSONB query from user example', () => {
31
+ const parser = new QueryParser();
32
+ const translator = new DrizzleTranslator();
33
+
34
+ const virtualFields: VirtualFieldsConfig<UserSchema, IMyContext> = {
35
+ my: {
36
+ allowedValues: ['assigned'] as const,
37
+ description: 'Filter by your relationship to items',
38
+ resolve: (input, ctx) => {
39
+ if (input.value === 'assigned') {
40
+ return jsonbContains('assigned_to', ctx.currentUserId);
41
+ }
42
+ throw new Error(`Unknown value: ${input.value}`);
43
+ }
44
+ }
45
+ };
46
+
47
+ const context: IMyContext = { currentUserId: 'user123' };
48
+
49
+ // Parse the query
50
+ const expr = parser.parse('my:assigned');
51
+ expect(expr.type).toBe('comparison');
52
+
53
+ // Resolve virtual fields
54
+ const resolved = resolveVirtualFields(expr, virtualFields, context);
55
+ expect(resolved.type).toBe('raw');
56
+
57
+ // Translate to SQL
58
+ const result = translator.translate(resolved);
59
+ expect(result).toBeDefined();
60
+
61
+ // Verify SQL contains expected elements
62
+ const sqlString = JSON.stringify(result);
63
+ expect(sqlString).toContain('assigned_to');
64
+ expect(sqlString).toContain('user123');
65
+ });
66
+
67
+ it('should parse and resolve priority:high computed field from user example', () => {
68
+ const parser = new QueryParser();
69
+ const translator = new DrizzleTranslator();
70
+
71
+ const virtualFields: VirtualFieldsConfig<UserSchema, IMyContext> = {
72
+ priority: {
73
+ allowedValues: ['high', 'medium', 'low'] as const,
74
+ description: 'Filter by computed priority (based on age)',
75
+ resolve: input => {
76
+ const thresholds = { high: 1, medium: 7, low: 30 };
77
+ const days = thresholds[input.value as keyof typeof thresholds];
78
+ return dateWithinDays('created_at', days);
79
+ }
80
+ }
81
+ };
82
+
83
+ const context: IMyContext = { currentUserId: 'user123' };
84
+
85
+ // Parse the query
86
+ const expr = parser.parse('priority:high');
87
+ expect(expr.type).toBe('comparison');
88
+
89
+ // Resolve virtual fields
90
+ const resolved = resolveVirtualFields(expr, virtualFields, context);
91
+ expect(resolved.type).toBe('raw');
92
+
93
+ // Translate to SQL
94
+ const result = translator.translate(resolved);
95
+ expect(result).toBeDefined();
96
+
97
+ // Verify SQL contains expected elements
98
+ const sqlString = JSON.stringify(result);
99
+ expect(sqlString).toContain('created_at');
100
+ expect(sqlString).toContain('1');
101
+ });
102
+
103
+ it('should handle combined query: my:assigned AND priority:high AND status:active', () => {
104
+ const parser = new QueryParser();
105
+ const translator = new DrizzleTranslator();
106
+
107
+ const virtualFields: VirtualFieldsConfig<UserSchema, IMyContext> = {
108
+ my: {
109
+ allowedValues: ['assigned'] as const,
110
+ resolve: (input, ctx) => jsonbContains('assigned_to', ctx.currentUserId)
111
+ },
112
+ priority: {
113
+ allowedValues: ['high', 'medium', 'low'] as const,
114
+ resolve: input => {
115
+ const days = { high: 1, medium: 7, low: 30 }[
116
+ input.value as 'high' | 'medium' | 'low'
117
+ ];
118
+ return dateWithinDays('created_at', days);
119
+ }
120
+ }
121
+ };
122
+
123
+ const context: IMyContext = { currentUserId: 'user123' };
124
+
125
+ // Parse the combined query
126
+ const expr = parser.parse(
127
+ 'my:assigned AND priority:high AND status:active'
128
+ );
129
+ expect(expr.type).toBe('logical');
130
+
131
+ // Resolve virtual fields
132
+ const resolved = resolveVirtualFields(expr, virtualFields, context);
133
+ expect(resolved.type).toBe('logical');
134
+
135
+ // Translate to SQL
136
+ const result = translator.translate(resolved);
137
+ expect(result).toBeDefined();
138
+
139
+ // Verify SQL contains all expected elements
140
+ const sqlString = JSON.stringify(result);
141
+ expect(sqlString).toContain('assigned_to');
142
+ expect(sqlString).toContain('created_at');
143
+ expect(sqlString).toContain('status');
144
+ expect(sqlString).toContain('AND');
145
+ });
146
+
147
+ it('should support custom raw SQL as shown in user example', () => {
148
+ const parser = new QueryParser();
149
+ const translator = new DrizzleTranslator();
150
+
151
+ const virtualFields: VirtualFieldsConfig<UserSchema, IMyContext> = {
152
+ custom: {
153
+ allowedValues: ['active'] as const,
154
+ resolve: (_input, ctx) => ({
155
+ type: 'raw',
156
+ toSql: (): SQL =>
157
+ sql`status = 'active' AND owner_id = ${ctx.currentUserId}`
158
+ })
159
+ }
160
+ };
161
+
162
+ const context: IMyContext = { currentUserId: 'user123' };
163
+
164
+ // Parse the query
165
+ const expr = parser.parse('custom:active');
166
+ expect(expr.type).toBe('comparison');
167
+
168
+ // Resolve virtual fields
169
+ const resolved = resolveVirtualFields(expr, virtualFields, context);
170
+ expect(resolved.type).toBe('raw');
171
+
172
+ // Translate to SQL
173
+ const result = translator.translate(resolved);
174
+ expect(result).toBeDefined();
175
+
176
+ // Verify SQL contains expected elements
177
+ const sqlString = JSON.stringify(result);
178
+ expect(sqlString).toContain('status');
179
+ expect(sqlString).toContain('active');
180
+ expect(sqlString).toContain('user123');
181
+ });
182
+ });