sqlite-zod-orm 3.8.0 → 3.10.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 +146 -93
- package/dist/index.js +2437 -2386
- package/package.json +5 -3
- package/src/ast.ts +1 -1
- package/src/builder.ts +311 -0
- package/src/context.ts +25 -0
- package/src/crud.ts +163 -0
- package/src/database.ts +173 -396
- package/src/entity.ts +62 -0
- package/src/helpers.ts +87 -0
- package/src/index.ts +2 -3
- package/src/iqo.ts +172 -0
- package/src/{proxy-query.ts → proxy.ts} +22 -58
- package/src/query.ts +136 -0
- package/src/types.ts +27 -6
- package/dist/satidb.js +0 -26
- package/src/build.ts +0 -21
- package/src/query-builder.ts +0 -669
package/src/entity.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entity.ts — Entity augmentation logic extracted from Database class.
|
|
3
|
+
*
|
|
4
|
+
* Handles attaching .update(), .delete(), relationship navigation methods,
|
|
5
|
+
* and the auto-persist proxy to raw row objects.
|
|
6
|
+
*/
|
|
7
|
+
import type { AugmentedEntity, Relationship } from './types';
|
|
8
|
+
import { getStorableFields, transformForStorage } from './schema';
|
|
9
|
+
import type { DatabaseContext } from './context';
|
|
10
|
+
import { getById, findMany, update, deleteEntity } from './crud';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Augment a raw entity with:
|
|
14
|
+
* - .update(data) → persist partial update
|
|
15
|
+
* - .delete() → delete from DB
|
|
16
|
+
* - Lazy relationship accessors (author(), books(), etc.)
|
|
17
|
+
* - Auto-persist proxy: `entity.name = 'New'` auto-updates DB
|
|
18
|
+
*/
|
|
19
|
+
export function attachMethods<T extends Record<string, any>>(
|
|
20
|
+
ctx: DatabaseContext,
|
|
21
|
+
entityName: string,
|
|
22
|
+
entity: T,
|
|
23
|
+
): AugmentedEntity<any> {
|
|
24
|
+
const augmented = entity as any;
|
|
25
|
+
augmented.update = (data: any) => update(ctx, entityName, entity.id, data);
|
|
26
|
+
augmented.delete = () => deleteEntity(ctx, entityName, entity.id);
|
|
27
|
+
|
|
28
|
+
// Attach lazy relationship navigation
|
|
29
|
+
for (const rel of ctx.relationships) {
|
|
30
|
+
if (rel.from === entityName && rel.type === 'belongs-to') {
|
|
31
|
+
// book.author() → lazy load parent via author_id FK
|
|
32
|
+
augmented[rel.relationshipField] = () => {
|
|
33
|
+
const fkValue = entity[rel.foreignKey];
|
|
34
|
+
return fkValue ? getById(ctx, rel.to, fkValue) : null;
|
|
35
|
+
};
|
|
36
|
+
} else if (rel.from === entityName && rel.type === 'one-to-many') {
|
|
37
|
+
// author.books() → lazy load children
|
|
38
|
+
const belongsToRel = ctx.relationships.find(
|
|
39
|
+
r => r.type === 'belongs-to' && r.from === rel.to && r.to === rel.from
|
|
40
|
+
);
|
|
41
|
+
if (belongsToRel) {
|
|
42
|
+
const fk = belongsToRel.foreignKey;
|
|
43
|
+
augmented[rel.relationshipField] = () => {
|
|
44
|
+
return findMany(ctx, rel.to, { [fk]: entity.id });
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Auto-persist proxy: setting a field auto-updates the DB row
|
|
51
|
+
const storableFieldNames = new Set(getStorableFields(ctx.schemas[entityName]!).map(f => f.name));
|
|
52
|
+
return new Proxy(augmented, {
|
|
53
|
+
set: (target, prop: string, value) => {
|
|
54
|
+
if (storableFieldNames.has(prop) && target[prop] !== value) {
|
|
55
|
+
update(ctx, entityName, target.id, { [prop]: value });
|
|
56
|
+
}
|
|
57
|
+
target[prop] = value;
|
|
58
|
+
return true;
|
|
59
|
+
},
|
|
60
|
+
get: (target, prop, receiver) => Reflect.get(target, prop, receiver),
|
|
61
|
+
});
|
|
62
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sql-helpers.ts — SQL utility functions extracted from Database class.
|
|
3
|
+
*
|
|
4
|
+
* Contains WHERE clause building and other SQL-level helpers.
|
|
5
|
+
*/
|
|
6
|
+
import { transformForStorage } from './schema';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a parameterized WHERE clause from a conditions object.
|
|
10
|
+
*
|
|
11
|
+
* Supports:
|
|
12
|
+
* - Simple equality: `{ name: 'Alice' }`
|
|
13
|
+
* - Operators: `{ age: { $gt: 18 } }`
|
|
14
|
+
* - $in: `{ status: { $in: ['active', 'pending'] } }`
|
|
15
|
+
* - $or: `{ $or: [{ name: 'Alice' }, { name: 'Bob' }] }`
|
|
16
|
+
*/
|
|
17
|
+
export function buildWhereClause(conditions: Record<string, any>, tablePrefix?: string): { clause: string; values: any[] } {
|
|
18
|
+
const parts: string[] = [];
|
|
19
|
+
const values: any[] = [];
|
|
20
|
+
|
|
21
|
+
for (const key in conditions) {
|
|
22
|
+
if (key.startsWith('$')) {
|
|
23
|
+
if (key === '$or' && Array.isArray(conditions[key])) {
|
|
24
|
+
const orBranches = conditions[key] as Record<string, any>[];
|
|
25
|
+
const orParts: string[] = [];
|
|
26
|
+
for (const branch of orBranches) {
|
|
27
|
+
const sub = buildWhereClause(branch, tablePrefix);
|
|
28
|
+
if (sub.clause) {
|
|
29
|
+
orParts.push(`(${sub.clause.replace(/^WHERE /, '')})`);
|
|
30
|
+
values.push(...sub.values);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (orParts.length > 0) parts.push(`(${orParts.join(' OR ')})`);
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const value = conditions[key];
|
|
38
|
+
const fieldName = tablePrefix ? `"${tablePrefix}"."${key}"` : `"${key}"`;
|
|
39
|
+
|
|
40
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
|
|
41
|
+
const operator = Object.keys(value)[0];
|
|
42
|
+
if (!operator?.startsWith('$')) {
|
|
43
|
+
throw new Error(`Querying on nested object '${key}' not supported. Use operators like $gt.`);
|
|
44
|
+
}
|
|
45
|
+
const operand = value[operator];
|
|
46
|
+
|
|
47
|
+
if (operator === '$in') {
|
|
48
|
+
if (!Array.isArray(operand)) throw new Error(`$in for '${key}' requires an array`);
|
|
49
|
+
if (operand.length === 0) { parts.push('1 = 0'); continue; }
|
|
50
|
+
parts.push(`${fieldName} IN (${operand.map(() => '?').join(', ')})`);
|
|
51
|
+
values.push(...operand.map((v: any) => transformForStorage({ v }).v));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (operator === '$notIn') {
|
|
56
|
+
if (!Array.isArray(operand)) throw new Error(`$notIn for '${key}' requires an array`);
|
|
57
|
+
if (operand.length === 0) continue; // no-op: everything is "not in" an empty set
|
|
58
|
+
parts.push(`${fieldName} NOT IN (${operand.map(() => '?').join(', ')})`);
|
|
59
|
+
values.push(...operand.map((v: any) => transformForStorage({ v }).v));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (operator === '$like') {
|
|
64
|
+
parts.push(`${fieldName} LIKE ?`);
|
|
65
|
+
values.push(operand);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (operator === '$between') {
|
|
70
|
+
if (!Array.isArray(operand) || operand.length !== 2) throw new Error(`$between for '${key}' requires [min, max]`);
|
|
71
|
+
parts.push(`${fieldName} BETWEEN ? AND ?`);
|
|
72
|
+
values.push(transformForStorage({ v: operand[0] }).v, transformForStorage({ v: operand[1] }).v);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sqlOp = ({ $gt: '>', $gte: '>=', $lt: '<', $lte: '<=', $ne: '!=' } as Record<string, string>)[operator];
|
|
77
|
+
if (!sqlOp) throw new Error(`Unsupported operator '${operator}' on '${key}'`);
|
|
78
|
+
parts.push(`${fieldName} ${sqlOp} ?`);
|
|
79
|
+
values.push(transformForStorage({ operand }).operand);
|
|
80
|
+
} else {
|
|
81
|
+
parts.push(`${fieldName} = ?`);
|
|
82
|
+
values.push(transformForStorage({ value }).value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { clause: parts.length > 0 ? `WHERE ${parts.join(' AND ')}` : '', values };
|
|
87
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,14 +9,13 @@ export type { DatabaseType } from './database';
|
|
|
9
9
|
export type {
|
|
10
10
|
SchemaMap, DatabaseOptions, Relationship,
|
|
11
11
|
EntityAccessor, TypedAccessors, AugmentedEntity, UpdateBuilder,
|
|
12
|
-
InferSchema, EntityData, IndexDef,
|
|
12
|
+
InferSchema, EntityData, IndexDef, ChangeEvent,
|
|
13
13
|
ProxyColumns, ColumnRef,
|
|
14
14
|
} from './types';
|
|
15
15
|
|
|
16
16
|
export { z } from 'zod';
|
|
17
17
|
|
|
18
|
-
export { QueryBuilder } from './query
|
|
19
|
-
export { ColumnNode, type ProxyQueryResult } from './proxy-query';
|
|
18
|
+
export { QueryBuilder, ColumnNode, compileIQO, type ProxyQueryResult } from './query';
|
|
20
19
|
export {
|
|
21
20
|
type ASTNode, type WhereCallback, type SetCallback,
|
|
22
21
|
type TypedColumnProxy, type FunctionProxy, type Operators,
|
package/src/iqo.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
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';
|
|
16
|
+
|
|
17
|
+
export interface WhereCondition {
|
|
18
|
+
field: string;
|
|
19
|
+
operator: '=' | '>' | '>=' | '<' | '<=' | '!=' | 'IN' | 'LIKE' | 'NOT IN' | 'BETWEEN';
|
|
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
|
+
limit: number | null;
|
|
38
|
+
offset: number | null;
|
|
39
|
+
orderBy: { field: string; direction: OrderDirection }[];
|
|
40
|
+
includes: string[];
|
|
41
|
+
raw: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const OPERATOR_MAP: Record<WhereOperator, string> = {
|
|
45
|
+
$gt: '>',
|
|
46
|
+
$gte: '>=',
|
|
47
|
+
$lt: '<',
|
|
48
|
+
$lte: '<=',
|
|
49
|
+
$ne: '!=',
|
|
50
|
+
$in: 'IN',
|
|
51
|
+
$like: 'LIKE',
|
|
52
|
+
$notIn: 'NOT IN',
|
|
53
|
+
$between: 'BETWEEN',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function transformValueForStorage(value: any): any {
|
|
57
|
+
if (value instanceof Date) return value.toISOString();
|
|
58
|
+
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Compile an Internal Query Object into executable SQL + params.
|
|
64
|
+
* Handles SELECT, JOIN, WHERE (object + AST + $or), ORDER BY, LIMIT, OFFSET.
|
|
65
|
+
*/
|
|
66
|
+
export function compileIQO(tableName: string, iqo: IQO): { sql: string; params: any[] } {
|
|
67
|
+
const params: any[] = [];
|
|
68
|
+
|
|
69
|
+
// SELECT clause
|
|
70
|
+
const selectParts: string[] = [];
|
|
71
|
+
if (iqo.selects.length > 0) {
|
|
72
|
+
selectParts.push(...iqo.selects.map(s => `${tableName}.${s}`));
|
|
73
|
+
} else {
|
|
74
|
+
selectParts.push(`${tableName}.*`);
|
|
75
|
+
}
|
|
76
|
+
for (const j of iqo.joins) {
|
|
77
|
+
if (j.columns.length > 0) {
|
|
78
|
+
selectParts.push(...j.columns.map(c => `${j.table}.${c} AS ${j.table}_${c}`));
|
|
79
|
+
} else {
|
|
80
|
+
selectParts.push(`${j.table}.*`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let sql = `SELECT ${selectParts.join(', ')} FROM ${tableName}`;
|
|
85
|
+
|
|
86
|
+
// JOIN clauses
|
|
87
|
+
for (const j of iqo.joins) {
|
|
88
|
+
sql += ` JOIN ${j.table} ON ${tableName}.${j.fromCol} = ${j.table}.${j.toCol}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// WHERE clause — AST-based takes precedence if set
|
|
92
|
+
if (iqo.whereAST) {
|
|
93
|
+
const compiled = compileAST(iqo.whereAST);
|
|
94
|
+
sql += ` WHERE ${compiled.sql}`;
|
|
95
|
+
params.push(...compiled.params);
|
|
96
|
+
} else if (iqo.wheres.length > 0) {
|
|
97
|
+
const hasJoins = iqo.joins.length > 0;
|
|
98
|
+
const qualify = (field: string) =>
|
|
99
|
+
hasJoins && !field.includes('.') ? `${tableName}.${field}` : field;
|
|
100
|
+
|
|
101
|
+
const whereParts: string[] = [];
|
|
102
|
+
for (const w of iqo.wheres) {
|
|
103
|
+
if (w.operator === 'IN') {
|
|
104
|
+
const arr = w.value as any[];
|
|
105
|
+
if (arr.length === 0) {
|
|
106
|
+
whereParts.push('1 = 0');
|
|
107
|
+
} else {
|
|
108
|
+
const placeholders = arr.map(() => '?').join(', ');
|
|
109
|
+
whereParts.push(`${qualify(w.field)} IN (${placeholders})`);
|
|
110
|
+
params.push(...arr.map(transformValueForStorage));
|
|
111
|
+
}
|
|
112
|
+
} else if (w.operator === 'NOT IN') {
|
|
113
|
+
const arr = w.value as any[];
|
|
114
|
+
if (arr.length === 0) continue; // no-op
|
|
115
|
+
const placeholders = arr.map(() => '?').join(', ');
|
|
116
|
+
whereParts.push(`${qualify(w.field)} NOT IN (${placeholders})`);
|
|
117
|
+
params.push(...arr.map(transformValueForStorage));
|
|
118
|
+
} else if (w.operator === 'BETWEEN') {
|
|
119
|
+
const [min, max] = w.value as [any, any];
|
|
120
|
+
whereParts.push(`${qualify(w.field)} BETWEEN ? AND ?`);
|
|
121
|
+
params.push(transformValueForStorage(min), transformValueForStorage(max));
|
|
122
|
+
} else {
|
|
123
|
+
whereParts.push(`${qualify(w.field)} ${w.operator} ?`);
|
|
124
|
+
params.push(transformValueForStorage(w.value));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (whereParts.length > 0) {
|
|
128
|
+
sql += ` WHERE ${whereParts.join(' AND ')}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Append OR groups (from $or)
|
|
133
|
+
if (iqo.whereOrs.length > 0) {
|
|
134
|
+
for (const orGroup of iqo.whereOrs) {
|
|
135
|
+
const orParts: string[] = [];
|
|
136
|
+
for (const w of orGroup) {
|
|
137
|
+
if (w.operator === 'IN') {
|
|
138
|
+
const arr = w.value as any[];
|
|
139
|
+
if (arr.length === 0) {
|
|
140
|
+
orParts.push('1 = 0');
|
|
141
|
+
} else {
|
|
142
|
+
orParts.push(`${w.field} IN (${arr.map(() => '?').join(', ')})`);
|
|
143
|
+
params.push(...arr.map(transformValueForStorage));
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
orParts.push(`${w.field} ${w.operator} ?`);
|
|
147
|
+
params.push(transformValueForStorage(w.value));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (orParts.length > 0) {
|
|
151
|
+
const orClause = `(${orParts.join(' OR ')})`;
|
|
152
|
+
sql += sql.includes(' WHERE ') ? ` AND ${orClause}` : ` WHERE ${orClause}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// GROUP BY
|
|
158
|
+
if (iqo.groupBy.length > 0) {
|
|
159
|
+
sql += ` GROUP BY ${iqo.groupBy.join(', ')}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ORDER BY
|
|
163
|
+
if (iqo.orderBy.length > 0) {
|
|
164
|
+
const parts = iqo.orderBy.map(o => `${o.field} ${o.direction.toUpperCase()}`);
|
|
165
|
+
sql += ` ORDER BY ${parts.join(', ')}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (iqo.limit !== null) sql += ` LIMIT ${iqo.limit}`;
|
|
169
|
+
if (iqo.offset !== null) sql += ` OFFSET ${iqo.offset}`;
|
|
170
|
+
|
|
171
|
+
return { sql, params };
|
|
172
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* proxy
|
|
2
|
+
* proxy.ts — Proxy Query System
|
|
3
3
|
*
|
|
4
|
-
* Enables db.query(c => {
|
|
5
|
-
*
|
|
6
|
-
* which are then compiled into parameterized SQL.
|
|
4
|
+
* Enables `db.query(c => { ... })` for SQL-like multi-table queries
|
|
5
|
+
* using destructured proxied table references.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
import { z } from 'zod';
|
|
8
|
+
import type { z } from 'zod';
|
|
10
9
|
|
|
11
|
-
//
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// ColumnNode
|
|
12
|
+
// =============================================================================
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Represents a reference to a specific table column with an alias.
|
|
@@ -34,22 +35,16 @@ export class ColumnNode {
|
|
|
34
35
|
|
|
35
36
|
// ---------- SQL Quoting Helpers ----------
|
|
36
37
|
|
|
37
|
-
/** Quote an identifier with double quotes */
|
|
38
38
|
function q(name: string): string {
|
|
39
39
|
return `"${name}"`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/** Quote a fully qualified reference: alias.column */
|
|
43
42
|
function qRef(alias: string, column: string): string {
|
|
44
43
|
return `"${alias}"."${column}"`;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
// ---------- Table Proxy ----------
|
|
48
47
|
|
|
49
|
-
/**
|
|
50
|
-
* Creates a proxy representing a table with a given alias.
|
|
51
|
-
* Property access returns ColumnNode objects.
|
|
52
|
-
*/
|
|
53
48
|
function createTableProxy(
|
|
54
49
|
tableName: string,
|
|
55
50
|
alias: string,
|
|
@@ -82,10 +77,6 @@ interface AliasEntry {
|
|
|
82
77
|
proxy: Record<string, ColumnNode>;
|
|
83
78
|
}
|
|
84
79
|
|
|
85
|
-
/**
|
|
86
|
-
* Creates the root context proxy `c` that the user destructures.
|
|
87
|
-
* Each table access generates a unique alias.
|
|
88
|
-
*/
|
|
89
80
|
export function createContextProxy(
|
|
90
81
|
schemas: Record<string, z.ZodType<any>>,
|
|
91
82
|
): { proxy: Record<string, Record<string, ColumnNode>>; aliasMap: Map<string, AliasEntry[]> } {
|
|
@@ -106,7 +97,6 @@ export function createContextProxy(
|
|
|
106
97
|
const alias = `t${aliasCounter}`;
|
|
107
98
|
const tableProxy = createTableProxy(tableName, alias, columns);
|
|
108
99
|
|
|
109
|
-
// Track alias
|
|
110
100
|
const entries = aliases.get(tableName) || [];
|
|
111
101
|
entries.push({ tableName, alias, proxy: tableProxy });
|
|
112
102
|
aliases.set(tableName, entries);
|
|
@@ -118,7 +108,7 @@ export function createContextProxy(
|
|
|
118
108
|
return { proxy, aliasMap: aliases };
|
|
119
109
|
}
|
|
120
110
|
|
|
121
|
-
// ---------- Query Result
|
|
111
|
+
// ---------- Proxy Query Result ----------
|
|
122
112
|
|
|
123
113
|
type AnyColumn = ColumnNode | (ColumnNode & string);
|
|
124
114
|
|
|
@@ -132,31 +122,26 @@ export interface ProxyQueryResult {
|
|
|
132
122
|
groupBy?: (AnyColumn | undefined)[];
|
|
133
123
|
}
|
|
134
124
|
|
|
135
|
-
// ---------- Query Compiler ----------
|
|
125
|
+
// ---------- Proxy Query Compiler ----------
|
|
136
126
|
|
|
137
127
|
function isColumnNode(val: any): val is ColumnNode {
|
|
138
128
|
return val && typeof val === 'object' && val._type === 'COL';
|
|
139
129
|
}
|
|
140
130
|
|
|
141
|
-
/**
|
|
142
|
-
* Compile the result of the user's callback into SQL.
|
|
143
|
-
*/
|
|
144
131
|
export function compileProxyQuery(
|
|
145
132
|
queryResult: ProxyQueryResult,
|
|
146
133
|
aliasMap: Map<string, AliasEntry[]>,
|
|
147
134
|
): { sql: string; params: any[] } {
|
|
148
135
|
const params: any[] = [];
|
|
149
136
|
|
|
150
|
-
// Collect all tables/aliases referenced
|
|
151
137
|
const tablesUsed = new Map<string, { tableName: string; alias: string }>();
|
|
152
|
-
|
|
153
138
|
for (const [tableName, entries] of aliasMap) {
|
|
154
139
|
for (const entry of entries) {
|
|
155
140
|
tablesUsed.set(entry.alias, { tableName, alias: entry.alias });
|
|
156
141
|
}
|
|
157
142
|
}
|
|
158
143
|
|
|
159
|
-
//
|
|
144
|
+
// SELECT
|
|
160
145
|
const selectParts: string[] = [];
|
|
161
146
|
for (const [outputName, colOrValue] of Object.entries(queryResult.select)) {
|
|
162
147
|
if (isColumnNode(colOrValue)) {
|
|
@@ -166,20 +151,18 @@ export function compileProxyQuery(
|
|
|
166
151
|
selectParts.push(`${qRef(colOrValue.alias, colOrValue.column)} AS ${q(outputName)}`);
|
|
167
152
|
}
|
|
168
153
|
} else {
|
|
169
|
-
// Literal value
|
|
170
154
|
selectParts.push(`? AS ${q(outputName)}`);
|
|
171
155
|
params.push(colOrValue);
|
|
172
156
|
}
|
|
173
157
|
}
|
|
174
158
|
|
|
175
|
-
//
|
|
159
|
+
// FROM / JOIN
|
|
176
160
|
const allAliases = [...tablesUsed.values()];
|
|
177
161
|
if (allAliases.length === 0) throw new Error('No tables referenced in query.');
|
|
178
162
|
|
|
179
163
|
const primaryAlias = allAliases[0]!;
|
|
180
164
|
let sql = `SELECT ${selectParts.join(', ')} FROM ${q(primaryAlias.tableName)} ${q(primaryAlias.alias)}`;
|
|
181
165
|
|
|
182
|
-
// Process JOINs
|
|
183
166
|
if (queryResult.join) {
|
|
184
167
|
const joins: [ColumnNode, ColumnNode][] = Array.isArray(queryResult.join[0])
|
|
185
168
|
? queryResult.join as [ColumnNode, ColumnNode][]
|
|
@@ -188,23 +171,18 @@ export function compileProxyQuery(
|
|
|
188
171
|
for (const [left, right] of joins) {
|
|
189
172
|
const leftTable = tablesUsed.get(left.alias);
|
|
190
173
|
const rightTable = tablesUsed.get(right.alias);
|
|
191
|
-
|
|
192
174
|
if (!leftTable || !rightTable) throw new Error('Join references unknown table alias.');
|
|
193
|
-
|
|
194
175
|
const joinAlias = leftTable.alias === primaryAlias.alias ? rightTable : leftTable;
|
|
195
|
-
|
|
196
176
|
sql += ` JOIN ${q(joinAlias.tableName)} ${q(joinAlias.alias)} ON ${qRef(left.alias, left.column)} = ${qRef(right.alias, right.column)}`;
|
|
197
177
|
}
|
|
198
178
|
}
|
|
199
179
|
|
|
200
|
-
//
|
|
180
|
+
// WHERE
|
|
201
181
|
if (queryResult.where && Object.keys(queryResult.where).length > 0) {
|
|
202
182
|
const whereParts: string[] = [];
|
|
203
183
|
|
|
204
184
|
for (const [key, value] of Object.entries(queryResult.where)) {
|
|
205
185
|
let fieldRef: string;
|
|
206
|
-
|
|
207
|
-
// Match quoted alias.column pattern: "alias"."column"
|
|
208
186
|
const quotedMatch = key.match(/^"([^"]+)"\."([^"]+)"$/);
|
|
209
187
|
if (quotedMatch && tablesUsed.has(quotedMatch[1]!)) {
|
|
210
188
|
fieldRef = key;
|
|
@@ -223,8 +201,8 @@ export function compileProxyQuery(
|
|
|
223
201
|
params.push(...value);
|
|
224
202
|
}
|
|
225
203
|
} else if (typeof value === 'object' && value !== null && !(value instanceof Date)) {
|
|
226
|
-
for (const [
|
|
227
|
-
if (
|
|
204
|
+
for (const [pOp, operand] of Object.entries(value)) {
|
|
205
|
+
if (pOp === '$in') {
|
|
228
206
|
const arr = operand as any[];
|
|
229
207
|
if (arr.length === 0) {
|
|
230
208
|
whereParts.push('1 = 0');
|
|
@@ -238,8 +216,8 @@ export function compileProxyQuery(
|
|
|
238
216
|
const opMap: Record<string, string> = {
|
|
239
217
|
$gt: '>', $gte: '>=', $lt: '<', $lte: '<=', $ne: '!=',
|
|
240
218
|
};
|
|
241
|
-
const sqlOp = opMap[
|
|
242
|
-
if (!sqlOp) throw new Error(`Unsupported where operator: ${
|
|
219
|
+
const sqlOp = opMap[pOp];
|
|
220
|
+
if (!sqlOp) throw new Error(`Unsupported where operator: ${pOp}`);
|
|
243
221
|
whereParts.push(`${fieldRef} ${sqlOp} ?`);
|
|
244
222
|
params.push(operand);
|
|
245
223
|
}
|
|
@@ -254,7 +232,7 @@ export function compileProxyQuery(
|
|
|
254
232
|
}
|
|
255
233
|
}
|
|
256
234
|
|
|
257
|
-
//
|
|
235
|
+
// ORDER BY
|
|
258
236
|
if (queryResult.orderBy) {
|
|
259
237
|
const parts: string[] = [];
|
|
260
238
|
for (const [key, dir] of Object.entries(queryResult.orderBy)) {
|
|
@@ -272,33 +250,20 @@ export function compileProxyQuery(
|
|
|
272
250
|
}
|
|
273
251
|
}
|
|
274
252
|
|
|
275
|
-
//
|
|
253
|
+
// GROUP BY
|
|
276
254
|
if (queryResult.groupBy && queryResult.groupBy.length > 0) {
|
|
277
255
|
const parts = queryResult.groupBy.filter(Boolean).map(col => qRef(col!.alias, col!.column));
|
|
278
256
|
sql += ` GROUP BY ${parts.join(', ')}`;
|
|
279
257
|
}
|
|
280
258
|
|
|
281
|
-
//
|
|
282
|
-
if (queryResult.limit !== undefined) {
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
if (queryResult.offset !== undefined) {
|
|
286
|
-
sql += ` OFFSET ${queryResult.offset}`;
|
|
287
|
-
}
|
|
259
|
+
// LIMIT / OFFSET
|
|
260
|
+
if (queryResult.limit !== undefined) sql += ` LIMIT ${queryResult.limit}`;
|
|
261
|
+
if (queryResult.offset !== undefined) sql += ` OFFSET ${queryResult.offset}`;
|
|
288
262
|
|
|
289
263
|
return { sql, params };
|
|
290
264
|
}
|
|
291
265
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* The main `db.query(c => {...})` entry point.
|
|
296
|
-
*
|
|
297
|
-
* @param schemas The schema map for all registered tables.
|
|
298
|
-
* @param callback The user's query callback that receives the context proxy.
|
|
299
|
-
* @param executor A function that runs the compiled SQL and returns rows.
|
|
300
|
-
* @returns The query results.
|
|
301
|
-
*/
|
|
266
|
+
/** The main `db.query(c => {...})` entry point. */
|
|
302
267
|
export function executeProxyQuery<T>(
|
|
303
268
|
schemas: Record<string, z.ZodType<any>>,
|
|
304
269
|
callback: (ctx: any) => ProxyQueryResult,
|
|
@@ -309,4 +274,3 @@ export function executeProxyQuery<T>(
|
|
|
309
274
|
const { sql, params } = compileProxyQuery(queryResult, aliasMap);
|
|
310
275
|
return executor(sql, params);
|
|
311
276
|
}
|
|
312
|
-
|