@vertz/db 0.2.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 +923 -0
- package/dist/diagnostic/index.d.ts +41 -0
- package/dist/diagnostic/index.js +10 -0
- package/dist/index.d.ts +1346 -0
- package/dist/index.js +2010 -0
- package/dist/internals.d.ts +223 -0
- package/dist/internals.js +25 -0
- package/dist/plugin/index.d.ts +66 -0
- package/dist/plugin/index.js +66 -0
- package/dist/shared/chunk-3f2grpak.js +428 -0
- package/dist/shared/chunk-hrfdj0rr.js +13 -0
- package/dist/shared/chunk-wj026daz.js +86 -0
- package/dist/shared/chunk-xp022dyp.js +296 -0
- package/dist/sql/index.d.ts +213 -0
- package/dist/sql/index.js +64 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2010 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildDelete,
|
|
3
|
+
buildInsert,
|
|
4
|
+
buildSelect,
|
|
5
|
+
buildUpdate,
|
|
6
|
+
buildWhere
|
|
7
|
+
} from "./shared/chunk-3f2grpak.js";
|
|
8
|
+
import {
|
|
9
|
+
CheckConstraintError,
|
|
10
|
+
ConnectionError,
|
|
11
|
+
ConnectionPoolExhaustedError,
|
|
12
|
+
DbError,
|
|
13
|
+
ForeignKeyError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
NotNullError,
|
|
16
|
+
UniqueConstraintError,
|
|
17
|
+
executeQuery,
|
|
18
|
+
getPrimaryKeyColumns,
|
|
19
|
+
getTimestampColumns,
|
|
20
|
+
mapRow,
|
|
21
|
+
mapRows,
|
|
22
|
+
parsePgError,
|
|
23
|
+
resolveSelectColumns
|
|
24
|
+
} from "./shared/chunk-xp022dyp.js";
|
|
25
|
+
import {
|
|
26
|
+
camelToSnake
|
|
27
|
+
} from "./shared/chunk-hrfdj0rr.js";
|
|
28
|
+
import {
|
|
29
|
+
diagnoseError,
|
|
30
|
+
explainError,
|
|
31
|
+
formatDiagnostic
|
|
32
|
+
} from "./shared/chunk-wj026daz.js";
|
|
33
|
+
|
|
34
|
+
// src/migration/differ.ts
|
|
35
|
+
function columnSimilarity(a, b) {
|
|
36
|
+
let score = 0;
|
|
37
|
+
let total = 0;
|
|
38
|
+
total += 3;
|
|
39
|
+
if (a.type === b.type)
|
|
40
|
+
score += 3;
|
|
41
|
+
total += 1;
|
|
42
|
+
if (a.nullable === b.nullable)
|
|
43
|
+
score += 1;
|
|
44
|
+
total += 1;
|
|
45
|
+
if (a.primary === b.primary)
|
|
46
|
+
score += 1;
|
|
47
|
+
total += 1;
|
|
48
|
+
if (a.unique === b.unique)
|
|
49
|
+
score += 1;
|
|
50
|
+
return score / total;
|
|
51
|
+
}
|
|
52
|
+
function indexKey(columns) {
|
|
53
|
+
return columns.join(",");
|
|
54
|
+
}
|
|
55
|
+
function computeDiff(before, after) {
|
|
56
|
+
const changes = [];
|
|
57
|
+
for (const tableName of Object.keys(after.tables)) {
|
|
58
|
+
if (!(tableName in before.tables)) {
|
|
59
|
+
changes.push({ type: "table_added", table: tableName });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const tableName of Object.keys(before.tables)) {
|
|
63
|
+
if (!(tableName in after.tables)) {
|
|
64
|
+
changes.push({ type: "table_removed", table: tableName });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const tableName of Object.keys(after.tables)) {
|
|
68
|
+
if (!(tableName in before.tables))
|
|
69
|
+
continue;
|
|
70
|
+
const beforeTable = before.tables[tableName];
|
|
71
|
+
const afterTable = after.tables[tableName];
|
|
72
|
+
if (!beforeTable || !afterTable)
|
|
73
|
+
continue;
|
|
74
|
+
const removedCols = Object.keys(beforeTable.columns).filter((c) => !(c in afterTable.columns));
|
|
75
|
+
const addedCols = Object.keys(afterTable.columns).filter((c) => !(c in beforeTable.columns));
|
|
76
|
+
const renames = [];
|
|
77
|
+
const matchedRemoved = new Set;
|
|
78
|
+
const matchedAdded = new Set;
|
|
79
|
+
for (const removed of removedCols) {
|
|
80
|
+
const removedSnap = beforeTable.columns[removed];
|
|
81
|
+
if (!removedSnap)
|
|
82
|
+
continue;
|
|
83
|
+
let bestMatch = null;
|
|
84
|
+
let bestScore = 0;
|
|
85
|
+
for (const added of addedCols) {
|
|
86
|
+
if (matchedAdded.has(added))
|
|
87
|
+
continue;
|
|
88
|
+
const addedSnap = afterTable.columns[added];
|
|
89
|
+
if (!addedSnap)
|
|
90
|
+
continue;
|
|
91
|
+
const score = columnSimilarity(removedSnap, addedSnap);
|
|
92
|
+
if (score > bestScore) {
|
|
93
|
+
bestScore = score;
|
|
94
|
+
bestMatch = added;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (bestMatch && bestScore >= 0.7) {
|
|
98
|
+
renames.push({ oldCol: removed, newCol: bestMatch, confidence: bestScore });
|
|
99
|
+
matchedRemoved.add(removed);
|
|
100
|
+
matchedAdded.add(bestMatch);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const rename of renames) {
|
|
104
|
+
changes.push({
|
|
105
|
+
type: "column_renamed",
|
|
106
|
+
table: tableName,
|
|
107
|
+
oldColumn: rename.oldCol,
|
|
108
|
+
newColumn: rename.newCol,
|
|
109
|
+
confidence: rename.confidence
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
for (const colName of addedCols) {
|
|
113
|
+
if (!matchedAdded.has(colName)) {
|
|
114
|
+
changes.push({ type: "column_added", table: tableName, column: colName });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const colName of removedCols) {
|
|
118
|
+
if (!matchedRemoved.has(colName)) {
|
|
119
|
+
changes.push({ type: "column_removed", table: tableName, column: colName });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const colName of Object.keys(afterTable.columns)) {
|
|
123
|
+
if (!(colName in beforeTable.columns))
|
|
124
|
+
continue;
|
|
125
|
+
const beforeCol = beforeTable.columns[colName];
|
|
126
|
+
const afterCol = afterTable.columns[colName];
|
|
127
|
+
if (!beforeCol || !afterCol)
|
|
128
|
+
continue;
|
|
129
|
+
if (beforeCol.type !== afterCol.type || beforeCol.nullable !== afterCol.nullable || beforeCol.default !== afterCol.default) {
|
|
130
|
+
const change = {
|
|
131
|
+
type: "column_altered",
|
|
132
|
+
table: tableName,
|
|
133
|
+
column: colName
|
|
134
|
+
};
|
|
135
|
+
if (beforeCol.type !== afterCol.type) {
|
|
136
|
+
change.oldType = beforeCol.type;
|
|
137
|
+
change.newType = afterCol.type;
|
|
138
|
+
}
|
|
139
|
+
if (beforeCol.nullable !== afterCol.nullable) {
|
|
140
|
+
change.oldNullable = beforeCol.nullable;
|
|
141
|
+
change.newNullable = afterCol.nullable;
|
|
142
|
+
}
|
|
143
|
+
if (beforeCol.default !== afterCol.default) {
|
|
144
|
+
change.oldDefault = beforeCol.default;
|
|
145
|
+
change.newDefault = afterCol.default;
|
|
146
|
+
}
|
|
147
|
+
changes.push(change);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const beforeIndexKeys = new Set(beforeTable.indexes.map((i) => indexKey(i.columns)));
|
|
151
|
+
const afterIndexKeys = new Set(afterTable.indexes.map((i) => indexKey(i.columns)));
|
|
152
|
+
for (const idx of afterTable.indexes) {
|
|
153
|
+
const key = indexKey(idx.columns);
|
|
154
|
+
if (!beforeIndexKeys.has(key)) {
|
|
155
|
+
changes.push({ type: "index_added", table: tableName, columns: [...idx.columns] });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const idx of beforeTable.indexes) {
|
|
159
|
+
const key = indexKey(idx.columns);
|
|
160
|
+
if (!afterIndexKeys.has(key)) {
|
|
161
|
+
changes.push({ type: "index_removed", table: tableName, columns: [...idx.columns] });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const enumName of Object.keys(after.enums)) {
|
|
166
|
+
if (!(enumName in before.enums)) {
|
|
167
|
+
changes.push({ type: "enum_added", enumName });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const enumName of Object.keys(before.enums)) {
|
|
171
|
+
if (!(enumName in after.enums)) {
|
|
172
|
+
changes.push({ type: "enum_removed", enumName });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const enumName of Object.keys(after.enums)) {
|
|
176
|
+
if (!(enumName in before.enums))
|
|
177
|
+
continue;
|
|
178
|
+
const beforeVals = before.enums[enumName];
|
|
179
|
+
const afterVals = after.enums[enumName];
|
|
180
|
+
if (!beforeVals || !afterVals)
|
|
181
|
+
continue;
|
|
182
|
+
const beforeSet = new Set(beforeVals);
|
|
183
|
+
const afterSet = new Set(afterVals);
|
|
184
|
+
const addedValues = afterVals.filter((v) => !beforeSet.has(v));
|
|
185
|
+
const removedValues = beforeVals.filter((v) => !afterSet.has(v));
|
|
186
|
+
if (addedValues.length > 0 || removedValues.length > 0) {
|
|
187
|
+
changes.push({ type: "enum_altered", enumName, addedValues, removedValues });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { changes };
|
|
191
|
+
}
|
|
192
|
+
// src/migration/runner.ts
|
|
193
|
+
import { createHash } from "node:crypto";
|
|
194
|
+
var HISTORY_TABLE = "_vertz_migrations";
|
|
195
|
+
var CREATE_HISTORY_SQL = `
|
|
196
|
+
CREATE TABLE IF NOT EXISTS "${HISTORY_TABLE}" (
|
|
197
|
+
"id" serial PRIMARY KEY,
|
|
198
|
+
"name" text NOT NULL UNIQUE,
|
|
199
|
+
"checksum" text NOT NULL,
|
|
200
|
+
"applied_at" timestamp with time zone NOT NULL DEFAULT now()
|
|
201
|
+
);
|
|
202
|
+
`;
|
|
203
|
+
function computeChecksum(sql) {
|
|
204
|
+
return createHash("sha256").update(sql).digest("hex");
|
|
205
|
+
}
|
|
206
|
+
function parseMigrationName(filename) {
|
|
207
|
+
const match = filename.match(/^(\d+)_(.+)\.sql$/);
|
|
208
|
+
if (!match?.[1] || !match[2])
|
|
209
|
+
return null;
|
|
210
|
+
return {
|
|
211
|
+
timestamp: Number.parseInt(match[1], 10),
|
|
212
|
+
name: filename
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function createMigrationRunner() {
|
|
216
|
+
return {
|
|
217
|
+
async createHistoryTable(queryFn) {
|
|
218
|
+
await queryFn(CREATE_HISTORY_SQL, []);
|
|
219
|
+
},
|
|
220
|
+
async apply(queryFn, sql, name, options) {
|
|
221
|
+
const checksum = computeChecksum(sql);
|
|
222
|
+
const recordSql = `INSERT INTO "${HISTORY_TABLE}" ("name", "checksum") VALUES ($1, $2)`;
|
|
223
|
+
const statements = [sql, recordSql];
|
|
224
|
+
if (options?.dryRun) {
|
|
225
|
+
return {
|
|
226
|
+
name,
|
|
227
|
+
sql,
|
|
228
|
+
checksum,
|
|
229
|
+
dryRun: true,
|
|
230
|
+
statements
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
await queryFn(sql, []);
|
|
234
|
+
await queryFn(recordSql, [name, checksum]);
|
|
235
|
+
return {
|
|
236
|
+
name,
|
|
237
|
+
sql,
|
|
238
|
+
checksum,
|
|
239
|
+
dryRun: false,
|
|
240
|
+
statements
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
async getApplied(queryFn) {
|
|
244
|
+
const result = await queryFn(`SELECT "name", "checksum", "applied_at" FROM "${HISTORY_TABLE}" ORDER BY "id" ASC`, []);
|
|
245
|
+
return result.rows.map((row) => ({
|
|
246
|
+
name: row.name,
|
|
247
|
+
checksum: row.checksum,
|
|
248
|
+
appliedAt: new Date(row.applied_at)
|
|
249
|
+
}));
|
|
250
|
+
},
|
|
251
|
+
getPending(files, applied) {
|
|
252
|
+
const appliedNames = new Set(applied.map((a) => a.name));
|
|
253
|
+
return files.filter((f) => !appliedNames.has(f.name)).sort((a, b) => a.timestamp - b.timestamp);
|
|
254
|
+
},
|
|
255
|
+
detectDrift(files, applied) {
|
|
256
|
+
const drifted = [];
|
|
257
|
+
const appliedMap = new Map(applied.map((a) => [a.name, a.checksum]));
|
|
258
|
+
for (const file of files) {
|
|
259
|
+
const appliedChecksum = appliedMap.get(file.name);
|
|
260
|
+
if (appliedChecksum && appliedChecksum !== computeChecksum(file.sql)) {
|
|
261
|
+
drifted.push(file.name);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return drifted;
|
|
265
|
+
},
|
|
266
|
+
detectOutOfOrder(files, applied) {
|
|
267
|
+
if (applied.length === 0)
|
|
268
|
+
return [];
|
|
269
|
+
const appliedNames = new Set(applied.map((a) => a.name));
|
|
270
|
+
const lastApplied = applied[applied.length - 1];
|
|
271
|
+
if (!lastApplied)
|
|
272
|
+
return [];
|
|
273
|
+
const lastAppliedFile = files.find((f) => f.name === lastApplied.name);
|
|
274
|
+
if (!lastAppliedFile)
|
|
275
|
+
return [];
|
|
276
|
+
return files.filter((f) => !appliedNames.has(f.name) && f.timestamp < lastAppliedFile.timestamp).map((f) => f.name);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/migration/files.ts
|
|
282
|
+
function formatMigrationFilename(num, description) {
|
|
283
|
+
return `${String(num).padStart(4, "0")}_${description}.sql`;
|
|
284
|
+
}
|
|
285
|
+
function nextMigrationNumber(existingFiles) {
|
|
286
|
+
let max = 0;
|
|
287
|
+
for (const file of existingFiles) {
|
|
288
|
+
const parsed = parseMigrationName(file);
|
|
289
|
+
if (parsed && parsed.timestamp > max) {
|
|
290
|
+
max = parsed.timestamp;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return max + 1;
|
|
294
|
+
}
|
|
295
|
+
// src/migration/sql-generator.ts
|
|
296
|
+
function escapeSqlString(value) {
|
|
297
|
+
return value.replace(/'/g, "''");
|
|
298
|
+
}
|
|
299
|
+
function columnDef(name, col) {
|
|
300
|
+
const snakeName = camelToSnake(name);
|
|
301
|
+
const parts = [`"${snakeName}" ${col.type}`];
|
|
302
|
+
if (!col.nullable) {
|
|
303
|
+
parts.push("NOT NULL");
|
|
304
|
+
}
|
|
305
|
+
if (col.unique) {
|
|
306
|
+
parts.push("UNIQUE");
|
|
307
|
+
}
|
|
308
|
+
if (col.default !== undefined) {
|
|
309
|
+
parts.push(`DEFAULT ${col.default}`);
|
|
310
|
+
}
|
|
311
|
+
return parts.join(" ");
|
|
312
|
+
}
|
|
313
|
+
function generateMigrationSql(changes, ctx) {
|
|
314
|
+
const statements = [];
|
|
315
|
+
const tables = ctx?.tables;
|
|
316
|
+
const enums = ctx?.enums;
|
|
317
|
+
for (const change of changes) {
|
|
318
|
+
switch (change.type) {
|
|
319
|
+
case "table_added": {
|
|
320
|
+
if (!change.table)
|
|
321
|
+
break;
|
|
322
|
+
const table = tables?.[change.table];
|
|
323
|
+
if (!table)
|
|
324
|
+
break;
|
|
325
|
+
const tableName = camelToSnake(change.table);
|
|
326
|
+
const cols = [];
|
|
327
|
+
const primaryKeys = [];
|
|
328
|
+
for (const [colName, col] of Object.entries(table.columns)) {
|
|
329
|
+
cols.push(` ${columnDef(colName, col)}`);
|
|
330
|
+
if (col.primary) {
|
|
331
|
+
primaryKeys.push(`"${camelToSnake(colName)}"`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (primaryKeys.length > 0) {
|
|
335
|
+
cols.push(` PRIMARY KEY (${primaryKeys.join(", ")})`);
|
|
336
|
+
}
|
|
337
|
+
for (const fk of table.foreignKeys) {
|
|
338
|
+
const fkCol = camelToSnake(fk.column);
|
|
339
|
+
const fkTarget = camelToSnake(fk.targetTable);
|
|
340
|
+
const fkTargetCol = camelToSnake(fk.targetColumn);
|
|
341
|
+
cols.push(` FOREIGN KEY ("${fkCol}") REFERENCES "${fkTarget}" ("${fkTargetCol}")`);
|
|
342
|
+
}
|
|
343
|
+
statements.push(`CREATE TABLE "${tableName}" (
|
|
344
|
+
${cols.join(`,
|
|
345
|
+
`)}
|
|
346
|
+
);`);
|
|
347
|
+
for (const idx of table.indexes) {
|
|
348
|
+
const idxCols = idx.columns.map((c) => `"${camelToSnake(c)}"`).join(", ");
|
|
349
|
+
const idxName = `idx_${tableName}_${idx.columns.map(camelToSnake).join("_")}`;
|
|
350
|
+
statements.push(`CREATE INDEX "${idxName}" ON "${tableName}" (${idxCols});`);
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case "table_removed": {
|
|
355
|
+
if (!change.table)
|
|
356
|
+
break;
|
|
357
|
+
statements.push(`DROP TABLE "${camelToSnake(change.table)}";`);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case "column_added": {
|
|
361
|
+
if (!change.table || !change.column)
|
|
362
|
+
break;
|
|
363
|
+
const col = tables?.[change.table]?.columns[change.column];
|
|
364
|
+
if (!col)
|
|
365
|
+
break;
|
|
366
|
+
statements.push(`ALTER TABLE "${camelToSnake(change.table)}" ADD COLUMN ${columnDef(change.column, col)};`);
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
case "column_removed": {
|
|
370
|
+
if (!change.table || !change.column)
|
|
371
|
+
break;
|
|
372
|
+
statements.push(`ALTER TABLE "${camelToSnake(change.table)}" DROP COLUMN "${camelToSnake(change.column)}";`);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
case "column_altered": {
|
|
376
|
+
if (!change.table || !change.column)
|
|
377
|
+
break;
|
|
378
|
+
const snakeTable = camelToSnake(change.table);
|
|
379
|
+
const snakeCol = camelToSnake(change.column);
|
|
380
|
+
if (change.newType !== undefined) {
|
|
381
|
+
statements.push(`ALTER TABLE "${snakeTable}" ALTER COLUMN "${snakeCol}" TYPE ${change.newType};`);
|
|
382
|
+
}
|
|
383
|
+
if (change.newNullable !== undefined) {
|
|
384
|
+
if (change.newNullable) {
|
|
385
|
+
statements.push(`ALTER TABLE "${snakeTable}" ALTER COLUMN "${snakeCol}" DROP NOT NULL;`);
|
|
386
|
+
} else {
|
|
387
|
+
statements.push(`ALTER TABLE "${snakeTable}" ALTER COLUMN "${snakeCol}" SET NOT NULL;`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (change.newDefault !== undefined) {
|
|
391
|
+
if (change.newDefault) {
|
|
392
|
+
statements.push(`ALTER TABLE "${snakeTable}" ALTER COLUMN "${snakeCol}" SET DEFAULT ${change.newDefault};`);
|
|
393
|
+
} else {
|
|
394
|
+
statements.push(`ALTER TABLE "${snakeTable}" ALTER COLUMN "${snakeCol}" DROP DEFAULT;`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
case "column_renamed": {
|
|
400
|
+
if (!change.table || !change.oldColumn || !change.newColumn)
|
|
401
|
+
break;
|
|
402
|
+
statements.push(`ALTER TABLE "${camelToSnake(change.table)}" RENAME COLUMN "${camelToSnake(change.oldColumn)}" TO "${camelToSnake(change.newColumn)}";`);
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
case "index_added": {
|
|
406
|
+
if (!change.table || !change.columns)
|
|
407
|
+
break;
|
|
408
|
+
const snakeTable = camelToSnake(change.table);
|
|
409
|
+
const idxCols = change.columns.map((c) => `"${camelToSnake(c)}"`).join(", ");
|
|
410
|
+
const idxName = `idx_${snakeTable}_${change.columns.map(camelToSnake).join("_")}`;
|
|
411
|
+
statements.push(`CREATE INDEX "${idxName}" ON "${snakeTable}" (${idxCols});`);
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
case "index_removed": {
|
|
415
|
+
if (!change.table || !change.columns)
|
|
416
|
+
break;
|
|
417
|
+
const snakeTable = camelToSnake(change.table);
|
|
418
|
+
const idxName = `idx_${snakeTable}_${change.columns.map(camelToSnake).join("_")}`;
|
|
419
|
+
statements.push(`DROP INDEX "${idxName}";`);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case "enum_added": {
|
|
423
|
+
if (!change.enumName)
|
|
424
|
+
break;
|
|
425
|
+
const values = enums?.[change.enumName];
|
|
426
|
+
if (!values || values.length === 0)
|
|
427
|
+
break;
|
|
428
|
+
const enumSnakeName = camelToSnake(change.enumName);
|
|
429
|
+
const valuesStr = values.map((v) => `'${escapeSqlString(v)}'`).join(", ");
|
|
430
|
+
statements.push(`CREATE TYPE "${enumSnakeName}" AS ENUM (${valuesStr});`);
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
case "enum_removed": {
|
|
434
|
+
if (!change.enumName)
|
|
435
|
+
break;
|
|
436
|
+
statements.push(`DROP TYPE "${camelToSnake(change.enumName)}";`);
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
case "enum_altered": {
|
|
440
|
+
if (!change.enumName || !change.addedValues)
|
|
441
|
+
break;
|
|
442
|
+
const enumSnakeName = camelToSnake(change.enumName);
|
|
443
|
+
for (const val of change.addedValues) {
|
|
444
|
+
statements.push(`ALTER TYPE "${enumSnakeName}" ADD VALUE '${escapeSqlString(val)}';`);
|
|
445
|
+
}
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return statements.join(`
|
|
451
|
+
|
|
452
|
+
`);
|
|
453
|
+
}
|
|
454
|
+
// src/cli/migrate-deploy.ts
|
|
455
|
+
async function migrateDeploy(options) {
|
|
456
|
+
const runner = createMigrationRunner();
|
|
457
|
+
const isDryRun = options.dryRun ?? false;
|
|
458
|
+
if (!isDryRun) {
|
|
459
|
+
await runner.createHistoryTable(options.queryFn);
|
|
460
|
+
}
|
|
461
|
+
let applied;
|
|
462
|
+
if (isDryRun) {
|
|
463
|
+
try {
|
|
464
|
+
applied = await runner.getApplied(options.queryFn);
|
|
465
|
+
} catch {
|
|
466
|
+
applied = [];
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
applied = await runner.getApplied(options.queryFn);
|
|
470
|
+
}
|
|
471
|
+
const pending = runner.getPending(options.migrationFiles, applied);
|
|
472
|
+
const appliedNames = [];
|
|
473
|
+
const migrationResults = [];
|
|
474
|
+
for (const migration of pending) {
|
|
475
|
+
const result = await runner.apply(options.queryFn, migration.sql, migration.name, {
|
|
476
|
+
dryRun: isDryRun
|
|
477
|
+
});
|
|
478
|
+
appliedNames.push(migration.name);
|
|
479
|
+
migrationResults.push(result);
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
applied: appliedNames,
|
|
483
|
+
alreadyApplied: applied.map((a) => a.name),
|
|
484
|
+
dryRun: isDryRun,
|
|
485
|
+
migrations: migrationResults.length > 0 ? migrationResults : undefined
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// src/cli/migrate-dev.ts
|
|
489
|
+
async function migrateDev(options) {
|
|
490
|
+
const diff = computeDiff(options.previousSnapshot, options.currentSnapshot);
|
|
491
|
+
const sql = generateMigrationSql(diff.changes, {
|
|
492
|
+
tables: options.currentSnapshot.tables,
|
|
493
|
+
enums: options.currentSnapshot.enums
|
|
494
|
+
});
|
|
495
|
+
const renames = diff.changes.filter((c) => c.type === "column_renamed").map((c) => ({
|
|
496
|
+
table: c.table,
|
|
497
|
+
oldColumn: c.oldColumn,
|
|
498
|
+
newColumn: c.newColumn,
|
|
499
|
+
confidence: c.confidence
|
|
500
|
+
}));
|
|
501
|
+
const num = nextMigrationNumber(options.existingFiles);
|
|
502
|
+
const filename = formatMigrationFilename(num, options.migrationName);
|
|
503
|
+
const filePath = `${options.migrationsDir}/${filename}`;
|
|
504
|
+
if (options.dryRun) {
|
|
505
|
+
return {
|
|
506
|
+
migrationFile: filename,
|
|
507
|
+
sql,
|
|
508
|
+
dryRun: true,
|
|
509
|
+
renames: renames.length > 0 ? renames : undefined,
|
|
510
|
+
snapshot: options.currentSnapshot
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
await options.writeFile(filePath, sql);
|
|
514
|
+
const runner = createMigrationRunner();
|
|
515
|
+
await runner.createHistoryTable(options.queryFn);
|
|
516
|
+
await runner.apply(options.queryFn, sql, filename, { dryRun: false });
|
|
517
|
+
return {
|
|
518
|
+
migrationFile: filename,
|
|
519
|
+
sql,
|
|
520
|
+
appliedAt: new Date,
|
|
521
|
+
dryRun: false,
|
|
522
|
+
renames: renames.length > 0 ? renames : undefined,
|
|
523
|
+
snapshot: options.currentSnapshot
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
// src/cli/push.ts
|
|
527
|
+
async function push(options) {
|
|
528
|
+
const diff = computeDiff(options.previousSnapshot, options.currentSnapshot);
|
|
529
|
+
const sql = generateMigrationSql(diff.changes, {
|
|
530
|
+
tables: options.currentSnapshot.tables,
|
|
531
|
+
enums: options.currentSnapshot.enums
|
|
532
|
+
});
|
|
533
|
+
if (sql.length > 0) {
|
|
534
|
+
await options.queryFn(sql, []);
|
|
535
|
+
}
|
|
536
|
+
const tablesAffected = [
|
|
537
|
+
...new Set(diff.changes.map((c) => c.table).filter((t) => t !== undefined))
|
|
538
|
+
];
|
|
539
|
+
return { sql, tablesAffected };
|
|
540
|
+
}
|
|
541
|
+
// src/cli/status.ts
|
|
542
|
+
async function migrateStatus(options) {
|
|
543
|
+
const runner = createMigrationRunner();
|
|
544
|
+
await runner.createHistoryTable(options.queryFn);
|
|
545
|
+
const applied = await runner.getApplied(options.queryFn);
|
|
546
|
+
const pending = runner.getPending(options.migrationFiles, applied);
|
|
547
|
+
return {
|
|
548
|
+
applied: applied.map((a) => ({
|
|
549
|
+
name: a.name,
|
|
550
|
+
checksum: a.checksum,
|
|
551
|
+
appliedAt: a.appliedAt
|
|
552
|
+
})),
|
|
553
|
+
pending: pending.map((p) => p.name)
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
// src/query/aggregate.ts
|
|
557
|
+
async function count(queryFn, table, options) {
|
|
558
|
+
const allParams = [];
|
|
559
|
+
let sql = `SELECT COUNT(*) AS "count" FROM "${table._name}"`;
|
|
560
|
+
if (options?.where) {
|
|
561
|
+
const whereResult = buildWhere(options.where);
|
|
562
|
+
if (whereResult.sql.length > 0) {
|
|
563
|
+
sql += ` WHERE ${whereResult.sql}`;
|
|
564
|
+
allParams.push(...whereResult.params);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const res = await executeQuery(queryFn, sql, allParams);
|
|
568
|
+
const row = res.rows[0];
|
|
569
|
+
return Number(row?.count ?? 0);
|
|
570
|
+
}
|
|
571
|
+
async function aggregate(queryFn, table, options) {
|
|
572
|
+
const selectParts = [];
|
|
573
|
+
const aggFunctions = [];
|
|
574
|
+
if (options._count !== undefined) {
|
|
575
|
+
if (options._count === true) {
|
|
576
|
+
selectParts.push('COUNT(*) AS "_count"');
|
|
577
|
+
aggFunctions.push({ fn: "count", columns: null, key: "_count" });
|
|
578
|
+
} else {
|
|
579
|
+
for (const col of Object.keys(options._count)) {
|
|
580
|
+
const snakeCol = camelToSnake(col);
|
|
581
|
+
selectParts.push(`COUNT("${snakeCol}") AS "_count_${snakeCol}"`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
for (const [fn, aggOpt] of [
|
|
586
|
+
["AVG", options._avg],
|
|
587
|
+
["SUM", options._sum],
|
|
588
|
+
["MIN", options._min],
|
|
589
|
+
["MAX", options._max]
|
|
590
|
+
]) {
|
|
591
|
+
if (!aggOpt)
|
|
592
|
+
continue;
|
|
593
|
+
for (const col of Object.keys(aggOpt)) {
|
|
594
|
+
const snakeCol = camelToSnake(col);
|
|
595
|
+
const alias = `_${fn.toLowerCase()}_${snakeCol}`;
|
|
596
|
+
selectParts.push(`${fn}("${snakeCol}") AS "${alias}"`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (selectParts.length === 0) {
|
|
600
|
+
return {};
|
|
601
|
+
}
|
|
602
|
+
const allParams = [];
|
|
603
|
+
let sql = `SELECT ${selectParts.join(", ")} FROM "${table._name}"`;
|
|
604
|
+
if (options.where) {
|
|
605
|
+
const whereResult = buildWhere(options.where);
|
|
606
|
+
if (whereResult.sql.length > 0) {
|
|
607
|
+
sql += ` WHERE ${whereResult.sql}`;
|
|
608
|
+
allParams.push(...whereResult.params);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const res = await executeQuery(queryFn, sql, allParams);
|
|
612
|
+
const row = res.rows[0];
|
|
613
|
+
if (!row)
|
|
614
|
+
return {};
|
|
615
|
+
const result = {};
|
|
616
|
+
if (options._count !== undefined) {
|
|
617
|
+
if (options._count === true) {
|
|
618
|
+
result._count = Number(row._count ?? 0);
|
|
619
|
+
} else {
|
|
620
|
+
const countObj = {};
|
|
621
|
+
for (const col of Object.keys(options._count)) {
|
|
622
|
+
const snakeCol = camelToSnake(col);
|
|
623
|
+
countObj[col] = Number(row[`_count_${snakeCol}`] ?? 0);
|
|
624
|
+
}
|
|
625
|
+
result._count = countObj;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
for (const [fn, aggOpt] of [
|
|
629
|
+
["avg", options._avg],
|
|
630
|
+
["sum", options._sum],
|
|
631
|
+
["min", options._min],
|
|
632
|
+
["max", options._max]
|
|
633
|
+
]) {
|
|
634
|
+
if (!aggOpt)
|
|
635
|
+
continue;
|
|
636
|
+
const fnObj = {};
|
|
637
|
+
for (const col of Object.keys(aggOpt)) {
|
|
638
|
+
const snakeCol = camelToSnake(col);
|
|
639
|
+
const val = row[`_${fn}_${snakeCol}`];
|
|
640
|
+
fnObj[col] = val === null || val === undefined ? null : Number(val);
|
|
641
|
+
}
|
|
642
|
+
result[`_${fn}`] = fnObj;
|
|
643
|
+
}
|
|
644
|
+
return result;
|
|
645
|
+
}
|
|
646
|
+
async function groupBy(queryFn, table, options) {
|
|
647
|
+
const selectParts = [];
|
|
648
|
+
const groupCols = [];
|
|
649
|
+
for (const col of options.by) {
|
|
650
|
+
const snakeCol = camelToSnake(col);
|
|
651
|
+
if (snakeCol === col) {
|
|
652
|
+
selectParts.push(`"${col}"`);
|
|
653
|
+
} else {
|
|
654
|
+
selectParts.push(`"${snakeCol}" AS "${col}"`);
|
|
655
|
+
}
|
|
656
|
+
groupCols.push(`"${snakeCol}"`);
|
|
657
|
+
}
|
|
658
|
+
if (options._count !== undefined) {
|
|
659
|
+
if (options._count === true) {
|
|
660
|
+
selectParts.push('COUNT(*) AS "_count"');
|
|
661
|
+
} else {
|
|
662
|
+
for (const col of Object.keys(options._count)) {
|
|
663
|
+
const snakeCol = camelToSnake(col);
|
|
664
|
+
selectParts.push(`COUNT("${snakeCol}") AS "_count_${snakeCol}"`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
for (const [fn, aggOpt] of [
|
|
669
|
+
["AVG", options._avg],
|
|
670
|
+
["SUM", options._sum],
|
|
671
|
+
["MIN", options._min],
|
|
672
|
+
["MAX", options._max]
|
|
673
|
+
]) {
|
|
674
|
+
if (!aggOpt)
|
|
675
|
+
continue;
|
|
676
|
+
for (const col of Object.keys(aggOpt)) {
|
|
677
|
+
const snakeCol = camelToSnake(col);
|
|
678
|
+
const alias = `_${fn.toLowerCase()}_${snakeCol}`;
|
|
679
|
+
selectParts.push(`${fn}("${snakeCol}") AS "${alias}"`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const allParams = [];
|
|
683
|
+
let sql = `SELECT ${selectParts.join(", ")} FROM "${table._name}"`;
|
|
684
|
+
if (options.where) {
|
|
685
|
+
const whereResult = buildWhere(options.where);
|
|
686
|
+
if (whereResult.sql.length > 0) {
|
|
687
|
+
sql += ` WHERE ${whereResult.sql}`;
|
|
688
|
+
allParams.push(...whereResult.params);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
sql += ` GROUP BY ${groupCols.join(", ")}`;
|
|
692
|
+
if (options.orderBy) {
|
|
693
|
+
const validAggAliases = new Set;
|
|
694
|
+
validAggAliases.add("_count");
|
|
695
|
+
if (options._count !== undefined && options._count !== true) {
|
|
696
|
+
for (const col of Object.keys(options._count)) {
|
|
697
|
+
validAggAliases.add(`_count_${camelToSnake(col)}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
for (const [fn, aggOpt] of [
|
|
701
|
+
["avg", options._avg],
|
|
702
|
+
["sum", options._sum],
|
|
703
|
+
["min", options._min],
|
|
704
|
+
["max", options._max]
|
|
705
|
+
]) {
|
|
706
|
+
if (!aggOpt)
|
|
707
|
+
continue;
|
|
708
|
+
for (const col of Object.keys(aggOpt)) {
|
|
709
|
+
validAggAliases.add(`_${fn}_${camelToSnake(col)}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const orderClauses = [];
|
|
713
|
+
for (const [col, dir] of Object.entries(options.orderBy)) {
|
|
714
|
+
const normalizedDir = dir.toLowerCase();
|
|
715
|
+
if (normalizedDir !== "asc" && normalizedDir !== "desc") {
|
|
716
|
+
throw new Error(`Invalid orderBy direction "${dir}". Only 'asc' or 'desc' are allowed.`);
|
|
717
|
+
}
|
|
718
|
+
const safeDir = normalizedDir === "desc" ? "DESC" : "ASC";
|
|
719
|
+
if (col === "_count") {
|
|
720
|
+
orderClauses.push(`COUNT(*) ${safeDir}`);
|
|
721
|
+
} else if (col.startsWith("_")) {
|
|
722
|
+
if (!validAggAliases.has(col)) {
|
|
723
|
+
throw new Error(`Invalid orderBy column "${col}". Underscore-prefixed columns must match a requested aggregation alias.`);
|
|
724
|
+
}
|
|
725
|
+
orderClauses.push(`"${col}" ${safeDir}`);
|
|
726
|
+
} else {
|
|
727
|
+
orderClauses.push(`"${camelToSnake(col)}" ${safeDir}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (orderClauses.length > 0) {
|
|
731
|
+
sql += ` ORDER BY ${orderClauses.join(", ")}`;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (options.limit !== undefined) {
|
|
735
|
+
allParams.push(options.limit);
|
|
736
|
+
sql += ` LIMIT $${allParams.length}`;
|
|
737
|
+
}
|
|
738
|
+
if (options.offset !== undefined) {
|
|
739
|
+
allParams.push(options.offset);
|
|
740
|
+
sql += ` OFFSET $${allParams.length}`;
|
|
741
|
+
}
|
|
742
|
+
const res = await executeQuery(queryFn, sql, allParams);
|
|
743
|
+
return res.rows.map((row) => {
|
|
744
|
+
const result = {};
|
|
745
|
+
for (const col of options.by) {
|
|
746
|
+
const snakeCol = camelToSnake(col);
|
|
747
|
+
result[col] = row[col] ?? row[snakeCol];
|
|
748
|
+
}
|
|
749
|
+
if (options._count !== undefined) {
|
|
750
|
+
if (options._count === true) {
|
|
751
|
+
result._count = Number(row._count ?? 0);
|
|
752
|
+
} else {
|
|
753
|
+
const countObj = {};
|
|
754
|
+
for (const col of Object.keys(options._count)) {
|
|
755
|
+
const snakeCol = camelToSnake(col);
|
|
756
|
+
countObj[col] = Number(row[`_count_${snakeCol}`] ?? 0);
|
|
757
|
+
}
|
|
758
|
+
result._count = countObj;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
for (const [fn, aggOpt] of [
|
|
762
|
+
["avg", options._avg],
|
|
763
|
+
["sum", options._sum],
|
|
764
|
+
["min", options._min],
|
|
765
|
+
["max", options._max]
|
|
766
|
+
]) {
|
|
767
|
+
if (!aggOpt)
|
|
768
|
+
continue;
|
|
769
|
+
const fnObj = {};
|
|
770
|
+
for (const col of Object.keys(aggOpt)) {
|
|
771
|
+
const snakeCol = camelToSnake(col);
|
|
772
|
+
const val = row[`_${fn}_${snakeCol}`];
|
|
773
|
+
fnObj[col] = val === null || val === undefined ? null : Number(val);
|
|
774
|
+
}
|
|
775
|
+
result[`_${fn}`] = fnObj;
|
|
776
|
+
}
|
|
777
|
+
return result;
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/query/crud.ts
|
|
782
|
+
function assertNonEmptyWhere(where, operation) {
|
|
783
|
+
if (Object.keys(where).length === 0) {
|
|
784
|
+
throw new Error(`${operation} requires a non-empty where clause. ` + "Passing an empty where object would affect all rows.");
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
async function get(queryFn, table, options) {
|
|
788
|
+
const columns = resolveSelectColumns(table, options?.select);
|
|
789
|
+
const result = buildSelect({
|
|
790
|
+
table: table._name,
|
|
791
|
+
columns,
|
|
792
|
+
where: options?.where,
|
|
793
|
+
orderBy: options?.orderBy,
|
|
794
|
+
limit: 1
|
|
795
|
+
});
|
|
796
|
+
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
797
|
+
if (res.rows.length === 0) {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
return mapRow(res.rows[0]);
|
|
801
|
+
}
|
|
802
|
+
async function getOrThrow(queryFn, table, options) {
|
|
803
|
+
const row = await get(queryFn, table, options);
|
|
804
|
+
if (row === null) {
|
|
805
|
+
throw new NotFoundError(table._name);
|
|
806
|
+
}
|
|
807
|
+
return row;
|
|
808
|
+
}
|
|
809
|
+
async function list(queryFn, table, options) {
|
|
810
|
+
const columns = resolveSelectColumns(table, options?.select);
|
|
811
|
+
const result = buildSelect({
|
|
812
|
+
table: table._name,
|
|
813
|
+
columns,
|
|
814
|
+
where: options?.where,
|
|
815
|
+
orderBy: options?.orderBy,
|
|
816
|
+
limit: options?.limit,
|
|
817
|
+
offset: options?.offset,
|
|
818
|
+
cursor: options?.cursor,
|
|
819
|
+
take: options?.take
|
|
820
|
+
});
|
|
821
|
+
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
822
|
+
return mapRows(res.rows);
|
|
823
|
+
}
|
|
824
|
+
async function listAndCount(queryFn, table, options) {
|
|
825
|
+
const columns = resolveSelectColumns(table, options?.select);
|
|
826
|
+
const result = buildSelect({
|
|
827
|
+
table: table._name,
|
|
828
|
+
columns,
|
|
829
|
+
where: options?.where,
|
|
830
|
+
orderBy: options?.orderBy,
|
|
831
|
+
limit: options?.limit,
|
|
832
|
+
offset: options?.offset,
|
|
833
|
+
cursor: options?.cursor,
|
|
834
|
+
take: options?.take,
|
|
835
|
+
withCount: true
|
|
836
|
+
});
|
|
837
|
+
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
838
|
+
const rows = res.rows;
|
|
839
|
+
if (rows.length === 0) {
|
|
840
|
+
return { data: [], total: 0 };
|
|
841
|
+
}
|
|
842
|
+
const firstRow = rows[0];
|
|
843
|
+
const total = Number(firstRow.totalCount ?? 0);
|
|
844
|
+
const data = rows.map((row) => {
|
|
845
|
+
const { totalCount: _tc, total_count: _tc2, ...rest } = row;
|
|
846
|
+
return mapRow(rest);
|
|
847
|
+
});
|
|
848
|
+
return { data, total };
|
|
849
|
+
}
|
|
850
|
+
async function create(queryFn, table, options) {
|
|
851
|
+
const returningColumns = resolveSelectColumns(table, options.select);
|
|
852
|
+
const nowColumns = getTimestampColumns(table);
|
|
853
|
+
const result = buildInsert({
|
|
854
|
+
table: table._name,
|
|
855
|
+
data: options.data,
|
|
856
|
+
returning: returningColumns,
|
|
857
|
+
nowColumns
|
|
858
|
+
});
|
|
859
|
+
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
860
|
+
return mapRow(res.rows[0]);
|
|
861
|
+
}
|
|
862
|
+
async function createMany(queryFn, table, options) {
|
|
863
|
+
if (options.data.length === 0) {
|
|
864
|
+
return { count: 0 };
|
|
865
|
+
}
|
|
866
|
+
const nowColumns = getTimestampColumns(table);
|
|
867
|
+
const result = buildInsert({
|
|
868
|
+
table: table._name,
|
|
869
|
+
data: options.data,
|
|
870
|
+
nowColumns
|
|
871
|
+
});
|
|
872
|
+
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
873
|
+
return { count: res.rowCount };
|
|
874
|
+
}
|
|
875
|
+
async function createManyAndReturn(queryFn, table, options) {
|
|
876
|
+
if (options.data.length === 0) {
|
|
877
|
+
return [];
|
|
878
|
+
}
|
|
879
|
+
const returningColumns = resolveSelectColumns(table, options.select);
|
|
880
|
+
const nowColumns = getTimestampColumns(table);
|
|
881
|
+
const result = buildInsert({
|
|
882
|
+
table: table._name,
|
|
883
|
+
data: options.data,
|
|
884
|
+
returning: returningColumns,
|
|
885
|
+
nowColumns
|
|
886
|
+
});
|
|
887
|
+
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
888
|
+
return mapRows(res.rows);
|
|
889
|
+
}
|
|
890
|
+
async function update(queryFn, table, options) {
|
|
891
|
+
const returningColumns = resolveSelectColumns(table, options.select);
|
|
892
|
+
const nowColumns = getTimestampColumns(table);
|
|
893
|
+
const result = buildUpdate({
|
|
894
|
+
table: table._name,
|
|
895
|
+
data: options.data,
|
|
896
|
+
where: options.where,
|
|
897
|
+
returning: returningColumns,
|
|
898
|
+
nowColumns
|
|
899
|
+
});
|
|
900
|
+
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
901
|
+
if (res.rows.length === 0) {
|
|
902
|
+
throw new NotFoundError(table._name);
|
|
903
|
+
}
|
|
904
|
+
return mapRow(res.rows[0]);
|
|
905
|
+
}
|
|
906
|
+
async function updateMany(queryFn, table, options) {
|
|
907
|
+
assertNonEmptyWhere(options.where, "updateMany");
|
|
908
|
+
const nowColumns = getTimestampColumns(table);
|
|
909
|
+
const result = buildUpdate({
|
|
910
|
+
table: table._name,
|
|
911
|
+
data: options.data,
|
|
912
|
+
where: options.where,
|
|
913
|
+
nowColumns
|
|
914
|
+
});
|
|
915
|
+
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
916
|
+
return { count: res.rowCount };
|
|
917
|
+
}
|
|
918
|
+
async function upsert(queryFn, table, options) {
|
|
919
|
+
const returningColumns = resolveSelectColumns(table, options.select);
|
|
920
|
+
const nowColumns = getTimestampColumns(table);
|
|
921
|
+
const conflictColumns = Object.keys(options.where);
|
|
922
|
+
const updateColumns = Object.keys(options.update);
|
|
923
|
+
const result = buildInsert({
|
|
924
|
+
table: table._name,
|
|
925
|
+
data: options.create,
|
|
926
|
+
returning: returningColumns,
|
|
927
|
+
nowColumns,
|
|
928
|
+
onConflict: {
|
|
929
|
+
columns: conflictColumns,
|
|
930
|
+
action: "update",
|
|
931
|
+
updateColumns,
|
|
932
|
+
updateValues: options.update
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
936
|
+
return mapRow(res.rows[0]);
|
|
937
|
+
}
|
|
938
|
+
async function deleteOne(queryFn, table, options) {
|
|
939
|
+
const returningColumns = resolveSelectColumns(table, options.select);
|
|
940
|
+
const result = buildDelete({
|
|
941
|
+
table: table._name,
|
|
942
|
+
where: options.where,
|
|
943
|
+
returning: returningColumns
|
|
944
|
+
});
|
|
945
|
+
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
946
|
+
if (res.rows.length === 0) {
|
|
947
|
+
throw new NotFoundError(table._name);
|
|
948
|
+
}
|
|
949
|
+
return mapRow(res.rows[0]);
|
|
950
|
+
}
|
|
951
|
+
async function deleteMany(queryFn, table, options) {
|
|
952
|
+
assertNonEmptyWhere(options.where, "deleteMany");
|
|
953
|
+
const result = buildDelete({
|
|
954
|
+
table: table._name,
|
|
955
|
+
where: options.where
|
|
956
|
+
});
|
|
957
|
+
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
958
|
+
return { count: res.rowCount };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/query/relation-loader.ts
|
|
962
|
+
function resolvePkColumn(table) {
|
|
963
|
+
const pkCols = getPrimaryKeyColumns(table);
|
|
964
|
+
const first = pkCols[0];
|
|
965
|
+
return first !== undefined ? first : "id";
|
|
966
|
+
}
|
|
967
|
+
async function loadRelations(queryFn, primaryRows, relations, include, depth = 0, tablesRegistry, primaryTable) {
|
|
968
|
+
if (depth > 2 || primaryRows.length === 0) {
|
|
969
|
+
return primaryRows;
|
|
970
|
+
}
|
|
971
|
+
const toLoad = [];
|
|
972
|
+
for (const [key, value] of Object.entries(include)) {
|
|
973
|
+
if (value === undefined)
|
|
974
|
+
continue;
|
|
975
|
+
const rel = relations[key];
|
|
976
|
+
if (!rel)
|
|
977
|
+
continue;
|
|
978
|
+
toLoad.push({ key, def: rel, includeValue: value });
|
|
979
|
+
}
|
|
980
|
+
if (toLoad.length === 0) {
|
|
981
|
+
return primaryRows;
|
|
982
|
+
}
|
|
983
|
+
for (const { key: relName, def, includeValue } of toLoad) {
|
|
984
|
+
const target = def._target();
|
|
985
|
+
if (def._type === "one") {
|
|
986
|
+
await loadOneRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry);
|
|
987
|
+
} else if (def._through) {
|
|
988
|
+
await loadManyToManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable);
|
|
989
|
+
} else {
|
|
990
|
+
await loadManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return primaryRows;
|
|
994
|
+
}
|
|
995
|
+
function findTargetRelations(target, tablesRegistry) {
|
|
996
|
+
if (!tablesRegistry)
|
|
997
|
+
return;
|
|
998
|
+
for (const entry of Object.values(tablesRegistry)) {
|
|
999
|
+
if (entry.table._name === target._name) {
|
|
1000
|
+
return entry.relations;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
async function loadOneRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry) {
|
|
1006
|
+
const fk = def._foreignKey;
|
|
1007
|
+
if (!fk)
|
|
1008
|
+
return;
|
|
1009
|
+
const fkValues = new Set;
|
|
1010
|
+
for (const row of primaryRows) {
|
|
1011
|
+
const val = row[fk];
|
|
1012
|
+
if (val !== null && val !== undefined) {
|
|
1013
|
+
fkValues.add(val);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (fkValues.size === 0) {
|
|
1017
|
+
for (const row of primaryRows) {
|
|
1018
|
+
row[relName] = null;
|
|
1019
|
+
}
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const targetPk = resolvePkColumn(target);
|
|
1023
|
+
const selectOpt = typeof includeValue === "object" ? includeValue.select : undefined;
|
|
1024
|
+
const columns = resolveSelectColumns(target, selectOpt);
|
|
1025
|
+
if (!columns.includes(targetPk)) {
|
|
1026
|
+
columns.push(targetPk);
|
|
1027
|
+
}
|
|
1028
|
+
const query = buildSelect({
|
|
1029
|
+
table: target._name,
|
|
1030
|
+
columns,
|
|
1031
|
+
where: { [targetPk]: { in: [...fkValues] } }
|
|
1032
|
+
});
|
|
1033
|
+
const res = await executeQuery(queryFn, query.sql, query.params);
|
|
1034
|
+
const lookup = new Map;
|
|
1035
|
+
for (const row of res.rows) {
|
|
1036
|
+
const mapped = mapRow(row);
|
|
1037
|
+
lookup.set(mapped[targetPk], mapped);
|
|
1038
|
+
}
|
|
1039
|
+
if (typeof includeValue === "object" && includeValue.include && depth < 2) {
|
|
1040
|
+
const targetRelations = findTargetRelations(target, tablesRegistry);
|
|
1041
|
+
if (targetRelations) {
|
|
1042
|
+
const childRows = [...lookup.values()];
|
|
1043
|
+
await loadRelations(queryFn, childRows, targetRelations, includeValue.include, depth + 1, tablesRegistry, target);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
for (const row of primaryRows) {
|
|
1047
|
+
const fkVal = row[fk];
|
|
1048
|
+
row[relName] = lookup.get(fkVal) ?? null;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
async function loadManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable) {
|
|
1052
|
+
const fk = def._foreignKey;
|
|
1053
|
+
if (!fk)
|
|
1054
|
+
return;
|
|
1055
|
+
const primaryPk = primaryTable ? resolvePkColumn(primaryTable) : "id";
|
|
1056
|
+
const pkValues = new Set;
|
|
1057
|
+
for (const row of primaryRows) {
|
|
1058
|
+
const val = row[primaryPk];
|
|
1059
|
+
if (val !== null && val !== undefined) {
|
|
1060
|
+
pkValues.add(val);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (pkValues.size === 0) {
|
|
1064
|
+
for (const row of primaryRows) {
|
|
1065
|
+
row[relName] = [];
|
|
1066
|
+
}
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const selectOpt = typeof includeValue === "object" ? includeValue.select : undefined;
|
|
1070
|
+
const columns = resolveSelectColumns(target, selectOpt);
|
|
1071
|
+
if (!columns.includes(fk)) {
|
|
1072
|
+
columns.push(fk);
|
|
1073
|
+
}
|
|
1074
|
+
const query = buildSelect({
|
|
1075
|
+
table: target._name,
|
|
1076
|
+
columns,
|
|
1077
|
+
where: { [fk]: { in: [...pkValues] } }
|
|
1078
|
+
});
|
|
1079
|
+
const res = await executeQuery(queryFn, query.sql, query.params);
|
|
1080
|
+
const lookup = new Map;
|
|
1081
|
+
for (const row of res.rows) {
|
|
1082
|
+
const mapped = mapRow(row);
|
|
1083
|
+
const parentId = mapped[fk];
|
|
1084
|
+
const existing = lookup.get(parentId);
|
|
1085
|
+
if (existing) {
|
|
1086
|
+
existing.push(mapped);
|
|
1087
|
+
} else {
|
|
1088
|
+
lookup.set(parentId, [mapped]);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (typeof includeValue === "object" && includeValue.include && depth < 2) {
|
|
1092
|
+
const targetRelations = findTargetRelations(target, tablesRegistry);
|
|
1093
|
+
if (targetRelations) {
|
|
1094
|
+
const allChildRows = [...lookup.values()].flat();
|
|
1095
|
+
await loadRelations(queryFn, allChildRows, targetRelations, includeValue.include, depth + 1, tablesRegistry, target);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
for (const row of primaryRows) {
|
|
1099
|
+
const pkVal = row[primaryPk];
|
|
1100
|
+
row[relName] = lookup.get(pkVal) ?? [];
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
async function loadManyToManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable) {
|
|
1104
|
+
const through = def._through;
|
|
1105
|
+
if (!through)
|
|
1106
|
+
return;
|
|
1107
|
+
const primaryPk = primaryTable ? resolvePkColumn(primaryTable) : "id";
|
|
1108
|
+
const targetPk = resolvePkColumn(target);
|
|
1109
|
+
const joinTable = through.table();
|
|
1110
|
+
const thisKey = through.thisKey;
|
|
1111
|
+
const thatKey = through.thatKey;
|
|
1112
|
+
const pkValues = new Set;
|
|
1113
|
+
for (const row of primaryRows) {
|
|
1114
|
+
const val = row[primaryPk];
|
|
1115
|
+
if (val !== null && val !== undefined) {
|
|
1116
|
+
pkValues.add(val);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (pkValues.size === 0) {
|
|
1120
|
+
for (const row of primaryRows) {
|
|
1121
|
+
row[relName] = [];
|
|
1122
|
+
}
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const joinQuery = buildSelect({
|
|
1126
|
+
table: joinTable._name,
|
|
1127
|
+
columns: [thisKey, thatKey],
|
|
1128
|
+
where: { [thisKey]: { in: [...pkValues] } }
|
|
1129
|
+
});
|
|
1130
|
+
const joinRes = await executeQuery(queryFn, joinQuery.sql, joinQuery.params);
|
|
1131
|
+
const primaryToTargetIds = new Map;
|
|
1132
|
+
const allTargetIds = new Set;
|
|
1133
|
+
for (const row of joinRes.rows) {
|
|
1134
|
+
const mapped = mapRow(row);
|
|
1135
|
+
const primaryId = mapped[thisKey];
|
|
1136
|
+
const targetId = mapped[thatKey];
|
|
1137
|
+
if (targetId !== null && targetId !== undefined) {
|
|
1138
|
+
allTargetIds.add(targetId);
|
|
1139
|
+
const existing = primaryToTargetIds.get(primaryId);
|
|
1140
|
+
if (existing) {
|
|
1141
|
+
existing.push(targetId);
|
|
1142
|
+
} else {
|
|
1143
|
+
primaryToTargetIds.set(primaryId, [targetId]);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (allTargetIds.size === 0) {
|
|
1148
|
+
for (const row of primaryRows) {
|
|
1149
|
+
row[relName] = [];
|
|
1150
|
+
}
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const selectOpt = typeof includeValue === "object" ? includeValue.select : undefined;
|
|
1154
|
+
const columns = resolveSelectColumns(target, selectOpt);
|
|
1155
|
+
if (!columns.includes(targetPk)) {
|
|
1156
|
+
columns.push(targetPk);
|
|
1157
|
+
}
|
|
1158
|
+
const targetQuery = buildSelect({
|
|
1159
|
+
table: target._name,
|
|
1160
|
+
columns,
|
|
1161
|
+
where: { [targetPk]: { in: [...allTargetIds] } }
|
|
1162
|
+
});
|
|
1163
|
+
const targetRes = await executeQuery(queryFn, targetQuery.sql, targetQuery.params);
|
|
1164
|
+
const targetLookup = new Map;
|
|
1165
|
+
for (const row of targetRes.rows) {
|
|
1166
|
+
const mapped = mapRow(row);
|
|
1167
|
+
targetLookup.set(mapped[targetPk], mapped);
|
|
1168
|
+
}
|
|
1169
|
+
if (typeof includeValue === "object" && includeValue.include && depth < 2) {
|
|
1170
|
+
const targetRelations = findTargetRelations(target, tablesRegistry);
|
|
1171
|
+
if (targetRelations) {
|
|
1172
|
+
const allTargetRows = [...targetLookup.values()];
|
|
1173
|
+
await loadRelations(queryFn, allTargetRows, targetRelations, includeValue.include, depth + 1, tablesRegistry, target);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
for (const row of primaryRows) {
|
|
1177
|
+
const pkVal = row[primaryPk];
|
|
1178
|
+
const targetIds = primaryToTargetIds.get(pkVal) ?? [];
|
|
1179
|
+
const relatedRows = [];
|
|
1180
|
+
for (const targetId of targetIds) {
|
|
1181
|
+
const targetRow = targetLookup.get(targetId);
|
|
1182
|
+
if (targetRow) {
|
|
1183
|
+
relatedRows.push(targetRow);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
row[relName] = relatedRows;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/client/postgres-driver.ts
|
|
1191
|
+
import postgresLib from "postgres";
|
|
1192
|
+
function isPostgresError(error) {
|
|
1193
|
+
return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && "message" in error && typeof error.message === "string";
|
|
1194
|
+
}
|
|
1195
|
+
function adaptPostgresError(error) {
|
|
1196
|
+
if (isPostgresError(error)) {
|
|
1197
|
+
const adapted = Object.assign(new Error(error.message), {
|
|
1198
|
+
code: error.code,
|
|
1199
|
+
message: error.message,
|
|
1200
|
+
table: error.table_name,
|
|
1201
|
+
column: error.column_name,
|
|
1202
|
+
constraint: error.constraint_name,
|
|
1203
|
+
detail: error.detail
|
|
1204
|
+
});
|
|
1205
|
+
throw adapted;
|
|
1206
|
+
}
|
|
1207
|
+
throw error;
|
|
1208
|
+
}
|
|
1209
|
+
function createPostgresDriver(url, pool) {
|
|
1210
|
+
const sql = postgresLib(url, {
|
|
1211
|
+
max: pool?.max ?? 10,
|
|
1212
|
+
idle_timeout: pool?.idleTimeout !== undefined ? pool.idleTimeout / 1000 : 30,
|
|
1213
|
+
connect_timeout: pool?.connectionTimeout !== undefined ? pool.connectionTimeout / 1000 : 10,
|
|
1214
|
+
fetch_types: false
|
|
1215
|
+
});
|
|
1216
|
+
const queryFn = async (sqlStr, params) => {
|
|
1217
|
+
try {
|
|
1218
|
+
const result = await sql.unsafe(sqlStr, params);
|
|
1219
|
+
const rows = result.map((row) => {
|
|
1220
|
+
const mapped = {};
|
|
1221
|
+
for (const [key, value] of Object.entries(row)) {
|
|
1222
|
+
mapped[key] = coerceValue(value);
|
|
1223
|
+
}
|
|
1224
|
+
return mapped;
|
|
1225
|
+
});
|
|
1226
|
+
return {
|
|
1227
|
+
rows,
|
|
1228
|
+
rowCount: result.count ?? rows.length
|
|
1229
|
+
};
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
adaptPostgresError(error);
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
return {
|
|
1235
|
+
queryFn,
|
|
1236
|
+
async close() {
|
|
1237
|
+
await sql.end();
|
|
1238
|
+
},
|
|
1239
|
+
async isHealthy() {
|
|
1240
|
+
try {
|
|
1241
|
+
const healthCheckTimeout = pool?.healthCheckTimeout ?? 5000;
|
|
1242
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Health check timed out")), healthCheckTimeout));
|
|
1243
|
+
await Promise.race([sql`SELECT 1`, timeout]);
|
|
1244
|
+
return true;
|
|
1245
|
+
} catch {
|
|
1246
|
+
return false;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function coerceValue(value) {
|
|
1252
|
+
if (typeof value === "string" && isTimestampString(value)) {
|
|
1253
|
+
const date = new Date(value);
|
|
1254
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1255
|
+
return date;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return value;
|
|
1259
|
+
}
|
|
1260
|
+
function isTimestampString(value) {
|
|
1261
|
+
return /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(value);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// src/client/tenant-graph.ts
|
|
1265
|
+
function computeTenantGraph(registry) {
|
|
1266
|
+
const entries = Object.entries(registry);
|
|
1267
|
+
const tableNameToKey = new Map;
|
|
1268
|
+
for (const [key, entry] of entries) {
|
|
1269
|
+
tableNameToKey.set(entry.table._name, key);
|
|
1270
|
+
}
|
|
1271
|
+
let root = null;
|
|
1272
|
+
const directlyScoped = [];
|
|
1273
|
+
const shared = [];
|
|
1274
|
+
for (const [key, entry] of entries) {
|
|
1275
|
+
if (entry.table._shared) {
|
|
1276
|
+
shared.push(key);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
const columns = entry.table._columns;
|
|
1280
|
+
for (const colKey of Object.keys(columns)) {
|
|
1281
|
+
const col = columns[colKey];
|
|
1282
|
+
if (col._meta.isTenant && col._meta.references) {
|
|
1283
|
+
if (!directlyScoped.includes(key)) {
|
|
1284
|
+
directlyScoped.push(key);
|
|
1285
|
+
}
|
|
1286
|
+
const rootTableName = col._meta.references.table;
|
|
1287
|
+
const rootKey = tableNameToKey.get(rootTableName);
|
|
1288
|
+
if (rootKey && root === null) {
|
|
1289
|
+
root = rootKey;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const scopedTableNames = new Set;
|
|
1295
|
+
if (root !== null) {
|
|
1296
|
+
const rootEntry = registry[root];
|
|
1297
|
+
if (rootEntry) {
|
|
1298
|
+
scopedTableNames.add(rootEntry.table._name);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
for (const key of directlyScoped) {
|
|
1302
|
+
const entry = registry[key];
|
|
1303
|
+
if (entry) {
|
|
1304
|
+
scopedTableNames.add(entry.table._name);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const indirectlyScoped = [];
|
|
1308
|
+
const indirectlyScopedNames = new Set;
|
|
1309
|
+
let changed = true;
|
|
1310
|
+
while (changed) {
|
|
1311
|
+
changed = false;
|
|
1312
|
+
for (const [key, entry] of entries) {
|
|
1313
|
+
if (key === root || directlyScoped.includes(key) || shared.includes(key) || indirectlyScopedNames.has(entry.table._name)) {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
const columns = entry.table._columns;
|
|
1317
|
+
for (const colKey of Object.keys(columns)) {
|
|
1318
|
+
const col = columns[colKey];
|
|
1319
|
+
if (col._meta.references && !col._meta.isTenant) {
|
|
1320
|
+
const refTable = col._meta.references.table;
|
|
1321
|
+
if (scopedTableNames.has(refTable) || indirectlyScopedNames.has(refTable)) {
|
|
1322
|
+
indirectlyScoped.push(key);
|
|
1323
|
+
indirectlyScopedNames.add(entry.table._name);
|
|
1324
|
+
scopedTableNames.add(entry.table._name);
|
|
1325
|
+
changed = true;
|
|
1326
|
+
break;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
root,
|
|
1334
|
+
directlyScoped,
|
|
1335
|
+
indirectlyScoped,
|
|
1336
|
+
shared
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/client/database.ts
|
|
1341
|
+
function isReadQuery(sqlStr) {
|
|
1342
|
+
let normalized = sqlStr.trim();
|
|
1343
|
+
while (normalized.startsWith("--") || normalized.startsWith("/*") || normalized.startsWith("//")) {
|
|
1344
|
+
if (normalized.startsWith("--")) {
|
|
1345
|
+
const newlineIdx = normalized.indexOf(`
|
|
1346
|
+
`);
|
|
1347
|
+
if (newlineIdx === -1)
|
|
1348
|
+
return false;
|
|
1349
|
+
normalized = normalized.slice(newlineIdx + 1).trim();
|
|
1350
|
+
} else if (normalized.startsWith("/*")) {
|
|
1351
|
+
const endIdx = normalized.indexOf("*/");
|
|
1352
|
+
if (endIdx === -1)
|
|
1353
|
+
return false;
|
|
1354
|
+
normalized = normalized.slice(endIdx + 2).trim();
|
|
1355
|
+
} else if (normalized.startsWith("//")) {
|
|
1356
|
+
const newlineIdx = normalized.indexOf(`
|
|
1357
|
+
`);
|
|
1358
|
+
if (newlineIdx === -1)
|
|
1359
|
+
return false;
|
|
1360
|
+
normalized = normalized.slice(newlineIdx + 1).trim();
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const upper = normalized.toUpperCase();
|
|
1364
|
+
if (upper.startsWith("WITH ")) {
|
|
1365
|
+
const hasInsert = /\bINSERT\s+INTO\b/is.test(normalized);
|
|
1366
|
+
const hasUpdate = /\bUPDATE\b/is.test(normalized);
|
|
1367
|
+
const hasDelete = /\bDELETE\s+FROM\b/is.test(normalized);
|
|
1368
|
+
if (hasInsert || hasUpdate || hasDelete) {
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
const selectMatch = normalized.match(/\bSELECT\s/is);
|
|
1372
|
+
return selectMatch !== null;
|
|
1373
|
+
}
|
|
1374
|
+
if (/\bFOR\s+(NO\s+KEY\s+)?(UPDATE|KEY\s+SHARE|SHARE)\b/is.test(upper)) {
|
|
1375
|
+
return false;
|
|
1376
|
+
}
|
|
1377
|
+
if (upper.startsWith("SELECT INTO") || /\bSELECT\s+.+\s+INTO\b/.test(upper)) {
|
|
1378
|
+
return false;
|
|
1379
|
+
}
|
|
1380
|
+
return upper.startsWith("SELECT");
|
|
1381
|
+
}
|
|
1382
|
+
function resolveTable(tables, name) {
|
|
1383
|
+
const entry = tables[name];
|
|
1384
|
+
if (!entry) {
|
|
1385
|
+
throw new Error(`Table "${name}" is not registered in the database.`);
|
|
1386
|
+
}
|
|
1387
|
+
return entry;
|
|
1388
|
+
}
|
|
1389
|
+
function createDb(options) {
|
|
1390
|
+
const { tables, log } = options;
|
|
1391
|
+
const tenantGraph = computeTenantGraph(tables);
|
|
1392
|
+
if (log && tenantGraph.root !== null) {
|
|
1393
|
+
const allScoped = new Set([
|
|
1394
|
+
...tenantGraph.root !== null ? [tenantGraph.root] : [],
|
|
1395
|
+
...tenantGraph.directlyScoped,
|
|
1396
|
+
...tenantGraph.indirectlyScoped,
|
|
1397
|
+
...tenantGraph.shared
|
|
1398
|
+
]);
|
|
1399
|
+
for (const [key, entry] of Object.entries(tables)) {
|
|
1400
|
+
if (!allScoped.has(key)) {
|
|
1401
|
+
log(`[vertz/db] Table "${entry.table._name}" has no tenant path and is not marked .shared(). ` + "It will not be automatically scoped to a tenant.");
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
const tablesRegistry = tables;
|
|
1406
|
+
let driver = null;
|
|
1407
|
+
let replicaDrivers = [];
|
|
1408
|
+
let replicaIndex = 0;
|
|
1409
|
+
const queryFn = (() => {
|
|
1410
|
+
if (options._queryFn) {
|
|
1411
|
+
return options._queryFn;
|
|
1412
|
+
}
|
|
1413
|
+
if (options.url) {
|
|
1414
|
+
driver = createPostgresDriver(options.url, options.pool);
|
|
1415
|
+
const replicas = options.pool?.replicas;
|
|
1416
|
+
if (replicas && replicas.length > 0) {
|
|
1417
|
+
replicaDrivers = replicas.map((replicaUrl) => createPostgresDriver(replicaUrl, options.pool));
|
|
1418
|
+
}
|
|
1419
|
+
return async (sqlStr, params) => {
|
|
1420
|
+
if (replicaDrivers.length === 0) {
|
|
1421
|
+
return driver.queryFn(sqlStr, params);
|
|
1422
|
+
}
|
|
1423
|
+
if (isReadQuery(sqlStr)) {
|
|
1424
|
+
const targetReplica = replicaDrivers[replicaIndex];
|
|
1425
|
+
replicaIndex = (replicaIndex + 1) % replicaDrivers.length;
|
|
1426
|
+
try {
|
|
1427
|
+
return await targetReplica.queryFn(sqlStr, params);
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
console.warn("[vertz/db] replica query failed, falling back to primary:", err.message);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return driver.queryFn(sqlStr, params);
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
return async () => {
|
|
1436
|
+
throw new Error("db.query() requires a connected postgres driver. " + "Provide a `url` to connect to PostgreSQL, or `_queryFn` for testing.");
|
|
1437
|
+
};
|
|
1438
|
+
})();
|
|
1439
|
+
return {
|
|
1440
|
+
_tables: tables,
|
|
1441
|
+
$tenantGraph: tenantGraph,
|
|
1442
|
+
async query(fragment) {
|
|
1443
|
+
return executeQuery(queryFn, fragment.sql, fragment.params);
|
|
1444
|
+
},
|
|
1445
|
+
async close() {
|
|
1446
|
+
if (driver) {
|
|
1447
|
+
await driver.close();
|
|
1448
|
+
}
|
|
1449
|
+
await Promise.all(replicaDrivers.map((r) => r.close()));
|
|
1450
|
+
},
|
|
1451
|
+
async isHealthy() {
|
|
1452
|
+
if (driver) {
|
|
1453
|
+
return driver.isHealthy();
|
|
1454
|
+
}
|
|
1455
|
+
return true;
|
|
1456
|
+
},
|
|
1457
|
+
async get(name, opts) {
|
|
1458
|
+
const entry = resolveTable(tables, name);
|
|
1459
|
+
const result = await get(queryFn, entry.table, opts);
|
|
1460
|
+
if (result !== null && opts?.include) {
|
|
1461
|
+
const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, tablesRegistry, entry.table);
|
|
1462
|
+
return rows[0] ?? null;
|
|
1463
|
+
}
|
|
1464
|
+
return result;
|
|
1465
|
+
},
|
|
1466
|
+
async getOrThrow(name, opts) {
|
|
1467
|
+
const entry = resolveTable(tables, name);
|
|
1468
|
+
const result = await getOrThrow(queryFn, entry.table, opts);
|
|
1469
|
+
if (opts?.include) {
|
|
1470
|
+
const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, tablesRegistry, entry.table);
|
|
1471
|
+
return rows[0];
|
|
1472
|
+
}
|
|
1473
|
+
return result;
|
|
1474
|
+
},
|
|
1475
|
+
async list(name, opts) {
|
|
1476
|
+
const entry = resolveTable(tables, name);
|
|
1477
|
+
const results = await list(queryFn, entry.table, opts);
|
|
1478
|
+
if (opts?.include && results.length > 0) {
|
|
1479
|
+
return loadRelations(queryFn, results, entry.relations, opts.include, 0, tablesRegistry, entry.table);
|
|
1480
|
+
}
|
|
1481
|
+
return results;
|
|
1482
|
+
},
|
|
1483
|
+
async listAndCount(name, opts) {
|
|
1484
|
+
const entry = resolveTable(tables, name);
|
|
1485
|
+
const { data, total } = await listAndCount(queryFn, entry.table, opts);
|
|
1486
|
+
if (opts?.include && data.length > 0) {
|
|
1487
|
+
const withRelations = await loadRelations(queryFn, data, entry.relations, opts.include, 0, tablesRegistry, entry.table);
|
|
1488
|
+
return { data: withRelations, total };
|
|
1489
|
+
}
|
|
1490
|
+
return { data, total };
|
|
1491
|
+
},
|
|
1492
|
+
get findOne() {
|
|
1493
|
+
return this.get;
|
|
1494
|
+
},
|
|
1495
|
+
get findOneOrThrow() {
|
|
1496
|
+
return this.getOrThrow;
|
|
1497
|
+
},
|
|
1498
|
+
get findMany() {
|
|
1499
|
+
return this.list;
|
|
1500
|
+
},
|
|
1501
|
+
get findManyAndCount() {
|
|
1502
|
+
return this.listAndCount;
|
|
1503
|
+
},
|
|
1504
|
+
async create(name, opts) {
|
|
1505
|
+
const entry = resolveTable(tables, name);
|
|
1506
|
+
return create(queryFn, entry.table, opts);
|
|
1507
|
+
},
|
|
1508
|
+
async createMany(name, opts) {
|
|
1509
|
+
const entry = resolveTable(tables, name);
|
|
1510
|
+
return createMany(queryFn, entry.table, opts);
|
|
1511
|
+
},
|
|
1512
|
+
async createManyAndReturn(name, opts) {
|
|
1513
|
+
const entry = resolveTable(tables, name);
|
|
1514
|
+
return createManyAndReturn(queryFn, entry.table, opts);
|
|
1515
|
+
},
|
|
1516
|
+
async update(name, opts) {
|
|
1517
|
+
const entry = resolveTable(tables, name);
|
|
1518
|
+
return update(queryFn, entry.table, opts);
|
|
1519
|
+
},
|
|
1520
|
+
async updateMany(name, opts) {
|
|
1521
|
+
const entry = resolveTable(tables, name);
|
|
1522
|
+
return updateMany(queryFn, entry.table, opts);
|
|
1523
|
+
},
|
|
1524
|
+
async upsert(name, opts) {
|
|
1525
|
+
const entry = resolveTable(tables, name);
|
|
1526
|
+
return upsert(queryFn, entry.table, opts);
|
|
1527
|
+
},
|
|
1528
|
+
async delete(name, opts) {
|
|
1529
|
+
const entry = resolveTable(tables, name);
|
|
1530
|
+
return deleteOne(queryFn, entry.table, opts);
|
|
1531
|
+
},
|
|
1532
|
+
async deleteMany(name, opts) {
|
|
1533
|
+
const entry = resolveTable(tables, name);
|
|
1534
|
+
return deleteMany(queryFn, entry.table, opts);
|
|
1535
|
+
},
|
|
1536
|
+
async count(name, opts) {
|
|
1537
|
+
const entry = resolveTable(tables, name);
|
|
1538
|
+
return count(queryFn, entry.table, opts);
|
|
1539
|
+
},
|
|
1540
|
+
async aggregate(name, opts) {
|
|
1541
|
+
const entry = resolveTable(tables, name);
|
|
1542
|
+
return aggregate(queryFn, entry.table, opts);
|
|
1543
|
+
},
|
|
1544
|
+
async groupBy(name, opts) {
|
|
1545
|
+
const entry = resolveTable(tables, name);
|
|
1546
|
+
return groupBy(queryFn, entry.table, opts);
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
// src/schema/column.ts
|
|
1551
|
+
function cloneWith(source, metaOverrides) {
|
|
1552
|
+
return createColumnWithMeta({ ...source._meta, ...metaOverrides });
|
|
1553
|
+
}
|
|
1554
|
+
function createColumnWithMeta(meta) {
|
|
1555
|
+
const col = {
|
|
1556
|
+
_meta: meta,
|
|
1557
|
+
primary() {
|
|
1558
|
+
return cloneWith(this, { primary: true, hasDefault: true });
|
|
1559
|
+
},
|
|
1560
|
+
unique() {
|
|
1561
|
+
return cloneWith(this, { unique: true });
|
|
1562
|
+
},
|
|
1563
|
+
nullable() {
|
|
1564
|
+
return cloneWith(this, { nullable: true });
|
|
1565
|
+
},
|
|
1566
|
+
default(value) {
|
|
1567
|
+
return cloneWith(this, { hasDefault: true, defaultValue: value });
|
|
1568
|
+
},
|
|
1569
|
+
sensitive() {
|
|
1570
|
+
return cloneWith(this, { sensitive: true });
|
|
1571
|
+
},
|
|
1572
|
+
hidden() {
|
|
1573
|
+
return cloneWith(this, { hidden: true });
|
|
1574
|
+
},
|
|
1575
|
+
check(sql) {
|
|
1576
|
+
return cloneWith(this, { check: sql });
|
|
1577
|
+
},
|
|
1578
|
+
references(table, column) {
|
|
1579
|
+
return cloneWith(this, {
|
|
1580
|
+
references: { table, column: column ?? "id" }
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
return col;
|
|
1585
|
+
}
|
|
1586
|
+
function defaultMeta(sqlType) {
|
|
1587
|
+
return {
|
|
1588
|
+
sqlType,
|
|
1589
|
+
primary: false,
|
|
1590
|
+
unique: false,
|
|
1591
|
+
nullable: false,
|
|
1592
|
+
hasDefault: false,
|
|
1593
|
+
sensitive: false,
|
|
1594
|
+
hidden: false,
|
|
1595
|
+
isTenant: false,
|
|
1596
|
+
references: null,
|
|
1597
|
+
check: null
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function createColumn(sqlType, extra) {
|
|
1601
|
+
return createColumnWithMeta({
|
|
1602
|
+
...defaultMeta(sqlType),
|
|
1603
|
+
...extra
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
function createSerialColumn() {
|
|
1607
|
+
return createColumnWithMeta({
|
|
1608
|
+
sqlType: "serial",
|
|
1609
|
+
primary: false,
|
|
1610
|
+
unique: false,
|
|
1611
|
+
nullable: false,
|
|
1612
|
+
hasDefault: true,
|
|
1613
|
+
sensitive: false,
|
|
1614
|
+
hidden: false,
|
|
1615
|
+
isTenant: false,
|
|
1616
|
+
references: null,
|
|
1617
|
+
check: null
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
function createTenantColumn(targetTableName) {
|
|
1621
|
+
return createColumnWithMeta({
|
|
1622
|
+
sqlType: "uuid",
|
|
1623
|
+
primary: false,
|
|
1624
|
+
unique: false,
|
|
1625
|
+
nullable: false,
|
|
1626
|
+
hasDefault: false,
|
|
1627
|
+
sensitive: false,
|
|
1628
|
+
hidden: false,
|
|
1629
|
+
isTenant: true,
|
|
1630
|
+
references: { table: targetTableName, column: "id" },
|
|
1631
|
+
check: null
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/schema/relation.ts
|
|
1636
|
+
function createOneRelation(target, foreignKey) {
|
|
1637
|
+
return {
|
|
1638
|
+
_type: "one",
|
|
1639
|
+
_target: target,
|
|
1640
|
+
_foreignKey: foreignKey,
|
|
1641
|
+
_through: null
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
function createManyRelation(target, foreignKey) {
|
|
1645
|
+
return {
|
|
1646
|
+
_type: "many",
|
|
1647
|
+
_target: target,
|
|
1648
|
+
_foreignKey: foreignKey ?? null,
|
|
1649
|
+
_through: null,
|
|
1650
|
+
through(joinTable, thisKey, thatKey) {
|
|
1651
|
+
return {
|
|
1652
|
+
_type: "many",
|
|
1653
|
+
_target: target,
|
|
1654
|
+
_foreignKey: null,
|
|
1655
|
+
_through: {
|
|
1656
|
+
table: joinTable,
|
|
1657
|
+
thisKey,
|
|
1658
|
+
thatKey
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// src/schema/table.ts
|
|
1666
|
+
function createIndex(columns) {
|
|
1667
|
+
return {
|
|
1668
|
+
columns: Array.isArray(columns) ? columns : [columns]
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
function createTable(name, columns, options) {
|
|
1672
|
+
return createTableInternal(name, columns, options?.indexes ?? [], false);
|
|
1673
|
+
}
|
|
1674
|
+
function createTableInternal(name, columns, indexes, shared) {
|
|
1675
|
+
const table = {
|
|
1676
|
+
_name: name,
|
|
1677
|
+
_columns: columns,
|
|
1678
|
+
_indexes: indexes,
|
|
1679
|
+
_shared: shared,
|
|
1680
|
+
get $infer() {
|
|
1681
|
+
return;
|
|
1682
|
+
},
|
|
1683
|
+
get $infer_all() {
|
|
1684
|
+
return;
|
|
1685
|
+
},
|
|
1686
|
+
get $insert() {
|
|
1687
|
+
return;
|
|
1688
|
+
},
|
|
1689
|
+
get $update() {
|
|
1690
|
+
return;
|
|
1691
|
+
},
|
|
1692
|
+
get $not_sensitive() {
|
|
1693
|
+
return;
|
|
1694
|
+
},
|
|
1695
|
+
get $not_hidden() {
|
|
1696
|
+
return;
|
|
1697
|
+
},
|
|
1698
|
+
shared() {
|
|
1699
|
+
return createTableInternal(name, columns, indexes, true);
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
return table;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// src/d.ts
|
|
1706
|
+
var d = {
|
|
1707
|
+
uuid: () => createColumn("uuid"),
|
|
1708
|
+
text: () => createColumn("text"),
|
|
1709
|
+
varchar: (length) => createColumn("varchar", { length }),
|
|
1710
|
+
email: () => createColumn("text", { format: "email" }),
|
|
1711
|
+
boolean: () => createColumn("boolean"),
|
|
1712
|
+
integer: () => createColumn("integer"),
|
|
1713
|
+
bigint: () => createColumn("bigint"),
|
|
1714
|
+
decimal: (precision, scale) => createColumn("decimal", { precision, scale }),
|
|
1715
|
+
real: () => createColumn("real"),
|
|
1716
|
+
doublePrecision: () => createColumn("double precision"),
|
|
1717
|
+
serial: () => createSerialColumn(),
|
|
1718
|
+
timestamp: () => createColumn("timestamp with time zone"),
|
|
1719
|
+
date: () => createColumn("date"),
|
|
1720
|
+
time: () => createColumn("time"),
|
|
1721
|
+
jsonb: (opts) => createColumn("jsonb", opts?.validator ? { validator: opts.validator } : {}),
|
|
1722
|
+
textArray: () => createColumn("text[]"),
|
|
1723
|
+
integerArray: () => createColumn("integer[]"),
|
|
1724
|
+
enum: (name, valuesOrSchema) => {
|
|
1725
|
+
const values = !Array.isArray(valuesOrSchema) && typeof valuesOrSchema.values !== "undefined" ? valuesOrSchema.values : valuesOrSchema;
|
|
1726
|
+
return createColumn("enum", {
|
|
1727
|
+
enumName: name,
|
|
1728
|
+
enumValues: values
|
|
1729
|
+
});
|
|
1730
|
+
},
|
|
1731
|
+
tenant: (targetTable) => createTenantColumn(targetTable._name),
|
|
1732
|
+
table: (name, columns, options) => createTable(name, columns, options),
|
|
1733
|
+
index: (columns) => createIndex(columns),
|
|
1734
|
+
ref: {
|
|
1735
|
+
one: (target, foreignKey) => createOneRelation(target, foreignKey),
|
|
1736
|
+
many: (target, foreignKey) => createManyRelation(target, foreignKey)
|
|
1737
|
+
},
|
|
1738
|
+
entry: (table, relations = {}) => ({
|
|
1739
|
+
table,
|
|
1740
|
+
relations
|
|
1741
|
+
})
|
|
1742
|
+
};
|
|
1743
|
+
// src/errors/error-codes.ts
|
|
1744
|
+
var DbErrorCode = {
|
|
1745
|
+
UNIQUE_VIOLATION: "23505",
|
|
1746
|
+
FOREIGN_KEY_VIOLATION: "23503",
|
|
1747
|
+
NOT_NULL_VIOLATION: "23502",
|
|
1748
|
+
CHECK_VIOLATION: "23514",
|
|
1749
|
+
EXCLUSION_VIOLATION: "23P01",
|
|
1750
|
+
SERIALIZATION_FAILURE: "40001",
|
|
1751
|
+
DEADLOCK_DETECTED: "40P01",
|
|
1752
|
+
CONNECTION_EXCEPTION: "08000",
|
|
1753
|
+
CONNECTION_DOES_NOT_EXIST: "08003",
|
|
1754
|
+
CONNECTION_FAILURE: "08006",
|
|
1755
|
+
NOT_FOUND: "NOT_FOUND",
|
|
1756
|
+
CONNECTION_ERROR: "CONNECTION_ERROR",
|
|
1757
|
+
POOL_EXHAUSTED: "POOL_EXHAUSTED"
|
|
1758
|
+
};
|
|
1759
|
+
var PgCodeToName = Object.fromEntries(Object.entries(DbErrorCode).map(([name, pgCode]) => [pgCode, name]));
|
|
1760
|
+
function resolveErrorCode(pgCode) {
|
|
1761
|
+
return PgCodeToName[pgCode];
|
|
1762
|
+
}
|
|
1763
|
+
// src/errors/http-adapter.ts
|
|
1764
|
+
function dbErrorToHttpError(error) {
|
|
1765
|
+
const body = error.toJSON();
|
|
1766
|
+
if (error instanceof UniqueConstraintError) {
|
|
1767
|
+
return { status: 409, body };
|
|
1768
|
+
}
|
|
1769
|
+
if (error instanceof NotFoundError) {
|
|
1770
|
+
return { status: 404, body };
|
|
1771
|
+
}
|
|
1772
|
+
if (error instanceof ForeignKeyError) {
|
|
1773
|
+
return { status: 422, body };
|
|
1774
|
+
}
|
|
1775
|
+
if (error instanceof NotNullError) {
|
|
1776
|
+
return { status: 422, body };
|
|
1777
|
+
}
|
|
1778
|
+
if (error instanceof CheckConstraintError) {
|
|
1779
|
+
return { status: 422, body };
|
|
1780
|
+
}
|
|
1781
|
+
if (error instanceof ConnectionError) {
|
|
1782
|
+
return { status: 503, body };
|
|
1783
|
+
}
|
|
1784
|
+
return { status: 500, body };
|
|
1785
|
+
}
|
|
1786
|
+
// src/schema/registry.ts
|
|
1787
|
+
function createRegistry(tables, relationsCallback) {
|
|
1788
|
+
const lookup = (name) => tables[name];
|
|
1789
|
+
const ref = new Proxy({}, {
|
|
1790
|
+
get(_target, _sourceKey) {
|
|
1791
|
+
return {
|
|
1792
|
+
one(targetName, foreignKey) {
|
|
1793
|
+
return createOneRelation(() => lookup(targetName), foreignKey);
|
|
1794
|
+
},
|
|
1795
|
+
many(targetName, foreignKey) {
|
|
1796
|
+
return createManyRelation(() => lookup(targetName), foreignKey);
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
const relationsMap = relationsCallback(ref);
|
|
1802
|
+
const relMap = relationsMap;
|
|
1803
|
+
const result = {};
|
|
1804
|
+
for (const key of Object.keys(tables)) {
|
|
1805
|
+
result[key] = {
|
|
1806
|
+
table: lookup(key),
|
|
1807
|
+
relations: relMap[key] ?? {}
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
return result;
|
|
1811
|
+
}
|
|
1812
|
+
// src/schema/enum-registry.ts
|
|
1813
|
+
function createEnumRegistry(definitions) {
|
|
1814
|
+
const registry = {};
|
|
1815
|
+
for (const [name, values] of Object.entries(definitions)) {
|
|
1816
|
+
registry[name] = { name, values };
|
|
1817
|
+
}
|
|
1818
|
+
return registry;
|
|
1819
|
+
}
|
|
1820
|
+
// src/codegen/type-gen.ts
|
|
1821
|
+
function fieldTypeToTs(field) {
|
|
1822
|
+
switch (field.type) {
|
|
1823
|
+
case "string":
|
|
1824
|
+
case "uuid":
|
|
1825
|
+
return "string";
|
|
1826
|
+
case "number":
|
|
1827
|
+
return "number";
|
|
1828
|
+
case "boolean":
|
|
1829
|
+
return "boolean";
|
|
1830
|
+
case "date":
|
|
1831
|
+
return "Date";
|
|
1832
|
+
case "json":
|
|
1833
|
+
return "unknown";
|
|
1834
|
+
case "enum":
|
|
1835
|
+
return field.enumName || "string";
|
|
1836
|
+
default:
|
|
1837
|
+
return "unknown";
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
function generateTypes(domain) {
|
|
1841
|
+
const { name, fields, relations } = domain;
|
|
1842
|
+
const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
1843
|
+
const lines = [];
|
|
1844
|
+
for (const field of Object.values(fields)) {
|
|
1845
|
+
if (field.type === "enum" && field.enumName && field.enumValues) {
|
|
1846
|
+
lines.push(`enum ${field.enumName} {`);
|
|
1847
|
+
for (const value of field.enumValues) {
|
|
1848
|
+
lines.push(` ${value.toUpperCase()} = '${value}',`);
|
|
1849
|
+
}
|
|
1850
|
+
lines.push("}");
|
|
1851
|
+
lines.push("");
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
lines.push(`interface ${pascalName} {`);
|
|
1855
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
1856
|
+
const tsType = fieldTypeToTs(field);
|
|
1857
|
+
const optional = field.required === false ? "?" : "";
|
|
1858
|
+
lines.push(` ${fieldName}${optional}: ${tsType};`);
|
|
1859
|
+
}
|
|
1860
|
+
if (relations) {
|
|
1861
|
+
for (const [relName, rel] of Object.entries(relations)) {
|
|
1862
|
+
if (rel.type === "belongsTo") {
|
|
1863
|
+
const targetPascal = rel.target.charAt(0).toUpperCase() + rel.target.slice(1);
|
|
1864
|
+
lines.push(` ${relName}: () => Promise<${targetPascal} | null>;`);
|
|
1865
|
+
} else if (rel.type === "hasMany") {
|
|
1866
|
+
const targetPascal = rel.target.charAt(0).toUpperCase() + rel.target.slice(1);
|
|
1867
|
+
lines.push(` ${relName}: () => Promise<${targetPascal}[]>;`);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
lines.push("}");
|
|
1872
|
+
lines.push("");
|
|
1873
|
+
lines.push(`interface Create${pascalName}Input {`);
|
|
1874
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
1875
|
+
if (field.primary)
|
|
1876
|
+
continue;
|
|
1877
|
+
const tsType = fieldTypeToTs(field);
|
|
1878
|
+
if (field.required !== false) {
|
|
1879
|
+
lines.push(` ${fieldName}: ${tsType};`);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
lines.push("}");
|
|
1883
|
+
lines.push("");
|
|
1884
|
+
lines.push(`interface Update${pascalName}Input {`);
|
|
1885
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
1886
|
+
if (field.primary)
|
|
1887
|
+
continue;
|
|
1888
|
+
const tsType = fieldTypeToTs(field);
|
|
1889
|
+
lines.push(` ${fieldName}?: ${tsType};`);
|
|
1890
|
+
}
|
|
1891
|
+
lines.push("}");
|
|
1892
|
+
lines.push("");
|
|
1893
|
+
lines.push(`interface ${pascalName}Where {`);
|
|
1894
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
1895
|
+
const tsType = fieldTypeToTs(field);
|
|
1896
|
+
lines.push(` ${fieldName}?: ${tsType};`);
|
|
1897
|
+
}
|
|
1898
|
+
lines.push("}");
|
|
1899
|
+
lines.push("");
|
|
1900
|
+
lines.push(`interface ${pascalName}OrderBy {`);
|
|
1901
|
+
for (const fieldName of Object.keys(fields)) {
|
|
1902
|
+
lines.push(` ${fieldName}?: 'asc' | 'desc';`);
|
|
1903
|
+
}
|
|
1904
|
+
lines.push("}");
|
|
1905
|
+
lines.push("");
|
|
1906
|
+
lines.push(`interface List${pascalName}Params {`);
|
|
1907
|
+
lines.push(` where?: ${pascalName}Where;`);
|
|
1908
|
+
lines.push(` orderBy?: ${pascalName}OrderBy;`);
|
|
1909
|
+
lines.push(` limit?: number;`);
|
|
1910
|
+
lines.push(` offset?: number;`);
|
|
1911
|
+
lines.push("}");
|
|
1912
|
+
lines.push("");
|
|
1913
|
+
lines.push(`interface ${pascalName}Client {`);
|
|
1914
|
+
lines.push(` list(): Promise<${pascalName}[]>;`);
|
|
1915
|
+
lines.push(` list(params?: List${pascalName}Params): Promise<${pascalName}[]>;`);
|
|
1916
|
+
let idField = "id";
|
|
1917
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
1918
|
+
if (field.primary) {
|
|
1919
|
+
idField = fieldName;
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
lines.push(` get(${idField}: string): Promise<${pascalName} | null>;`);
|
|
1924
|
+
lines.push(` create(data: Create${pascalName}Input): Promise<${pascalName}>;`);
|
|
1925
|
+
lines.push(` update(${idField}: string, data: Update${pascalName}Input): Promise<${pascalName}>;`);
|
|
1926
|
+
lines.push(` delete(${idField}: string): Promise<void>;`);
|
|
1927
|
+
lines.push("}");
|
|
1928
|
+
return lines.join(`
|
|
1929
|
+
`);
|
|
1930
|
+
}
|
|
1931
|
+
// src/codegen/client-gen.ts
|
|
1932
|
+
function generateClient(domains) {
|
|
1933
|
+
const lines = [];
|
|
1934
|
+
for (const domain of domains) {
|
|
1935
|
+
const types = generateTypes(domain);
|
|
1936
|
+
lines.push(types);
|
|
1937
|
+
lines.push("");
|
|
1938
|
+
}
|
|
1939
|
+
lines.push("export const db = {");
|
|
1940
|
+
for (const domain of domains) {
|
|
1941
|
+
const { name, fields, relations } = domain;
|
|
1942
|
+
const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
1943
|
+
let idField = "id";
|
|
1944
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
1945
|
+
if (field.primary) {
|
|
1946
|
+
idField = fieldName;
|
|
1947
|
+
break;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
lines.push(` ${name}: {`);
|
|
1951
|
+
lines.push(` list: (params?: List${pascalName}Params) => Promise<${pascalName}[]>,`);
|
|
1952
|
+
lines.push(` get: (${idField}: string) => Promise<${pascalName} | null>,`);
|
|
1953
|
+
lines.push(` create: (data: Create${pascalName}Input) => Promise<${pascalName}>,`);
|
|
1954
|
+
lines.push(` update: (${idField}: string, data: Update${pascalName}Input) => Promise<${pascalName}>,`);
|
|
1955
|
+
lines.push(` delete: (${idField}: string) => Promise<void>,`);
|
|
1956
|
+
if (relations) {
|
|
1957
|
+
for (const [relName, rel] of Object.entries(relations)) {
|
|
1958
|
+
if (rel.type === "belongsTo") {
|
|
1959
|
+
const targetPascal = rel.target.charAt(0).toUpperCase() + rel.target.slice(1);
|
|
1960
|
+
const relParamName = `${name}Id`;
|
|
1961
|
+
lines.push(` ${relName}: {`);
|
|
1962
|
+
lines.push(` get(${relParamName}: string): Promise<${targetPascal} | null>,`);
|
|
1963
|
+
lines.push(` },`);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
lines.push(` },`);
|
|
1968
|
+
}
|
|
1969
|
+
lines.push("} as const;");
|
|
1970
|
+
return lines.join(`
|
|
1971
|
+
`);
|
|
1972
|
+
}
|
|
1973
|
+
// src/domain.ts
|
|
1974
|
+
function defineDomain(name, config) {
|
|
1975
|
+
return {
|
|
1976
|
+
name,
|
|
1977
|
+
fields: config.fields,
|
|
1978
|
+
relations: config.relations
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
export {
|
|
1982
|
+
resolveErrorCode,
|
|
1983
|
+
push,
|
|
1984
|
+
parsePgError,
|
|
1985
|
+
migrateStatus,
|
|
1986
|
+
migrateDev,
|
|
1987
|
+
migrateDeploy,
|
|
1988
|
+
generateTypes,
|
|
1989
|
+
generateClient,
|
|
1990
|
+
formatDiagnostic,
|
|
1991
|
+
explainError,
|
|
1992
|
+
diagnoseError,
|
|
1993
|
+
defineDomain,
|
|
1994
|
+
dbErrorToHttpError,
|
|
1995
|
+
d,
|
|
1996
|
+
createRegistry,
|
|
1997
|
+
createEnumRegistry,
|
|
1998
|
+
createDb,
|
|
1999
|
+
computeTenantGraph,
|
|
2000
|
+
UniqueConstraintError,
|
|
2001
|
+
PgCodeToName,
|
|
2002
|
+
NotNullError,
|
|
2003
|
+
NotFoundError,
|
|
2004
|
+
ForeignKeyError,
|
|
2005
|
+
DbErrorCode,
|
|
2006
|
+
DbError,
|
|
2007
|
+
ConnectionPoolExhaustedError,
|
|
2008
|
+
ConnectionError,
|
|
2009
|
+
CheckConstraintError
|
|
2010
|
+
};
|