sqlite-zod-orm 3.9.0 → 3.11.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/dist/index.js +242 -72
- package/package.json +1 -1
- package/src/builder.ts +399 -0
- package/src/context.ts +9 -0
- package/src/crud.ts +44 -3
- package/src/database.ts +53 -3
- package/src/helpers.ts +11 -0
- package/src/index.ts +1 -1
- package/src/iqo.ts +198 -0
- package/src/proxy.ts +276 -0
- package/src/query.ts +23 -736
- package/src/types.ts +26 -3
package/src/iqo.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iqo.ts — Internal Query Object types and SQL compiler
|
|
3
|
+
*
|
|
4
|
+
* Defines the IQO structure, WHERE operators, and the `compileIQO` function
|
|
5
|
+
* that transforms an IQO into executable SQL + params.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type ASTNode, compileAST } from './ast';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// IQO — Internal Query Object
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export type OrderDirection = 'asc' | 'desc';
|
|
15
|
+
export type WhereOperator = '$gt' | '$gte' | '$lt' | '$lte' | '$ne' | '$in' | '$like' | '$notIn' | '$between' | '$isNull' | '$isNotNull';
|
|
16
|
+
|
|
17
|
+
export interface WhereCondition {
|
|
18
|
+
field: string;
|
|
19
|
+
operator: '=' | '>' | '>=' | '<' | '<=' | '!=' | 'IN' | 'LIKE' | 'NOT IN' | 'BETWEEN' | 'IS NULL' | 'IS NOT NULL';
|
|
20
|
+
value: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface JoinClause {
|
|
24
|
+
table: string;
|
|
25
|
+
fromCol: string;
|
|
26
|
+
toCol: string;
|
|
27
|
+
columns: string[]; // columns to SELECT from the joined table
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface IQO {
|
|
31
|
+
selects: string[];
|
|
32
|
+
wheres: WhereCondition[];
|
|
33
|
+
whereOrs: WhereCondition[][]; // Each sub-array is an OR group
|
|
34
|
+
whereAST: ASTNode | null;
|
|
35
|
+
joins: JoinClause[];
|
|
36
|
+
groupBy: string[];
|
|
37
|
+
having: WhereCondition[];
|
|
38
|
+
limit: number | null;
|
|
39
|
+
offset: number | null;
|
|
40
|
+
orderBy: { field: string; direction: OrderDirection }[];
|
|
41
|
+
includes: string[];
|
|
42
|
+
raw: boolean;
|
|
43
|
+
distinct: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const OPERATOR_MAP: Record<WhereOperator, string> = {
|
|
47
|
+
$gt: '>',
|
|
48
|
+
$gte: '>=',
|
|
49
|
+
$lt: '<',
|
|
50
|
+
$lte: '<=',
|
|
51
|
+
$ne: '!=',
|
|
52
|
+
$in: 'IN',
|
|
53
|
+
$like: 'LIKE',
|
|
54
|
+
$notIn: 'NOT IN',
|
|
55
|
+
$between: 'BETWEEN',
|
|
56
|
+
$isNull: 'IS NULL',
|
|
57
|
+
$isNotNull: 'IS NOT NULL',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function transformValueForStorage(value: any): any {
|
|
61
|
+
if (value instanceof Date) return value.toISOString();
|
|
62
|
+
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Compile an Internal Query Object into executable SQL + params.
|
|
68
|
+
* Handles SELECT, JOIN, WHERE (object + AST + $or), ORDER BY, LIMIT, OFFSET.
|
|
69
|
+
*/
|
|
70
|
+
export function compileIQO(tableName: string, iqo: IQO): { sql: string; params: any[] } {
|
|
71
|
+
const params: any[] = [];
|
|
72
|
+
|
|
73
|
+
// SELECT clause
|
|
74
|
+
const selectParts: string[] = [];
|
|
75
|
+
if (iqo.selects.length > 0) {
|
|
76
|
+
selectParts.push(...iqo.selects.map(s => `${tableName}.${s}`));
|
|
77
|
+
} else {
|
|
78
|
+
selectParts.push(`${tableName}.*`);
|
|
79
|
+
}
|
|
80
|
+
for (const j of iqo.joins) {
|
|
81
|
+
if (j.columns.length > 0) {
|
|
82
|
+
selectParts.push(...j.columns.map(c => `${j.table}.${c} AS ${j.table}_${c}`));
|
|
83
|
+
} else {
|
|
84
|
+
selectParts.push(`${j.table}.*`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let sql = `SELECT ${iqo.distinct ? 'DISTINCT ' : ''}${selectParts.join(', ')} FROM ${tableName}`;
|
|
89
|
+
|
|
90
|
+
// JOIN clauses
|
|
91
|
+
for (const j of iqo.joins) {
|
|
92
|
+
sql += ` JOIN ${j.table} ON ${tableName}.${j.fromCol} = ${j.table}.${j.toCol}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// WHERE clause — AST-based takes precedence if set
|
|
96
|
+
if (iqo.whereAST) {
|
|
97
|
+
const compiled = compileAST(iqo.whereAST);
|
|
98
|
+
sql += ` WHERE ${compiled.sql}`;
|
|
99
|
+
params.push(...compiled.params);
|
|
100
|
+
} else if (iqo.wheres.length > 0) {
|
|
101
|
+
const hasJoins = iqo.joins.length > 0;
|
|
102
|
+
const qualify = (field: string) =>
|
|
103
|
+
hasJoins && !field.includes('.') ? `${tableName}.${field}` : field;
|
|
104
|
+
|
|
105
|
+
const whereParts: string[] = [];
|
|
106
|
+
for (const w of iqo.wheres) {
|
|
107
|
+
if (w.operator === 'IN') {
|
|
108
|
+
const arr = w.value as any[];
|
|
109
|
+
if (arr.length === 0) {
|
|
110
|
+
whereParts.push('1 = 0');
|
|
111
|
+
} else {
|
|
112
|
+
const placeholders = arr.map(() => '?').join(', ');
|
|
113
|
+
whereParts.push(`${qualify(w.field)} IN (${placeholders})`);
|
|
114
|
+
params.push(...arr.map(transformValueForStorage));
|
|
115
|
+
}
|
|
116
|
+
} else if (w.operator === 'NOT IN') {
|
|
117
|
+
const arr = w.value as any[];
|
|
118
|
+
if (arr.length === 0) continue; // no-op
|
|
119
|
+
const placeholders = arr.map(() => '?').join(', ');
|
|
120
|
+
whereParts.push(`${qualify(w.field)} NOT IN (${placeholders})`);
|
|
121
|
+
params.push(...arr.map(transformValueForStorage));
|
|
122
|
+
} else if (w.operator === 'BETWEEN') {
|
|
123
|
+
const [min, max] = w.value as [any, any];
|
|
124
|
+
whereParts.push(`${qualify(w.field)} BETWEEN ? AND ?`);
|
|
125
|
+
params.push(transformValueForStorage(min), transformValueForStorage(max));
|
|
126
|
+
} else if (w.operator === 'IS NULL') {
|
|
127
|
+
whereParts.push(`${qualify(w.field)} IS NULL`);
|
|
128
|
+
} else if (w.operator === 'IS NOT NULL') {
|
|
129
|
+
whereParts.push(`${qualify(w.field)} IS NOT NULL`);
|
|
130
|
+
} else {
|
|
131
|
+
whereParts.push(`${qualify(w.field)} ${w.operator} ?`);
|
|
132
|
+
params.push(transformValueForStorage(w.value));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (whereParts.length > 0) {
|
|
136
|
+
sql += ` WHERE ${whereParts.join(' AND ')}`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Append OR groups (from $or)
|
|
141
|
+
if (iqo.whereOrs.length > 0) {
|
|
142
|
+
for (const orGroup of iqo.whereOrs) {
|
|
143
|
+
const orParts: string[] = [];
|
|
144
|
+
for (const w of orGroup) {
|
|
145
|
+
if (w.operator === 'IN') {
|
|
146
|
+
const arr = w.value as any[];
|
|
147
|
+
if (arr.length === 0) {
|
|
148
|
+
orParts.push('1 = 0');
|
|
149
|
+
} else {
|
|
150
|
+
orParts.push(`${w.field} IN (${arr.map(() => '?').join(', ')})`);
|
|
151
|
+
params.push(...arr.map(transformValueForStorage));
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
orParts.push(`${w.field} ${w.operator} ?`);
|
|
155
|
+
params.push(transformValueForStorage(w.value));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (orParts.length > 0) {
|
|
159
|
+
const orClause = `(${orParts.join(' OR ')})`;
|
|
160
|
+
sql += sql.includes(' WHERE ') ? ` AND ${orClause}` : ` WHERE ${orClause}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// GROUP BY
|
|
166
|
+
if (iqo.groupBy.length > 0) {
|
|
167
|
+
sql += ` GROUP BY ${iqo.groupBy.join(', ')}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// HAVING
|
|
171
|
+
if (iqo.having && iqo.having.length > 0) {
|
|
172
|
+
const havingParts: string[] = [];
|
|
173
|
+
for (const h of iqo.having) {
|
|
174
|
+
if (h.operator === 'IS NULL') {
|
|
175
|
+
havingParts.push(`${h.field} IS NULL`);
|
|
176
|
+
} else if (h.operator === 'IS NOT NULL') {
|
|
177
|
+
havingParts.push(`${h.field} IS NOT NULL`);
|
|
178
|
+
} else {
|
|
179
|
+
havingParts.push(`${h.field} ${h.operator} ?`);
|
|
180
|
+
params.push(transformValueForStorage(h.value));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (havingParts.length > 0) {
|
|
184
|
+
sql += ` HAVING ${havingParts.join(' AND ')}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ORDER BY
|
|
189
|
+
if (iqo.orderBy.length > 0) {
|
|
190
|
+
const parts = iqo.orderBy.map(o => `${o.field} ${o.direction.toUpperCase()}`);
|
|
191
|
+
sql += ` ORDER BY ${parts.join(', ')}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (iqo.limit !== null) sql += ` LIMIT ${iqo.limit}`;
|
|
195
|
+
if (iqo.offset !== null) sql += ` OFFSET ${iqo.offset}`;
|
|
196
|
+
|
|
197
|
+
return { sql, params };
|
|
198
|
+
}
|
package/src/proxy.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* proxy.ts — Proxy Query System
|
|
3
|
+
*
|
|
4
|
+
* Enables `db.query(c => { ... })` for SQL-like multi-table queries
|
|
5
|
+
* using destructured proxied table references.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// ColumnNode
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Represents a reference to a specific table column with an alias.
|
|
16
|
+
* Used as a building block for SQL query construction.
|
|
17
|
+
*/
|
|
18
|
+
export class ColumnNode {
|
|
19
|
+
readonly _type = 'COL' as const;
|
|
20
|
+
constructor(
|
|
21
|
+
readonly table: string,
|
|
22
|
+
readonly column: string,
|
|
23
|
+
readonly alias: string,
|
|
24
|
+
) { }
|
|
25
|
+
|
|
26
|
+
/** Quoted alias.column for use as computed property key */
|
|
27
|
+
toString(): string {
|
|
28
|
+
return `"${this.alias}"."${this.column}"`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
[Symbol.toPrimitive](): string {
|
|
32
|
+
return this.toString();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------- SQL Quoting Helpers ----------
|
|
37
|
+
|
|
38
|
+
function q(name: string): string {
|
|
39
|
+
return `"${name}"`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function qRef(alias: string, column: string): string {
|
|
43
|
+
return `"${alias}"."${column}"`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------- Table Proxy ----------
|
|
47
|
+
|
|
48
|
+
function createTableProxy(
|
|
49
|
+
tableName: string,
|
|
50
|
+
alias: string,
|
|
51
|
+
columns: Set<string>,
|
|
52
|
+
): Record<string, ColumnNode> {
|
|
53
|
+
return new Proxy({} as Record<string, ColumnNode>, {
|
|
54
|
+
get(_target, prop: string): ColumnNode | undefined {
|
|
55
|
+
if (prop === Symbol.toPrimitive as any || prop === 'toString' || prop === 'valueOf') {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return new ColumnNode(tableName, prop, alias);
|
|
59
|
+
},
|
|
60
|
+
ownKeys() {
|
|
61
|
+
return [...columns];
|
|
62
|
+
},
|
|
63
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
64
|
+
if (columns.has(prop as string)) {
|
|
65
|
+
return { configurable: true, enumerable: true, value: new ColumnNode(tableName, prop as string, alias) };
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------- Context Proxy ----------
|
|
73
|
+
|
|
74
|
+
interface AliasEntry {
|
|
75
|
+
tableName: string;
|
|
76
|
+
alias: string;
|
|
77
|
+
proxy: Record<string, ColumnNode>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createContextProxy(
|
|
81
|
+
schemas: Record<string, z.ZodType<any>>,
|
|
82
|
+
): { proxy: Record<string, Record<string, ColumnNode>>; aliasMap: Map<string, AliasEntry[]> } {
|
|
83
|
+
const aliases = new Map<string, AliasEntry[]>();
|
|
84
|
+
let aliasCounter = 0;
|
|
85
|
+
|
|
86
|
+
const proxy = new Proxy({} as Record<string, Record<string, ColumnNode>>, {
|
|
87
|
+
get(_target, tableName: string) {
|
|
88
|
+
if (typeof tableName !== 'string') return undefined;
|
|
89
|
+
|
|
90
|
+
const schema = schemas[tableName];
|
|
91
|
+
const shape = schema
|
|
92
|
+
? (schema as unknown as z.ZodObject<any>).shape
|
|
93
|
+
: {};
|
|
94
|
+
const columns = new Set(Object.keys(shape));
|
|
95
|
+
|
|
96
|
+
aliasCounter++;
|
|
97
|
+
const alias = `t${aliasCounter}`;
|
|
98
|
+
const tableProxy = createTableProxy(tableName, alias, columns);
|
|
99
|
+
|
|
100
|
+
const entries = aliases.get(tableName) || [];
|
|
101
|
+
entries.push({ tableName, alias, proxy: tableProxy });
|
|
102
|
+
aliases.set(tableName, entries);
|
|
103
|
+
|
|
104
|
+
return tableProxy;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return { proxy, aliasMap: aliases };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------- Proxy Query Result ----------
|
|
112
|
+
|
|
113
|
+
type AnyColumn = ColumnNode | (ColumnNode & string);
|
|
114
|
+
|
|
115
|
+
export interface ProxyQueryResult {
|
|
116
|
+
select: Record<string, AnyColumn | undefined>;
|
|
117
|
+
join?: [AnyColumn | undefined, AnyColumn | undefined] | [AnyColumn | undefined, AnyColumn | undefined][];
|
|
118
|
+
where?: Record<string, any>;
|
|
119
|
+
orderBy?: Record<string, 'asc' | 'desc'>;
|
|
120
|
+
limit?: number;
|
|
121
|
+
offset?: number;
|
|
122
|
+
groupBy?: (AnyColumn | undefined)[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------- Proxy Query Compiler ----------
|
|
126
|
+
|
|
127
|
+
function isColumnNode(val: any): val is ColumnNode {
|
|
128
|
+
return val && typeof val === 'object' && val._type === 'COL';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function compileProxyQuery(
|
|
132
|
+
queryResult: ProxyQueryResult,
|
|
133
|
+
aliasMap: Map<string, AliasEntry[]>,
|
|
134
|
+
): { sql: string; params: any[] } {
|
|
135
|
+
const params: any[] = [];
|
|
136
|
+
|
|
137
|
+
const tablesUsed = new Map<string, { tableName: string; alias: string }>();
|
|
138
|
+
for (const [tableName, entries] of aliasMap) {
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
tablesUsed.set(entry.alias, { tableName, alias: entry.alias });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// SELECT
|
|
145
|
+
const selectParts: string[] = [];
|
|
146
|
+
for (const [outputName, colOrValue] of Object.entries(queryResult.select)) {
|
|
147
|
+
if (isColumnNode(colOrValue)) {
|
|
148
|
+
if (outputName === colOrValue.column) {
|
|
149
|
+
selectParts.push(qRef(colOrValue.alias, colOrValue.column));
|
|
150
|
+
} else {
|
|
151
|
+
selectParts.push(`${qRef(colOrValue.alias, colOrValue.column)} AS ${q(outputName)}`);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
selectParts.push(`? AS ${q(outputName)}`);
|
|
155
|
+
params.push(colOrValue);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// FROM / JOIN
|
|
160
|
+
const allAliases = [...tablesUsed.values()];
|
|
161
|
+
if (allAliases.length === 0) throw new Error('No tables referenced in query.');
|
|
162
|
+
|
|
163
|
+
const primaryAlias = allAliases[0]!;
|
|
164
|
+
let sql = `SELECT ${selectParts.join(', ')} FROM ${q(primaryAlias.tableName)} ${q(primaryAlias.alias)}`;
|
|
165
|
+
|
|
166
|
+
if (queryResult.join) {
|
|
167
|
+
const joins: [ColumnNode, ColumnNode][] = Array.isArray(queryResult.join[0])
|
|
168
|
+
? queryResult.join as [ColumnNode, ColumnNode][]
|
|
169
|
+
: [queryResult.join as [ColumnNode, ColumnNode]];
|
|
170
|
+
|
|
171
|
+
for (const [left, right] of joins) {
|
|
172
|
+
const leftTable = tablesUsed.get(left.alias);
|
|
173
|
+
const rightTable = tablesUsed.get(right.alias);
|
|
174
|
+
if (!leftTable || !rightTable) throw new Error('Join references unknown table alias.');
|
|
175
|
+
const joinAlias = leftTable.alias === primaryAlias.alias ? rightTable : leftTable;
|
|
176
|
+
sql += ` JOIN ${q(joinAlias.tableName)} ${q(joinAlias.alias)} ON ${qRef(left.alias, left.column)} = ${qRef(right.alias, right.column)}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// WHERE
|
|
181
|
+
if (queryResult.where && Object.keys(queryResult.where).length > 0) {
|
|
182
|
+
const whereParts: string[] = [];
|
|
183
|
+
|
|
184
|
+
for (const [key, value] of Object.entries(queryResult.where)) {
|
|
185
|
+
let fieldRef: string;
|
|
186
|
+
const quotedMatch = key.match(/^"([^"]+)"\."([^"]+)"$/);
|
|
187
|
+
if (quotedMatch && tablesUsed.has(quotedMatch[1]!)) {
|
|
188
|
+
fieldRef = key;
|
|
189
|
+
} else {
|
|
190
|
+
fieldRef = qRef(primaryAlias.alias, key);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (isColumnNode(value)) {
|
|
194
|
+
whereParts.push(`${fieldRef} = ${qRef(value.alias, value.column)}`);
|
|
195
|
+
} else if (Array.isArray(value)) {
|
|
196
|
+
if (value.length === 0) {
|
|
197
|
+
whereParts.push('1 = 0');
|
|
198
|
+
} else {
|
|
199
|
+
const placeholders = value.map(() => '?').join(', ');
|
|
200
|
+
whereParts.push(`${fieldRef} IN (${placeholders})`);
|
|
201
|
+
params.push(...value);
|
|
202
|
+
}
|
|
203
|
+
} else if (typeof value === 'object' && value !== null && !(value instanceof Date)) {
|
|
204
|
+
for (const [pOp, operand] of Object.entries(value)) {
|
|
205
|
+
if (pOp === '$in') {
|
|
206
|
+
const arr = operand as any[];
|
|
207
|
+
if (arr.length === 0) {
|
|
208
|
+
whereParts.push('1 = 0');
|
|
209
|
+
} else {
|
|
210
|
+
const placeholders = arr.map(() => '?').join(', ');
|
|
211
|
+
whereParts.push(`${fieldRef} IN (${placeholders})`);
|
|
212
|
+
params.push(...arr);
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const opMap: Record<string, string> = {
|
|
217
|
+
$gt: '>', $gte: '>=', $lt: '<', $lte: '<=', $ne: '!=',
|
|
218
|
+
};
|
|
219
|
+
const sqlOp = opMap[pOp];
|
|
220
|
+
if (!sqlOp) throw new Error(`Unsupported where operator: ${pOp}`);
|
|
221
|
+
whereParts.push(`${fieldRef} ${sqlOp} ?`);
|
|
222
|
+
params.push(operand);
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
whereParts.push(`${fieldRef} = ?`);
|
|
226
|
+
params.push(value instanceof Date ? value.toISOString() : value);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (whereParts.length > 0) {
|
|
231
|
+
sql += ` WHERE ${whereParts.join(' AND ')}`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ORDER BY
|
|
236
|
+
if (queryResult.orderBy) {
|
|
237
|
+
const parts: string[] = [];
|
|
238
|
+
for (const [key, dir] of Object.entries(queryResult.orderBy)) {
|
|
239
|
+
let fieldRef: string;
|
|
240
|
+
const quotedMatch = key.match(/^"([^"]+)"\."([^"]+)"$/);
|
|
241
|
+
if (quotedMatch && tablesUsed.has(quotedMatch[1]!)) {
|
|
242
|
+
fieldRef = key;
|
|
243
|
+
} else {
|
|
244
|
+
fieldRef = qRef(primaryAlias.alias, key);
|
|
245
|
+
}
|
|
246
|
+
parts.push(`${fieldRef} ${dir.toUpperCase()}`);
|
|
247
|
+
}
|
|
248
|
+
if (parts.length > 0) {
|
|
249
|
+
sql += ` ORDER BY ${parts.join(', ')}`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// GROUP BY
|
|
254
|
+
if (queryResult.groupBy && queryResult.groupBy.length > 0) {
|
|
255
|
+
const parts = queryResult.groupBy.filter(Boolean).map(col => qRef(col!.alias, col!.column));
|
|
256
|
+
sql += ` GROUP BY ${parts.join(', ')}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// LIMIT / OFFSET
|
|
260
|
+
if (queryResult.limit !== undefined) sql += ` LIMIT ${queryResult.limit}`;
|
|
261
|
+
if (queryResult.offset !== undefined) sql += ` OFFSET ${queryResult.offset}`;
|
|
262
|
+
|
|
263
|
+
return { sql, params };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The main `db.query(c => {...})` entry point. */
|
|
267
|
+
export function executeProxyQuery<T>(
|
|
268
|
+
schemas: Record<string, z.ZodType<any>>,
|
|
269
|
+
callback: (ctx: any) => ProxyQueryResult,
|
|
270
|
+
executor: (sql: string, params: any[]) => T[],
|
|
271
|
+
): T[] {
|
|
272
|
+
const { proxy, aliasMap } = createContextProxy(schemas);
|
|
273
|
+
const queryResult = callback(proxy);
|
|
274
|
+
const { sql, params } = compileProxyQuery(queryResult, aliasMap);
|
|
275
|
+
return executor(sql, params);
|
|
276
|
+
}
|