@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.
- package/README.md +192 -0
- package/dist/parser/types.d.ts +17 -1
- package/dist/security/validator.js +14 -5
- package/dist/translators/drizzle/index.js +11 -0
- package/dist/virtual-fields/helpers.d.ts +32 -0
- package/dist/virtual-fields/helpers.js +74 -0
- package/dist/virtual-fields/index.d.ts +1 -0
- package/dist/virtual-fields/index.js +1 -0
- package/dist/virtual-fields/resolver.js +4 -0
- package/dist/virtual-fields/types.d.ts +20 -3
- package/package.json +1 -1
- package/src/parser/types.ts +21 -1
- package/src/security/validator.ts +15 -5
- package/src/translators/drizzle/index.ts +18 -0
- package/src/virtual-fields/helpers.ts +81 -0
- package/src/virtual-fields/index.ts +1 -0
- package/src/virtual-fields/raw-sql.test.ts +978 -0
- package/src/virtual-fields/resolver.ts +5 -0
- package/src/virtual-fields/types.ts +22 -2
- package/src/virtual-fields/user-example-integration.test.ts +182 -0
|
@@ -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
|
|
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
|
+
});
|