sqlite-zod-orm 3.0.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 +314 -0
- package/dist/satidb.js +26 -0
- package/package.json +55 -0
- package/src/ast.ts +107 -0
- package/src/build.ts +23 -0
- package/src/proxy-query.ts +308 -0
- package/src/query-builder.ts +398 -0
- package/src/satidb.ts +1153 -0
package/src/ast.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// ==========================================
|
|
4
|
+
// AST Node Types
|
|
5
|
+
// ==========================================
|
|
6
|
+
|
|
7
|
+
export type ASTNode =
|
|
8
|
+
| { type: 'column'; name: string }
|
|
9
|
+
| { type: 'function'; name: string; args: ASTNode[] }
|
|
10
|
+
| { type: 'operator'; op: string; left: ASTNode; right: ASTNode }
|
|
11
|
+
| { type: 'literal'; value: any };
|
|
12
|
+
|
|
13
|
+
/** Wraps raw JS values into AST literal nodes; passes through existing AST nodes. */
|
|
14
|
+
export const wrapNode = (val: any): ASTNode =>
|
|
15
|
+
(val !== null && typeof val === 'object' && 'type' in val) ? val : { type: 'literal', value: val };
|
|
16
|
+
|
|
17
|
+
// ==========================================
|
|
18
|
+
// AST Compiler → SQL + Params
|
|
19
|
+
// ==========================================
|
|
20
|
+
|
|
21
|
+
export function compileAST(node: ASTNode): { sql: string; params: any[] } {
|
|
22
|
+
if (node.type === 'column') return { sql: `"${node.name}"`, params: [] };
|
|
23
|
+
if (node.type === 'literal') {
|
|
24
|
+
if (node.value instanceof Date) return { sql: '?', params: [node.value.toISOString()] };
|
|
25
|
+
if (typeof node.value === 'boolean') return { sql: '?', params: [node.value ? 1 : 0] };
|
|
26
|
+
return { sql: '?', params: [node.value] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (node.type === 'function') {
|
|
30
|
+
const compiledArgs = node.args.map(compileAST);
|
|
31
|
+
return {
|
|
32
|
+
sql: `${node.name}(${compiledArgs.map(c => c.sql).join(', ')})`,
|
|
33
|
+
params: compiledArgs.flatMap(c => c.params),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (node.type === 'operator') {
|
|
38
|
+
const left = compileAST(node.left);
|
|
39
|
+
const right = compileAST(node.right);
|
|
40
|
+
return {
|
|
41
|
+
sql: `(${left.sql} ${node.op} ${right.sql})`,
|
|
42
|
+
params: [...left.params, ...right.params],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new Error('Unknown AST node type');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ==========================================
|
|
50
|
+
// Proxy Factories (Column, Function, Operator)
|
|
51
|
+
// ==========================================
|
|
52
|
+
|
|
53
|
+
/** Column proxy: c.name => { type: 'column', name: 'name' } */
|
|
54
|
+
export const createColumnProxy = <T>(): TypedColumnProxy<T> =>
|
|
55
|
+
new Proxy({} as any, {
|
|
56
|
+
get: (_, prop: string) => ({ type: 'column', name: prop } as ASTNode),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Function proxy: f.lower(c.name) => { type: 'function', name: 'LOWER', args: [...] } */
|
|
60
|
+
export const createFunctionProxy = (): FunctionProxy =>
|
|
61
|
+
new Proxy({} as any, {
|
|
62
|
+
get: (_, funcName: string) => (...args: any[]) => ({
|
|
63
|
+
type: 'function',
|
|
64
|
+
name: funcName.toUpperCase(),
|
|
65
|
+
args: args.map(wrapNode),
|
|
66
|
+
} as ASTNode),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** Standard SQL operators as composable AST builders. */
|
|
70
|
+
export const op = {
|
|
71
|
+
eq: (left: any, right: any): ASTNode => ({ type: 'operator', op: '=', left: wrapNode(left), right: wrapNode(right) }),
|
|
72
|
+
ne: (left: any, right: any): ASTNode => ({ type: 'operator', op: '!=', left: wrapNode(left), right: wrapNode(right) }),
|
|
73
|
+
gt: (left: any, right: any): ASTNode => ({ type: 'operator', op: '>', left: wrapNode(left), right: wrapNode(right) }),
|
|
74
|
+
gte: (left: any, right: any): ASTNode => ({ type: 'operator', op: '>=', left: wrapNode(left), right: wrapNode(right) }),
|
|
75
|
+
lt: (left: any, right: any): ASTNode => ({ type: 'operator', op: '<', left: wrapNode(left), right: wrapNode(right) }),
|
|
76
|
+
lte: (left: any, right: any): ASTNode => ({ type: 'operator', op: '<=', left: wrapNode(left), right: wrapNode(right) }),
|
|
77
|
+
and: (left: any, right: any): ASTNode => ({ type: 'operator', op: 'AND', left: wrapNode(left), right: wrapNode(right) }),
|
|
78
|
+
or: (left: any, right: any): ASTNode => ({ type: 'operator', op: 'OR', left: wrapNode(left), right: wrapNode(right) }),
|
|
79
|
+
like: (left: any, right: any): ASTNode => ({ type: 'operator', op: 'LIKE', left: wrapNode(left), right: wrapNode(right) }),
|
|
80
|
+
isNull: (node: any): ASTNode => ({ type: 'operator', op: 'IS', left: wrapNode(node), right: { type: 'literal', value: null } as ASTNode }),
|
|
81
|
+
isNotNull: (node: any): ASTNode => ({ type: 'operator', op: 'IS NOT', left: wrapNode(node), right: { type: 'literal', value: null } as ASTNode }),
|
|
82
|
+
in: (left: any, values: any[]): ASTNode => ({
|
|
83
|
+
type: 'function',
|
|
84
|
+
name: `${compileAST(wrapNode(left)).sql} IN`,
|
|
85
|
+
args: values.map(v => wrapNode(v)),
|
|
86
|
+
}),
|
|
87
|
+
not: (node: any): ASTNode => ({ type: 'operator', op: 'NOT', left: { type: 'literal', value: '' } as ASTNode, right: wrapNode(node) }),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ==========================================
|
|
91
|
+
// Type Definitions for IDE Autocomplete
|
|
92
|
+
// ==========================================
|
|
93
|
+
|
|
94
|
+
/** Maps schema fields to AST column nodes for typed autocomplete. */
|
|
95
|
+
export type TypedColumnProxy<T> = { [K in keyof T]: ASTNode };
|
|
96
|
+
|
|
97
|
+
/** SQL function proxy — any function name produces an AST function node. */
|
|
98
|
+
export type FunctionProxy = Record<string, (...args: any[]) => ASTNode>;
|
|
99
|
+
|
|
100
|
+
/** The operators object type. */
|
|
101
|
+
export type Operators = typeof op;
|
|
102
|
+
|
|
103
|
+
/** Callback signature for WHERE clauses. */
|
|
104
|
+
export type WhereCallback<T> = (c: TypedColumnProxy<T>, f: FunctionProxy, op: Operators) => ASTNode;
|
|
105
|
+
|
|
106
|
+
/** Callback signature for SET clauses in updates. */
|
|
107
|
+
export type SetCallback<T> = (c: TypedColumnProxy<T>, f: FunctionProxy) => Partial<Record<keyof T, any>>;
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { EOL } from 'os';
|
|
2
|
+
|
|
3
|
+
console.log("Starting build process for satidb...");
|
|
4
|
+
|
|
5
|
+
const result = await Bun.build({
|
|
6
|
+
entrypoints: ['./src/satidb.ts'],
|
|
7
|
+
outdir: './dist',
|
|
8
|
+
target: 'bun', // Optimize for the Bun runtime
|
|
9
|
+
format: 'esm',
|
|
10
|
+
minify: true, // Minify for smaller file size
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (!result.success) {
|
|
14
|
+
console.error("Build failed");
|
|
15
|
+
for (const message of result.logs) {
|
|
16
|
+
console.error(message);
|
|
17
|
+
}
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const outFile = result.outputs[0].path;
|
|
22
|
+
|
|
23
|
+
console.log(`Build successful! Executable created at: ${outFile}`);
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// ---------- SQL Identifier Quoting ----------
|
|
4
|
+
|
|
5
|
+
/** Quote an identifier (table name, alias, column) to handle reserved words. */
|
|
6
|
+
function q(name: string): string {
|
|
7
|
+
return `"${name}"`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Quote a fully qualified alias.column reference. */
|
|
11
|
+
function qRef(alias: string, column: string): string {
|
|
12
|
+
return `${q(alias)}.${q(column)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ---------- AST Node Types ----------
|
|
16
|
+
|
|
17
|
+
/** Represents a column reference in the query AST. */
|
|
18
|
+
export class ColumnNode {
|
|
19
|
+
readonly _type = 'COL' as const;
|
|
20
|
+
readonly table: string;
|
|
21
|
+
readonly column: string;
|
|
22
|
+
readonly alias: string;
|
|
23
|
+
|
|
24
|
+
constructor(table: string, column: string, alias: string) {
|
|
25
|
+
this.table = table;
|
|
26
|
+
this.column = column;
|
|
27
|
+
this.alias = alias;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* When used as object key via `[t.id]`, JS calls toString().
|
|
32
|
+
* This returns the qualified column name for the ORM to parse.
|
|
33
|
+
*/
|
|
34
|
+
toString(): string {
|
|
35
|
+
return `${q(this.alias)}.${q(this.column)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Also override valueOf for numeric contexts. */
|
|
39
|
+
valueOf(): string {
|
|
40
|
+
return this.toString();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Convenience for Symbol.toPrimitive */
|
|
44
|
+
[Symbol.toPrimitive](hint: string): string {
|
|
45
|
+
return this.toString();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------- Table Proxy ----------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Creates a proxy representing a table with a given alias.
|
|
53
|
+
* Property access returns ColumnNode objects.
|
|
54
|
+
*/
|
|
55
|
+
function createTableProxy(
|
|
56
|
+
tableName: string,
|
|
57
|
+
alias: string,
|
|
58
|
+
columns: Set<string>,
|
|
59
|
+
): Record<string, ColumnNode> {
|
|
60
|
+
return new Proxy({} as Record<string, ColumnNode>, {
|
|
61
|
+
get(_target, prop: string): ColumnNode | undefined {
|
|
62
|
+
if (prop === Symbol.toPrimitive as any || prop === 'toString' || prop === 'valueOf') {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
// Allow any property access — the column may be inferred or wildcard
|
|
66
|
+
return new ColumnNode(tableName, prop, alias);
|
|
67
|
+
},
|
|
68
|
+
ownKeys() {
|
|
69
|
+
return [...columns];
|
|
70
|
+
},
|
|
71
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
72
|
+
if (columns.has(prop as string)) {
|
|
73
|
+
return { configurable: true, enumerable: true, value: new ColumnNode(tableName, prop as string, alias) };
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------- Context Proxy ----------
|
|
81
|
+
|
|
82
|
+
interface AliasEntry {
|
|
83
|
+
tableName: string;
|
|
84
|
+
alias: string;
|
|
85
|
+
proxy: Record<string, ColumnNode>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Creates the root context proxy `c` that the user destructures.
|
|
90
|
+
* Each table access generates a unique alias.
|
|
91
|
+
*/
|
|
92
|
+
export function createContextProxy(
|
|
93
|
+
schemas: Record<string, z.ZodType<any>>,
|
|
94
|
+
): { proxy: Record<string, Record<string, ColumnNode>>; aliasMap: Map<string, AliasEntry[]> } {
|
|
95
|
+
const aliases = new Map<string, AliasEntry[]>();
|
|
96
|
+
let aliasCounter = 0;
|
|
97
|
+
|
|
98
|
+
const proxy = new Proxy({} as Record<string, Record<string, ColumnNode>>, {
|
|
99
|
+
get(_target, tableName: string) {
|
|
100
|
+
if (typeof tableName !== 'string') return undefined;
|
|
101
|
+
|
|
102
|
+
const schema = schemas[tableName];
|
|
103
|
+
const columns = schema
|
|
104
|
+
? new Set(Object.keys((schema as unknown as z.ZodObject<any>).shape))
|
|
105
|
+
: new Set<string>();
|
|
106
|
+
|
|
107
|
+
aliasCounter++;
|
|
108
|
+
const alias = `t${aliasCounter}`;
|
|
109
|
+
const tableProxy = createTableProxy(tableName, alias, columns);
|
|
110
|
+
|
|
111
|
+
// Track alias
|
|
112
|
+
const entries = aliases.get(tableName) || [];
|
|
113
|
+
entries.push({ tableName, alias, proxy: tableProxy });
|
|
114
|
+
aliases.set(tableName, entries);
|
|
115
|
+
|
|
116
|
+
return tableProxy;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return { proxy, aliasMap: aliases };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------- Query Result Shape ----------
|
|
124
|
+
|
|
125
|
+
export interface ProxyQueryResult {
|
|
126
|
+
select: Record<string, ColumnNode>;
|
|
127
|
+
join?: [ColumnNode, ColumnNode] | [ColumnNode, ColumnNode][];
|
|
128
|
+
where?: Record<string, any>;
|
|
129
|
+
orderBy?: Record<string, 'asc' | 'desc'>;
|
|
130
|
+
limit?: number;
|
|
131
|
+
offset?: number;
|
|
132
|
+
groupBy?: ColumnNode[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------- Query Compiler ----------
|
|
136
|
+
|
|
137
|
+
function isColumnNode(val: any): val is ColumnNode {
|
|
138
|
+
return val && typeof val === 'object' && val._type === 'COL';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compile the result of the user's callback into SQL.
|
|
143
|
+
*/
|
|
144
|
+
export function compileProxyQuery(
|
|
145
|
+
queryResult: ProxyQueryResult,
|
|
146
|
+
aliasMap: Map<string, AliasEntry[]>,
|
|
147
|
+
): { sql: string; params: any[] } {
|
|
148
|
+
const params: any[] = [];
|
|
149
|
+
|
|
150
|
+
// Collect all tables/aliases referenced
|
|
151
|
+
const tablesUsed = new Map<string, { tableName: string; alias: string }>();
|
|
152
|
+
|
|
153
|
+
for (const [tableName, entries] of aliasMap) {
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
tablesUsed.set(entry.alias, { tableName, alias: entry.alias });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------- SELECT ----------
|
|
160
|
+
const selectParts: string[] = [];
|
|
161
|
+
for (const [outputName, colOrValue] of Object.entries(queryResult.select)) {
|
|
162
|
+
if (isColumnNode(colOrValue)) {
|
|
163
|
+
if (outputName === colOrValue.column) {
|
|
164
|
+
selectParts.push(qRef(colOrValue.alias, colOrValue.column));
|
|
165
|
+
} else {
|
|
166
|
+
selectParts.push(`${qRef(colOrValue.alias, colOrValue.column)} AS ${q(outputName)}`);
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
// Literal value
|
|
170
|
+
selectParts.push(`? AS ${q(outputName)}`);
|
|
171
|
+
params.push(colOrValue);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------- FROM / JOIN ----------
|
|
176
|
+
// First table in aliases is the primary; rest are joined
|
|
177
|
+
const allAliases = [...tablesUsed.values()];
|
|
178
|
+
if (allAliases.length === 0) throw new Error('No tables referenced in query.');
|
|
179
|
+
|
|
180
|
+
const primaryAlias = allAliases[0]!;
|
|
181
|
+
let sql = `SELECT ${selectParts.join(', ')} FROM ${q(primaryAlias.tableName)} ${q(primaryAlias.alias)}`;
|
|
182
|
+
|
|
183
|
+
// Process JOINs
|
|
184
|
+
if (queryResult.join) {
|
|
185
|
+
const joins: [ColumnNode, ColumnNode][] = Array.isArray(queryResult.join[0])
|
|
186
|
+
? queryResult.join as [ColumnNode, ColumnNode][]
|
|
187
|
+
: [queryResult.join as [ColumnNode, ColumnNode]];
|
|
188
|
+
|
|
189
|
+
for (const [left, right] of joins) {
|
|
190
|
+
// Determine which side is the joined table (not the primary)
|
|
191
|
+
const leftTable = tablesUsed.get(left.alias);
|
|
192
|
+
const rightTable = tablesUsed.get(right.alias);
|
|
193
|
+
|
|
194
|
+
if (!leftTable || !rightTable) throw new Error('Join references unknown table alias.');
|
|
195
|
+
|
|
196
|
+
// The non-primary side needs a JOIN clause
|
|
197
|
+
const joinAlias = leftTable.alias === primaryAlias.alias ? rightTable : leftTable;
|
|
198
|
+
|
|
199
|
+
sql += ` JOIN ${q(joinAlias.tableName)} ${q(joinAlias.alias)} ON ${qRef(left.alias, left.column)} = ${qRef(right.alias, right.column)}`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------- WHERE ----------
|
|
204
|
+
if (queryResult.where && Object.keys(queryResult.where).length > 0) {
|
|
205
|
+
const whereParts: string[] = [];
|
|
206
|
+
|
|
207
|
+
for (const [key, value] of Object.entries(queryResult.where)) {
|
|
208
|
+
// The key could be '"t1"."column"' (from toString trick) or a plain string
|
|
209
|
+
let fieldRef: string;
|
|
210
|
+
|
|
211
|
+
// Match quoted alias.column pattern: "alias"."column"
|
|
212
|
+
const quotedMatch = key.match(/^"([^"]+)"\."([^"]+)"$/);
|
|
213
|
+
if (quotedMatch && tablesUsed.has(quotedMatch[1]!)) {
|
|
214
|
+
// Already fully quoted
|
|
215
|
+
fieldRef = key;
|
|
216
|
+
} else {
|
|
217
|
+
// Plain field name — use the first table
|
|
218
|
+
fieldRef = qRef(primaryAlias.alias, key);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (isColumnNode(value)) {
|
|
222
|
+
// Column-to-column comparison
|
|
223
|
+
whereParts.push(`${fieldRef} = ${qRef(value.alias, value.column)}`);
|
|
224
|
+
} else if (Array.isArray(value)) {
|
|
225
|
+
if (value.length === 0) {
|
|
226
|
+
whereParts.push('1 = 0');
|
|
227
|
+
} else {
|
|
228
|
+
const placeholders = value.map(() => '?').join(', ');
|
|
229
|
+
whereParts.push(`${fieldRef} IN (${placeholders})`);
|
|
230
|
+
params.push(...value);
|
|
231
|
+
}
|
|
232
|
+
} else if (typeof value === 'object' && value !== null && !(value instanceof Date)) {
|
|
233
|
+
// Operator object like { $gt: 5 }
|
|
234
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
235
|
+
const opMap: Record<string, string> = {
|
|
236
|
+
$gt: '>', $gte: '>=', $lt: '<', $lte: '<=', $ne: '!=',
|
|
237
|
+
};
|
|
238
|
+
const sqlOp = opMap[op];
|
|
239
|
+
if (!sqlOp) throw new Error(`Unsupported where operator: ${op}`);
|
|
240
|
+
whereParts.push(`${fieldRef} ${sqlOp} ?`);
|
|
241
|
+
params.push(operand);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
whereParts.push(`${fieldRef} = ?`);
|
|
245
|
+
params.push(value instanceof Date ? value.toISOString() : value);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (whereParts.length > 0) {
|
|
250
|
+
sql += ` WHERE ${whereParts.join(' AND ')}`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ---------- ORDER BY ----------
|
|
255
|
+
if (queryResult.orderBy) {
|
|
256
|
+
const parts: string[] = [];
|
|
257
|
+
for (const [key, dir] of Object.entries(queryResult.orderBy)) {
|
|
258
|
+
let fieldRef: string;
|
|
259
|
+
const quotedMatch = key.match(/^"([^"]+)"\."([^"]+)"$/);
|
|
260
|
+
if (quotedMatch && tablesUsed.has(quotedMatch[1]!)) {
|
|
261
|
+
fieldRef = key;
|
|
262
|
+
} else {
|
|
263
|
+
fieldRef = qRef(primaryAlias.alias, key);
|
|
264
|
+
}
|
|
265
|
+
parts.push(`${fieldRef} ${dir.toUpperCase()}`);
|
|
266
|
+
}
|
|
267
|
+
if (parts.length > 0) {
|
|
268
|
+
sql += ` ORDER BY ${parts.join(', ')}`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---------- GROUP BY ----------
|
|
273
|
+
if (queryResult.groupBy && queryResult.groupBy.length > 0) {
|
|
274
|
+
const parts = queryResult.groupBy.map(col => qRef(col.alias, col.column));
|
|
275
|
+
sql += ` GROUP BY ${parts.join(', ')}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---------- LIMIT / OFFSET ----------
|
|
279
|
+
if (queryResult.limit !== undefined) {
|
|
280
|
+
sql += ` LIMIT ${queryResult.limit}`;
|
|
281
|
+
}
|
|
282
|
+
if (queryResult.offset !== undefined) {
|
|
283
|
+
sql += ` OFFSET ${queryResult.offset}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { sql, params };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------- Public API ----------
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* The main `db.query(c => {...})` entry point.
|
|
293
|
+
*
|
|
294
|
+
* @param schemas The schema map for all registered tables.
|
|
295
|
+
* @param callback The user's query callback that receives the context proxy.
|
|
296
|
+
* @param executor A function that runs the compiled SQL and returns rows.
|
|
297
|
+
* @returns The query results.
|
|
298
|
+
*/
|
|
299
|
+
export function executeProxyQuery<T>(
|
|
300
|
+
schemas: Record<string, z.ZodType<any>>,
|
|
301
|
+
callback: (ctx: any) => ProxyQueryResult,
|
|
302
|
+
executor: (sql: string, params: any[]) => T[],
|
|
303
|
+
): T[] {
|
|
304
|
+
const { proxy, aliasMap } = createContextProxy(schemas);
|
|
305
|
+
const queryResult = callback(proxy);
|
|
306
|
+
const { sql, params } = compileProxyQuery(queryResult, aliasMap);
|
|
307
|
+
return executor(sql, params);
|
|
308
|
+
}
|