@vertz/db 0.2.0 → 0.2.1
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 +412 -694
- package/dist/d1/index.d.ts +241 -0
- package/dist/d1/index.js +8 -0
- package/dist/diagnostic/index.js +1 -1
- package/dist/index.d.ts +932 -626
- package/dist/index.js +1753 -533
- package/dist/internals.d.ts +96 -31
- package/dist/internals.js +8 -7
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.js +7 -3
- package/dist/postgres/index.d.ts +77 -0
- package/dist/postgres/index.js +7 -0
- package/dist/shared/{chunk-3f2grpak.js → chunk-0e1vy9qd.js} +147 -52
- package/dist/shared/chunk-2gd1fqcw.js +7 -0
- package/dist/shared/{chunk-xp022dyp.js → chunk-agyds4jw.js} +25 -19
- package/dist/shared/chunk-dvwe5jsq.js +7 -0
- package/dist/shared/chunk-fwk49jvg.js +302 -0
- package/dist/shared/chunk-j4kwq1gh.js +5 -0
- package/dist/shared/{chunk-wj026daz.js → chunk-k04v1jjx.js} +2 -2
- package/dist/shared/chunk-kb4tnn2k.js +26 -0
- package/dist/shared/chunk-ktbebkz5.js +48 -0
- package/dist/shared/chunk-rqe0prft.js +100 -0
- package/dist/shared/chunk-ssga2xea.js +9 -0
- package/dist/shared/{chunk-hrfdj0rr.js → chunk-v2qm94qp.js} +12 -2
- package/dist/sql/index.d.ts +61 -61
- package/dist/sql/index.js +2 -2
- package/dist/sqlite/index.d.ts +221 -0
- package/dist/sqlite/index.js +845 -0
- package/package.json +31 -4
package/dist/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
+
PostgresDialect,
|
|
3
|
+
SqliteDialect,
|
|
2
4
|
buildDelete,
|
|
3
5
|
buildInsert,
|
|
4
6
|
buildSelect,
|
|
5
7
|
buildUpdate,
|
|
6
|
-
buildWhere
|
|
7
|
-
|
|
8
|
+
buildWhere,
|
|
9
|
+
defaultPostgresDialect,
|
|
10
|
+
defaultSqliteDialect
|
|
11
|
+
} from "./shared/chunk-0e1vy9qd.js";
|
|
8
12
|
import {
|
|
9
13
|
CheckConstraintError,
|
|
10
14
|
ConnectionError,
|
|
@@ -15,21 +19,87 @@ import {
|
|
|
15
19
|
NotNullError,
|
|
16
20
|
UniqueConstraintError,
|
|
17
21
|
executeQuery,
|
|
22
|
+
getAutoUpdateColumns,
|
|
18
23
|
getPrimaryKeyColumns,
|
|
24
|
+
getReadOnlyColumns,
|
|
19
25
|
getTimestampColumns,
|
|
20
26
|
mapRow,
|
|
21
27
|
mapRows,
|
|
22
28
|
parsePgError,
|
|
23
29
|
resolveSelectColumns
|
|
24
|
-
} from "./shared/chunk-
|
|
30
|
+
} from "./shared/chunk-agyds4jw.js";
|
|
31
|
+
import"./shared/chunk-kb4tnn2k.js";
|
|
25
32
|
import {
|
|
26
33
|
camelToSnake
|
|
27
|
-
} from "./shared/chunk-
|
|
34
|
+
} from "./shared/chunk-v2qm94qp.js";
|
|
35
|
+
import {
|
|
36
|
+
sha256Hex
|
|
37
|
+
} from "./shared/chunk-ssga2xea.js";
|
|
28
38
|
import {
|
|
29
39
|
diagnoseError,
|
|
30
40
|
explainError,
|
|
31
41
|
formatDiagnostic
|
|
32
|
-
} from "./shared/chunk-
|
|
42
|
+
} from "./shared/chunk-k04v1jjx.js";
|
|
43
|
+
import {
|
|
44
|
+
createD1Adapter,
|
|
45
|
+
createD1Driver
|
|
46
|
+
} from "./shared/chunk-ktbebkz5.js";
|
|
47
|
+
import {
|
|
48
|
+
generateId
|
|
49
|
+
} from "./shared/chunk-fwk49jvg.js";
|
|
50
|
+
// src/adapters/database-bridge-adapter.ts
|
|
51
|
+
function createDatabaseBridgeAdapter(db, tableName) {
|
|
52
|
+
const delegate = db[tableName];
|
|
53
|
+
return {
|
|
54
|
+
async get(id) {
|
|
55
|
+
const result = await delegate.get({ where: { id } });
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return result.data;
|
|
60
|
+
},
|
|
61
|
+
async list(options) {
|
|
62
|
+
const dbOptions = {};
|
|
63
|
+
if (options?.where) {
|
|
64
|
+
dbOptions.where = options.where;
|
|
65
|
+
}
|
|
66
|
+
if (options?.orderBy) {
|
|
67
|
+
dbOptions.orderBy = options.orderBy;
|
|
68
|
+
}
|
|
69
|
+
if (options?.limit !== undefined) {
|
|
70
|
+
dbOptions.limit = options.limit;
|
|
71
|
+
}
|
|
72
|
+
const result = await delegate.listAndCount(dbOptions);
|
|
73
|
+
if (!result.ok) {
|
|
74
|
+
throw result.error;
|
|
75
|
+
}
|
|
76
|
+
return result.data;
|
|
77
|
+
},
|
|
78
|
+
async create(data) {
|
|
79
|
+
const result = await delegate.create({ data });
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
throw result.error;
|
|
82
|
+
}
|
|
83
|
+
return result.data;
|
|
84
|
+
},
|
|
85
|
+
async update(id, data) {
|
|
86
|
+
const result = await delegate.update({ where: { id }, data });
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
throw result.error;
|
|
89
|
+
}
|
|
90
|
+
return result.data;
|
|
91
|
+
},
|
|
92
|
+
async delete(id) {
|
|
93
|
+
const result = await delegate.delete({ where: { id } });
|
|
94
|
+
if (!result.ok) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return result.data;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// src/migration/auto-migrate.ts
|
|
102
|
+
import { isErr } from "@vertz/errors";
|
|
33
103
|
|
|
34
104
|
// src/migration/differ.ts
|
|
35
105
|
function columnSimilarity(a, b) {
|
|
@@ -189,10 +259,26 @@ function computeDiff(before, after) {
|
|
|
189
259
|
}
|
|
190
260
|
return { changes };
|
|
191
261
|
}
|
|
262
|
+
|
|
192
263
|
// src/migration/runner.ts
|
|
193
|
-
import {
|
|
264
|
+
import {
|
|
265
|
+
createMigrationQueryError,
|
|
266
|
+
err,
|
|
267
|
+
ok
|
|
268
|
+
} from "@vertz/errors";
|
|
194
269
|
var HISTORY_TABLE = "_vertz_migrations";
|
|
195
|
-
|
|
270
|
+
function buildCreateHistorySql(dialect) {
|
|
271
|
+
if (dialect.name === "sqlite") {
|
|
272
|
+
return `
|
|
273
|
+
CREATE TABLE IF NOT EXISTS "${HISTORY_TABLE}" (
|
|
274
|
+
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
275
|
+
"name" TEXT NOT NULL UNIQUE,
|
|
276
|
+
"checksum" TEXT NOT NULL,
|
|
277
|
+
"applied_at" TEXT NOT NULL DEFAULT (datetime('now'))
|
|
278
|
+
);
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
return `
|
|
196
282
|
CREATE TABLE IF NOT EXISTS "${HISTORY_TABLE}" (
|
|
197
283
|
"id" serial PRIMARY KEY,
|
|
198
284
|
"name" text NOT NULL UNIQUE,
|
|
@@ -200,8 +286,9 @@ CREATE TABLE IF NOT EXISTS "${HISTORY_TABLE}" (
|
|
|
200
286
|
"applied_at" timestamp with time zone NOT NULL DEFAULT now()
|
|
201
287
|
);
|
|
202
288
|
`;
|
|
203
|
-
|
|
204
|
-
|
|
289
|
+
}
|
|
290
|
+
async function computeChecksum(sql) {
|
|
291
|
+
return sha256Hex(sql);
|
|
205
292
|
}
|
|
206
293
|
function parseMigrationName(filename) {
|
|
207
294
|
const match = filename.match(/^(\d+)_(.+)\.sql$/);
|
|
@@ -212,52 +299,75 @@ function parseMigrationName(filename) {
|
|
|
212
299
|
name: filename
|
|
213
300
|
};
|
|
214
301
|
}
|
|
215
|
-
function createMigrationRunner() {
|
|
302
|
+
function createMigrationRunner(options) {
|
|
303
|
+
const dialect = options?.dialect ?? defaultPostgresDialect;
|
|
304
|
+
const createHistorySql = buildCreateHistorySql(dialect);
|
|
216
305
|
return {
|
|
217
306
|
async createHistoryTable(queryFn) {
|
|
218
|
-
|
|
307
|
+
try {
|
|
308
|
+
await queryFn(createHistorySql, []);
|
|
309
|
+
return ok(undefined);
|
|
310
|
+
} catch (cause) {
|
|
311
|
+
return err(createMigrationQueryError("Failed to create migration history table", {
|
|
312
|
+
sql: createHistorySql,
|
|
313
|
+
cause
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
219
316
|
},
|
|
220
|
-
async apply(queryFn, sql, name,
|
|
221
|
-
const checksum = computeChecksum(sql);
|
|
222
|
-
const recordSql = `INSERT INTO "${HISTORY_TABLE}" ("name", "checksum") VALUES ($1, $2)`;
|
|
317
|
+
async apply(queryFn, sql, name, options2) {
|
|
318
|
+
const checksum = await computeChecksum(sql);
|
|
319
|
+
const recordSql = `INSERT INTO "${HISTORY_TABLE}" ("name", "checksum") VALUES (${dialect.param(1)}, ${dialect.param(2)})`;
|
|
223
320
|
const statements = [sql, recordSql];
|
|
224
|
-
if (
|
|
225
|
-
return {
|
|
321
|
+
if (options2?.dryRun) {
|
|
322
|
+
return ok({
|
|
226
323
|
name,
|
|
227
324
|
sql,
|
|
228
325
|
checksum,
|
|
229
326
|
dryRun: true,
|
|
230
327
|
statements
|
|
231
|
-
};
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
await queryFn(sql, []);
|
|
332
|
+
await queryFn(recordSql, [name, checksum]);
|
|
333
|
+
return ok({
|
|
334
|
+
name,
|
|
335
|
+
sql,
|
|
336
|
+
checksum,
|
|
337
|
+
dryRun: false,
|
|
338
|
+
statements
|
|
339
|
+
});
|
|
340
|
+
} catch (cause) {
|
|
341
|
+
return err(createMigrationQueryError(`Failed to apply migration: ${name}`, {
|
|
342
|
+
sql,
|
|
343
|
+
cause
|
|
344
|
+
}));
|
|
232
345
|
}
|
|
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
346
|
},
|
|
243
347
|
async getApplied(queryFn) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
348
|
+
try {
|
|
349
|
+
const result = await queryFn(`SELECT "name", "checksum", "applied_at" FROM "${HISTORY_TABLE}" ORDER BY "id" ASC`, []);
|
|
350
|
+
return ok(result.rows.map((row) => ({
|
|
351
|
+
name: row.name,
|
|
352
|
+
checksum: row.checksum,
|
|
353
|
+
appliedAt: new Date(row.applied_at)
|
|
354
|
+
})));
|
|
355
|
+
} catch (cause) {
|
|
356
|
+
return err(createMigrationQueryError("Failed to retrieve applied migrations", {
|
|
357
|
+
cause
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
250
360
|
},
|
|
251
361
|
getPending(files, applied) {
|
|
252
362
|
const appliedNames = new Set(applied.map((a) => a.name));
|
|
253
363
|
return files.filter((f) => !appliedNames.has(f.name)).sort((a, b) => a.timestamp - b.timestamp);
|
|
254
364
|
},
|
|
255
|
-
detectDrift(files, applied) {
|
|
365
|
+
async detectDrift(files, applied) {
|
|
256
366
|
const drifted = [];
|
|
257
367
|
const appliedMap = new Map(applied.map((a) => [a.name, a.checksum]));
|
|
258
368
|
for (const file of files) {
|
|
259
369
|
const appliedChecksum = appliedMap.get(file.name);
|
|
260
|
-
if (appliedChecksum && appliedChecksum !== computeChecksum(file.sql)) {
|
|
370
|
+
if (appliedChecksum && appliedChecksum !== await computeChecksum(file.sql)) {
|
|
261
371
|
drifted.push(file.name);
|
|
262
372
|
}
|
|
263
373
|
}
|
|
@@ -278,27 +388,55 @@ function createMigrationRunner() {
|
|
|
278
388
|
};
|
|
279
389
|
}
|
|
280
390
|
|
|
281
|
-
// src/migration/
|
|
282
|
-
function
|
|
283
|
-
return
|
|
391
|
+
// src/migration/sql-generator.ts
|
|
392
|
+
function escapeSqlString(value) {
|
|
393
|
+
return value.replace(/'/g, "''");
|
|
284
394
|
}
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
395
|
+
function isEnumType(col, enums) {
|
|
396
|
+
const typeLower = col.type.toLowerCase();
|
|
397
|
+
if (enums) {
|
|
398
|
+
for (const enumName of Object.keys(enums)) {
|
|
399
|
+
if (typeLower === enumName.toLowerCase() || typeLower === enumName) {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
291
402
|
}
|
|
292
403
|
}
|
|
293
|
-
return
|
|
404
|
+
return false;
|
|
294
405
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
406
|
+
function getEnumValues(col, enums) {
|
|
407
|
+
if (!enums)
|
|
408
|
+
return;
|
|
409
|
+
for (const [enumName, values] of Object.entries(enums)) {
|
|
410
|
+
if (col.type.toLowerCase() === enumName.toLowerCase() || col.type === enumName) {
|
|
411
|
+
return values;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
298
415
|
}
|
|
299
|
-
function columnDef(name, col) {
|
|
416
|
+
function columnDef(name, col, dialect, enums) {
|
|
300
417
|
const snakeName = camelToSnake(name);
|
|
301
|
-
const
|
|
418
|
+
const isEnum = isEnumType(col, enums);
|
|
419
|
+
let sqlType;
|
|
420
|
+
let checkConstraint;
|
|
421
|
+
if (isEnum && dialect.name === "sqlite") {
|
|
422
|
+
sqlType = dialect.mapColumnType("text");
|
|
423
|
+
const enumValues = getEnumValues(col, enums);
|
|
424
|
+
if (enumValues && enumValues.length > 0) {
|
|
425
|
+
const escapedValues = enumValues.map((v) => `'${escapeSqlString(v)}'`).join(", ");
|
|
426
|
+
checkConstraint = `CHECK("${snakeName}" IN (${escapedValues}))`;
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
if (dialect.name === "postgres" && col.type === col.type.toLowerCase()) {
|
|
430
|
+
sqlType = col.type;
|
|
431
|
+
} else {
|
|
432
|
+
const normalizedType = normalizeColumnType(col.type);
|
|
433
|
+
sqlType = dialect.mapColumnType(normalizedType);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const parts = [`"${snakeName}" ${sqlType}`];
|
|
437
|
+
if (checkConstraint) {
|
|
438
|
+
parts.push(checkConstraint);
|
|
439
|
+
}
|
|
302
440
|
if (!col.nullable) {
|
|
303
441
|
parts.push("NOT NULL");
|
|
304
442
|
}
|
|
@@ -310,12 +448,44 @@ function columnDef(name, col) {
|
|
|
310
448
|
}
|
|
311
449
|
return parts.join(" ");
|
|
312
450
|
}
|
|
313
|
-
function
|
|
451
|
+
function normalizeColumnType(type) {
|
|
452
|
+
const typeUpper = type.toUpperCase();
|
|
453
|
+
const typeMap = {
|
|
454
|
+
UUID: "uuid",
|
|
455
|
+
TEXT: "text",
|
|
456
|
+
INTEGER: "integer",
|
|
457
|
+
SERIAL: "serial",
|
|
458
|
+
BOOLEAN: "boolean",
|
|
459
|
+
TIMESTAMPTZ: "timestamp",
|
|
460
|
+
TIMESTAMP: "timestamp",
|
|
461
|
+
"DOUBLE PRECISION": "float",
|
|
462
|
+
JSONB: "json",
|
|
463
|
+
JSON: "json",
|
|
464
|
+
NUMERIC: "decimal",
|
|
465
|
+
REAL: "float",
|
|
466
|
+
VARCHAR: "varchar"
|
|
467
|
+
};
|
|
468
|
+
return typeMap[typeUpper] || type.toLowerCase();
|
|
469
|
+
}
|
|
470
|
+
function generateMigrationSql(changes, ctx, dialect = defaultPostgresDialect) {
|
|
314
471
|
const statements = [];
|
|
315
472
|
const tables = ctx?.tables;
|
|
316
473
|
const enums = ctx?.enums;
|
|
317
474
|
for (const change of changes) {
|
|
318
475
|
switch (change.type) {
|
|
476
|
+
case "enum_added": {
|
|
477
|
+
if (!change.enumName)
|
|
478
|
+
break;
|
|
479
|
+
if (dialect.name === "postgres") {
|
|
480
|
+
const values = enums?.[change.enumName];
|
|
481
|
+
if (!values || values.length === 0)
|
|
482
|
+
break;
|
|
483
|
+
const enumSnakeName = camelToSnake(change.enumName);
|
|
484
|
+
const valuesStr = values.map((v) => `'${escapeSqlString(v)}'`).join(", ");
|
|
485
|
+
statements.push(`CREATE TYPE "${enumSnakeName}" AS ENUM (${valuesStr});`);
|
|
486
|
+
}
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
319
489
|
case "table_added": {
|
|
320
490
|
if (!change.table)
|
|
321
491
|
break;
|
|
@@ -325,8 +495,23 @@ function generateMigrationSql(changes, ctx) {
|
|
|
325
495
|
const tableName = camelToSnake(change.table);
|
|
326
496
|
const cols = [];
|
|
327
497
|
const primaryKeys = [];
|
|
498
|
+
if (dialect.name === "postgres" && enums) {
|
|
499
|
+
for (const [, col] of Object.entries(table.columns)) {
|
|
500
|
+
if (isEnumType(col, enums)) {
|
|
501
|
+
const enumValues = getEnumValues(col, enums);
|
|
502
|
+
if (enumValues && enumValues.length > 0) {
|
|
503
|
+
const enumSnakeName = camelToSnake(col.type);
|
|
504
|
+
const alreadyEmitted = statements.some((s) => s.includes(`CREATE TYPE "${enumSnakeName}"`));
|
|
505
|
+
if (!alreadyEmitted) {
|
|
506
|
+
const valuesStr = enumValues.map((v) => `'${escapeSqlString(v)}'`).join(", ");
|
|
507
|
+
statements.push(`CREATE TYPE "${enumSnakeName}" AS ENUM (${valuesStr});`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
328
513
|
for (const [colName, col] of Object.entries(table.columns)) {
|
|
329
|
-
cols.push(` ${columnDef(colName, col)}`);
|
|
514
|
+
cols.push(` ${columnDef(colName, col, dialect, enums)}`);
|
|
330
515
|
if (col.primary) {
|
|
331
516
|
primaryKeys.push(`"${camelToSnake(colName)}"`);
|
|
332
517
|
}
|
|
@@ -346,7 +531,7 @@ ${cols.join(`,
|
|
|
346
531
|
);`);
|
|
347
532
|
for (const idx of table.indexes) {
|
|
348
533
|
const idxCols = idx.columns.map((c) => `"${camelToSnake(c)}"`).join(", ");
|
|
349
|
-
const idxName = `idx_${tableName}_${idx.columns.map(camelToSnake).join("_")}`;
|
|
534
|
+
const idxName = `idx_${tableName}_${idx.columns.map((c) => camelToSnake(c)).join("_")}`;
|
|
350
535
|
statements.push(`CREATE INDEX "${idxName}" ON "${tableName}" (${idxCols});`);
|
|
351
536
|
}
|
|
352
537
|
break;
|
|
@@ -363,7 +548,7 @@ ${cols.join(`,
|
|
|
363
548
|
const col = tables?.[change.table]?.columns[change.column];
|
|
364
549
|
if (!col)
|
|
365
550
|
break;
|
|
366
|
-
statements.push(`ALTER TABLE "${camelToSnake(change.table)}" ADD COLUMN ${columnDef(change.column, col)};`);
|
|
551
|
+
statements.push(`ALTER TABLE "${camelToSnake(change.table)}" ADD COLUMN ${columnDef(change.column, col, dialect, enums)};`);
|
|
367
552
|
break;
|
|
368
553
|
}
|
|
369
554
|
case "column_removed": {
|
|
@@ -407,7 +592,7 @@ ${cols.join(`,
|
|
|
407
592
|
break;
|
|
408
593
|
const snakeTable = camelToSnake(change.table);
|
|
409
594
|
const idxCols = change.columns.map((c) => `"${camelToSnake(c)}"`).join(", ");
|
|
410
|
-
const idxName = `idx_${snakeTable}_${change.columns.map(camelToSnake).join("_")}`;
|
|
595
|
+
const idxName = `idx_${snakeTable}_${change.columns.map((c) => camelToSnake(c)).join("_")}`;
|
|
411
596
|
statements.push(`CREATE INDEX "${idxName}" ON "${snakeTable}" (${idxCols});`);
|
|
412
597
|
break;
|
|
413
598
|
}
|
|
@@ -415,33 +600,26 @@ ${cols.join(`,
|
|
|
415
600
|
if (!change.table || !change.columns)
|
|
416
601
|
break;
|
|
417
602
|
const snakeTable = camelToSnake(change.table);
|
|
418
|
-
const idxName = `idx_${snakeTable}_${change.columns.map(camelToSnake).join("_")}`;
|
|
603
|
+
const idxName = `idx_${snakeTable}_${change.columns.map((c) => camelToSnake(c)).join("_")}`;
|
|
419
604
|
statements.push(`DROP INDEX "${idxName}";`);
|
|
420
605
|
break;
|
|
421
606
|
}
|
|
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
607
|
case "enum_removed": {
|
|
434
608
|
if (!change.enumName)
|
|
435
609
|
break;
|
|
436
|
-
|
|
610
|
+
if (dialect.name === "postgres") {
|
|
611
|
+
statements.push(`DROP TYPE "${camelToSnake(change.enumName)}";`);
|
|
612
|
+
}
|
|
437
613
|
break;
|
|
438
614
|
}
|
|
439
615
|
case "enum_altered": {
|
|
440
616
|
if (!change.enumName || !change.addedValues)
|
|
441
617
|
break;
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
618
|
+
if (dialect.name === "postgres") {
|
|
619
|
+
const enumSnakeName = camelToSnake(change.enumName);
|
|
620
|
+
for (const val of change.addedValues) {
|
|
621
|
+
statements.push(`ALTER TYPE "${enumSnakeName}" ADD VALUE '${escapeSqlString(val)}';`);
|
|
622
|
+
}
|
|
445
623
|
}
|
|
446
624
|
break;
|
|
447
625
|
}
|
|
@@ -451,22 +629,391 @@ ${cols.join(`,
|
|
|
451
629
|
|
|
452
630
|
`);
|
|
453
631
|
}
|
|
632
|
+
// src/migration/files.ts
|
|
633
|
+
function formatMigrationFilename(num, description) {
|
|
634
|
+
return `${String(num).padStart(4, "0")}_${description}.sql`;
|
|
635
|
+
}
|
|
636
|
+
function nextMigrationNumber(existingFiles) {
|
|
637
|
+
let max = 0;
|
|
638
|
+
for (const file of existingFiles) {
|
|
639
|
+
const parsed = parseMigrationName(file);
|
|
640
|
+
if (parsed && parsed.timestamp > max) {
|
|
641
|
+
max = parsed.timestamp;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return max + 1;
|
|
645
|
+
}
|
|
646
|
+
// src/migration/introspect.ts
|
|
647
|
+
var SQLITE_EXCLUDED_TABLES = new Set(["sqlite_sequence", "_vertz_migrations"]);
|
|
648
|
+
function mapSqliteType(rawType) {
|
|
649
|
+
const upper = rawType.toUpperCase();
|
|
650
|
+
if (upper === "TEXT")
|
|
651
|
+
return "text";
|
|
652
|
+
if (upper === "INTEGER" || upper === "INT")
|
|
653
|
+
return "integer";
|
|
654
|
+
if (upper === "REAL")
|
|
655
|
+
return "float";
|
|
656
|
+
if (upper === "BLOB")
|
|
657
|
+
return "blob";
|
|
658
|
+
return rawType.toLowerCase();
|
|
659
|
+
}
|
|
660
|
+
async function introspectSqlite(queryFn) {
|
|
661
|
+
const snapshot = {
|
|
662
|
+
version: 1,
|
|
663
|
+
tables: {},
|
|
664
|
+
enums: {}
|
|
665
|
+
};
|
|
666
|
+
const { rows: tableRows } = await queryFn("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", []);
|
|
667
|
+
for (const row of tableRows) {
|
|
668
|
+
const tableName = row.name;
|
|
669
|
+
if (SQLITE_EXCLUDED_TABLES.has(tableName))
|
|
670
|
+
continue;
|
|
671
|
+
const columns = {};
|
|
672
|
+
const { rows: colRows } = await queryFn(`PRAGMA table_info("${tableName}")`, []);
|
|
673
|
+
for (const col of colRows) {
|
|
674
|
+
const colName = col.name;
|
|
675
|
+
const colSnap = {
|
|
676
|
+
type: mapSqliteType(col.type),
|
|
677
|
+
nullable: col.notnull === 0 && col.pk === 0,
|
|
678
|
+
primary: col.pk > 0,
|
|
679
|
+
unique: false
|
|
680
|
+
};
|
|
681
|
+
if (col.dflt_value != null) {
|
|
682
|
+
colSnap.default = String(col.dflt_value);
|
|
683
|
+
}
|
|
684
|
+
columns[colName] = colSnap;
|
|
685
|
+
}
|
|
686
|
+
const indexes = [];
|
|
687
|
+
const { rows: indexRows } = await queryFn(`PRAGMA index_list("${tableName}")`, []);
|
|
688
|
+
for (const idx of indexRows) {
|
|
689
|
+
const idxName = idx.name;
|
|
690
|
+
const isUnique = idx.unique === 1;
|
|
691
|
+
const origin = idx.origin;
|
|
692
|
+
const { rows: idxInfoRows } = await queryFn(`PRAGMA index_info("${idxName}")`, []);
|
|
693
|
+
const idxColumns = idxInfoRows.map((r) => r.name);
|
|
694
|
+
if (isUnique && idxColumns.length === 1 && origin === "u") {
|
|
695
|
+
const colName = idxColumns[0];
|
|
696
|
+
if (colName && columns[colName]) {
|
|
697
|
+
columns[colName].unique = true;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (origin === "c") {
|
|
701
|
+
indexes.push({
|
|
702
|
+
columns: idxColumns,
|
|
703
|
+
name: idxName,
|
|
704
|
+
unique: isUnique
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const foreignKeys = [];
|
|
709
|
+
const { rows: fkRows } = await queryFn(`PRAGMA foreign_key_list("${tableName}")`, []);
|
|
710
|
+
for (const fk of fkRows) {
|
|
711
|
+
foreignKeys.push({
|
|
712
|
+
column: fk.from,
|
|
713
|
+
targetTable: fk.table,
|
|
714
|
+
targetColumn: fk.to
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
snapshot.tables[tableName] = {
|
|
718
|
+
columns,
|
|
719
|
+
indexes,
|
|
720
|
+
foreignKeys,
|
|
721
|
+
_metadata: {}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
return snapshot;
|
|
725
|
+
}
|
|
726
|
+
var PG_EXCLUDED_TABLES = new Set(["_vertz_migrations"]);
|
|
727
|
+
async function introspectPostgres(queryFn) {
|
|
728
|
+
const snapshot = {
|
|
729
|
+
version: 1,
|
|
730
|
+
tables: {},
|
|
731
|
+
enums: {}
|
|
732
|
+
};
|
|
733
|
+
const { rows: tableRows } = await queryFn("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'", []);
|
|
734
|
+
const { rows: pkRows } = await queryFn(`SELECT kcu.table_name, kcu.column_name
|
|
735
|
+
FROM information_schema.table_constraints tc
|
|
736
|
+
JOIN information_schema.key_column_usage kcu
|
|
737
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
738
|
+
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public'`, []);
|
|
739
|
+
const pkMap = new Map;
|
|
740
|
+
for (const pk of pkRows) {
|
|
741
|
+
const tbl = pk.table_name;
|
|
742
|
+
if (!pkMap.has(tbl))
|
|
743
|
+
pkMap.set(tbl, new Set);
|
|
744
|
+
pkMap.get(tbl)?.add(pk.column_name);
|
|
745
|
+
}
|
|
746
|
+
const { rows: uniqueRows } = await queryFn(`SELECT tc.table_name, kcu.column_name, tc.constraint_name
|
|
747
|
+
FROM information_schema.table_constraints tc
|
|
748
|
+
JOIN information_schema.key_column_usage kcu
|
|
749
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
750
|
+
WHERE tc.constraint_type = 'UNIQUE' AND tc.table_schema = 'public'`, []);
|
|
751
|
+
const uniqueConstraintCols = new Map;
|
|
752
|
+
for (const u of uniqueRows) {
|
|
753
|
+
const cName = u.constraint_name;
|
|
754
|
+
if (!uniqueConstraintCols.has(cName)) {
|
|
755
|
+
uniqueConstraintCols.set(cName, { table: u.table_name, columns: [] });
|
|
756
|
+
}
|
|
757
|
+
uniqueConstraintCols.get(cName)?.columns.push(u.column_name);
|
|
758
|
+
}
|
|
759
|
+
const uniqueColMap = new Map;
|
|
760
|
+
for (const [, val] of uniqueConstraintCols) {
|
|
761
|
+
if (val.columns.length === 1) {
|
|
762
|
+
if (!uniqueColMap.has(val.table))
|
|
763
|
+
uniqueColMap.set(val.table, new Set);
|
|
764
|
+
uniqueColMap.get(val.table)?.add(val.columns[0]);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
for (const row of tableRows) {
|
|
768
|
+
const tableName = row.table_name;
|
|
769
|
+
if (PG_EXCLUDED_TABLES.has(tableName))
|
|
770
|
+
continue;
|
|
771
|
+
const columns = {};
|
|
772
|
+
const pkCols = pkMap.get(tableName) ?? new Set;
|
|
773
|
+
const uniqueCols = uniqueColMap.get(tableName) ?? new Set;
|
|
774
|
+
const { rows: colRows } = await queryFn(`SELECT column_name, data_type, is_nullable, column_default
|
|
775
|
+
FROM information_schema.columns
|
|
776
|
+
WHERE table_name = $1 AND table_schema = 'public'
|
|
777
|
+
ORDER BY ordinal_position`, [tableName]);
|
|
778
|
+
for (const col of colRows) {
|
|
779
|
+
const colName = col.column_name;
|
|
780
|
+
const isPrimary = pkCols.has(colName);
|
|
781
|
+
const isUnique = uniqueCols.has(colName);
|
|
782
|
+
const colSnap = {
|
|
783
|
+
type: col.data_type,
|
|
784
|
+
nullable: col.is_nullable === "YES",
|
|
785
|
+
primary: isPrimary,
|
|
786
|
+
unique: isUnique
|
|
787
|
+
};
|
|
788
|
+
if (col.column_default != null) {
|
|
789
|
+
colSnap.default = String(col.column_default);
|
|
790
|
+
}
|
|
791
|
+
columns[colName] = colSnap;
|
|
792
|
+
}
|
|
793
|
+
const foreignKeys = [];
|
|
794
|
+
const { rows: fkRows } = await queryFn(`SELECT
|
|
795
|
+
kcu.column_name,
|
|
796
|
+
ccu.table_name AS target_table,
|
|
797
|
+
ccu.column_name AS target_column
|
|
798
|
+
FROM information_schema.table_constraints tc
|
|
799
|
+
JOIN information_schema.key_column_usage kcu
|
|
800
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
801
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
802
|
+
ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
|
|
803
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
804
|
+
AND tc.table_name = $1
|
|
805
|
+
AND tc.table_schema = 'public'`, [tableName]);
|
|
806
|
+
for (const fk of fkRows) {
|
|
807
|
+
foreignKeys.push({
|
|
808
|
+
column: fk.column_name,
|
|
809
|
+
targetTable: fk.target_table,
|
|
810
|
+
targetColumn: fk.target_column
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
const indexes = [];
|
|
814
|
+
const { rows: idxRows } = await queryFn(`SELECT i.relname AS index_name,
|
|
815
|
+
array_agg(a.attname ORDER BY k.n) AS columns,
|
|
816
|
+
ix.indisunique AS is_unique
|
|
817
|
+
FROM pg_index ix
|
|
818
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
819
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
820
|
+
JOIN pg_namespace ns ON ns.oid = t.relnamespace
|
|
821
|
+
JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n) ON true
|
|
822
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
823
|
+
WHERE t.relname = $1
|
|
824
|
+
AND ns.nspname = 'public'
|
|
825
|
+
AND NOT ix.indisprimary
|
|
826
|
+
AND NOT ix.indisunique
|
|
827
|
+
GROUP BY i.relname, ix.indisunique`, [tableName]);
|
|
828
|
+
for (const idx of idxRows) {
|
|
829
|
+
indexes.push({
|
|
830
|
+
columns: idx.columns,
|
|
831
|
+
name: idx.index_name,
|
|
832
|
+
unique: idx.is_unique
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
snapshot.tables[tableName] = {
|
|
836
|
+
columns,
|
|
837
|
+
indexes,
|
|
838
|
+
foreignKeys,
|
|
839
|
+
_metadata: {}
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
const { rows: enumRows } = await queryFn(`SELECT t.typname AS enum_name, e.enumlabel AS enum_value
|
|
843
|
+
FROM pg_type t
|
|
844
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
845
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
846
|
+
WHERE n.nspname = 'public'
|
|
847
|
+
ORDER BY t.typname, e.enumsortorder`, []);
|
|
848
|
+
for (const row of enumRows) {
|
|
849
|
+
const enumName = row.enum_name;
|
|
850
|
+
const enumValue = row.enum_value;
|
|
851
|
+
if (!snapshot.enums[enumName]) {
|
|
852
|
+
snapshot.enums[enumName] = [];
|
|
853
|
+
}
|
|
854
|
+
snapshot.enums[enumName].push(enumValue);
|
|
855
|
+
}
|
|
856
|
+
return snapshot;
|
|
857
|
+
}
|
|
858
|
+
// src/migration/journal.ts
|
|
859
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
860
|
+
import { dirname } from "node:path";
|
|
861
|
+
function createJournal() {
|
|
862
|
+
return {
|
|
863
|
+
version: 1,
|
|
864
|
+
migrations: []
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function addJournalEntry(journal, entry) {
|
|
868
|
+
return {
|
|
869
|
+
...journal,
|
|
870
|
+
migrations: [...journal.migrations, entry]
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function detectCollisions(journal, existingFiles) {
|
|
874
|
+
const journalSeqNumbers = new Map;
|
|
875
|
+
for (const entry of journal.migrations) {
|
|
876
|
+
const parsed = parseMigrationName(entry.name);
|
|
877
|
+
if (parsed) {
|
|
878
|
+
journalSeqNumbers.set(parsed.timestamp, entry.name);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const allUsedSeqNumbers = new Set(journalSeqNumbers.keys());
|
|
882
|
+
for (const file of existingFiles) {
|
|
883
|
+
const parsed = parseMigrationName(file);
|
|
884
|
+
if (parsed) {
|
|
885
|
+
allUsedSeqNumbers.add(parsed.timestamp);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const collisions = [];
|
|
889
|
+
for (const file of existingFiles) {
|
|
890
|
+
const parsed = parseMigrationName(file);
|
|
891
|
+
if (!parsed)
|
|
892
|
+
continue;
|
|
893
|
+
const journalEntry = journalSeqNumbers.get(parsed.timestamp);
|
|
894
|
+
if (journalEntry && journalEntry !== file) {
|
|
895
|
+
let nextSeq = Math.max(...allUsedSeqNumbers) + 1;
|
|
896
|
+
for (const c of collisions) {
|
|
897
|
+
const suggestedParsed = parseMigrationName(c.suggestedName);
|
|
898
|
+
if (suggestedParsed && suggestedParsed.timestamp >= nextSeq) {
|
|
899
|
+
nextSeq = suggestedParsed.timestamp + 1;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const description = file.replace(/^\d+_/, "").replace(/\.sql$/, "");
|
|
903
|
+
collisions.push({
|
|
904
|
+
existingName: journalEntry,
|
|
905
|
+
conflictingName: file,
|
|
906
|
+
sequenceNumber: parsed.timestamp,
|
|
907
|
+
suggestedName: formatMigrationFilename(nextSeq, description)
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return collisions;
|
|
912
|
+
}
|
|
913
|
+
// src/migration/snapshot.ts
|
|
914
|
+
function createSnapshot(tables) {
|
|
915
|
+
const snapshot = {
|
|
916
|
+
version: 1,
|
|
917
|
+
tables: {},
|
|
918
|
+
enums: {}
|
|
919
|
+
};
|
|
920
|
+
for (const table of tables) {
|
|
921
|
+
const columns = {};
|
|
922
|
+
const foreignKeys = [];
|
|
923
|
+
const indexes = [];
|
|
924
|
+
for (const [colName, col] of Object.entries(table._columns)) {
|
|
925
|
+
const meta = col._meta;
|
|
926
|
+
const colSnap = {
|
|
927
|
+
type: meta.sqlType,
|
|
928
|
+
nullable: meta.nullable,
|
|
929
|
+
primary: meta.primary,
|
|
930
|
+
unique: meta.unique
|
|
931
|
+
};
|
|
932
|
+
if (meta.hasDefault && meta.defaultValue !== undefined) {
|
|
933
|
+
const rawDefault = String(meta.defaultValue);
|
|
934
|
+
colSnap.default = rawDefault === "now" ? "now()" : rawDefault;
|
|
935
|
+
}
|
|
936
|
+
const annotationNames = meta._annotations ? Object.keys(meta._annotations) : [];
|
|
937
|
+
if (annotationNames.length > 0) {
|
|
938
|
+
colSnap.annotations = annotationNames;
|
|
939
|
+
}
|
|
940
|
+
columns[colName] = colSnap;
|
|
941
|
+
if (meta.references) {
|
|
942
|
+
foreignKeys.push({
|
|
943
|
+
column: colName,
|
|
944
|
+
targetTable: meta.references.table,
|
|
945
|
+
targetColumn: meta.references.column
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if (meta.enumName && meta.enumValues) {
|
|
949
|
+
snapshot.enums[meta.enumName] = [...meta.enumValues];
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
for (const idx of table._indexes) {
|
|
953
|
+
indexes.push({ columns: [...idx.columns] });
|
|
954
|
+
}
|
|
955
|
+
snapshot.tables[table._name] = {
|
|
956
|
+
columns,
|
|
957
|
+
indexes,
|
|
958
|
+
foreignKeys,
|
|
959
|
+
_metadata: {}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
return snapshot;
|
|
963
|
+
}
|
|
964
|
+
// src/cli/baseline.ts
|
|
965
|
+
async function baseline(options) {
|
|
966
|
+
const runner = createMigrationRunner({ dialect: options.dialect });
|
|
967
|
+
const createResult = await runner.createHistoryTable(options.queryFn);
|
|
968
|
+
if (!createResult.ok) {
|
|
969
|
+
return createResult;
|
|
970
|
+
}
|
|
971
|
+
const appliedResult = await runner.getApplied(options.queryFn);
|
|
972
|
+
if (!appliedResult.ok) {
|
|
973
|
+
return appliedResult;
|
|
974
|
+
}
|
|
975
|
+
const appliedNames = new Set(appliedResult.data.map((a) => a.name));
|
|
976
|
+
const recorded = [];
|
|
977
|
+
for (const file of options.migrationFiles) {
|
|
978
|
+
if (appliedNames.has(file.name)) {
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const checksum = await computeChecksum(file.sql);
|
|
982
|
+
const dialect = options.dialect;
|
|
983
|
+
const param1 = dialect ? dialect.param(1) : "$1";
|
|
984
|
+
const param2 = dialect ? dialect.param(2) : "$2";
|
|
985
|
+
await options.queryFn(`INSERT INTO "_vertz_migrations" ("name", "checksum") VALUES (${param1}, ${param2})`, [file.name, checksum]);
|
|
986
|
+
recorded.push(file.name);
|
|
987
|
+
}
|
|
988
|
+
return {
|
|
989
|
+
ok: true,
|
|
990
|
+
data: { recorded }
|
|
991
|
+
};
|
|
992
|
+
}
|
|
454
993
|
// src/cli/migrate-deploy.ts
|
|
455
994
|
async function migrateDeploy(options) {
|
|
456
995
|
const runner = createMigrationRunner();
|
|
457
996
|
const isDryRun = options.dryRun ?? false;
|
|
458
997
|
if (!isDryRun) {
|
|
459
|
-
await runner.createHistoryTable(options.queryFn);
|
|
998
|
+
const createResult = await runner.createHistoryTable(options.queryFn);
|
|
999
|
+
if (!createResult.ok) {
|
|
1000
|
+
return createResult;
|
|
1001
|
+
}
|
|
460
1002
|
}
|
|
461
1003
|
let applied;
|
|
462
1004
|
if (isDryRun) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
} catch {
|
|
1005
|
+
const appliedResult = await runner.getApplied(options.queryFn);
|
|
1006
|
+
if (!appliedResult.ok) {
|
|
466
1007
|
applied = [];
|
|
1008
|
+
} else {
|
|
1009
|
+
applied = appliedResult.data;
|
|
467
1010
|
}
|
|
468
1011
|
} else {
|
|
469
|
-
|
|
1012
|
+
const appliedResult = await runner.getApplied(options.queryFn);
|
|
1013
|
+
if (!appliedResult.ok) {
|
|
1014
|
+
return appliedResult;
|
|
1015
|
+
}
|
|
1016
|
+
applied = appliedResult.data;
|
|
470
1017
|
}
|
|
471
1018
|
const pending = runner.getPending(options.migrationFiles, applied);
|
|
472
1019
|
const appliedNames = [];
|
|
@@ -475,17 +1022,72 @@ async function migrateDeploy(options) {
|
|
|
475
1022
|
const result = await runner.apply(options.queryFn, migration.sql, migration.name, {
|
|
476
1023
|
dryRun: isDryRun
|
|
477
1024
|
});
|
|
1025
|
+
if (!result.ok) {
|
|
1026
|
+
return result;
|
|
1027
|
+
}
|
|
478
1028
|
appliedNames.push(migration.name);
|
|
479
|
-
migrationResults.push(result);
|
|
1029
|
+
migrationResults.push(result.data);
|
|
480
1030
|
}
|
|
481
1031
|
return {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
1032
|
+
ok: true,
|
|
1033
|
+
data: {
|
|
1034
|
+
applied: appliedNames,
|
|
1035
|
+
alreadyApplied: applied.map((a) => a.name),
|
|
1036
|
+
dryRun: isDryRun,
|
|
1037
|
+
migrations: migrationResults.length > 0 ? migrationResults : undefined
|
|
1038
|
+
}
|
|
486
1039
|
};
|
|
487
1040
|
}
|
|
488
1041
|
// src/cli/migrate-dev.ts
|
|
1042
|
+
function toKebab(str) {
|
|
1043
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1044
|
+
}
|
|
1045
|
+
function generateMigrationName(changes) {
|
|
1046
|
+
if (changes.length === 0)
|
|
1047
|
+
return "empty-migration";
|
|
1048
|
+
if (changes.length === 1) {
|
|
1049
|
+
const change = changes[0];
|
|
1050
|
+
switch (change.type) {
|
|
1051
|
+
case "table_added":
|
|
1052
|
+
return `add-${toKebab(change.table)}-table`;
|
|
1053
|
+
case "table_removed":
|
|
1054
|
+
return `drop-${toKebab(change.table)}-table`;
|
|
1055
|
+
case "column_added":
|
|
1056
|
+
return `add-${toKebab(change.column)}-to-${toKebab(change.table)}`;
|
|
1057
|
+
case "column_removed":
|
|
1058
|
+
return `drop-${toKebab(change.column)}-from-${toKebab(change.table)}`;
|
|
1059
|
+
case "column_altered":
|
|
1060
|
+
return `alter-${toKebab(change.column)}-in-${toKebab(change.table)}`;
|
|
1061
|
+
case "column_renamed":
|
|
1062
|
+
return `rename-${toKebab(change.oldColumn)}-to-${toKebab(change.newColumn)}-in-${toKebab(change.table)}`;
|
|
1063
|
+
case "index_added":
|
|
1064
|
+
return `add-index-to-${toKebab(change.table)}`;
|
|
1065
|
+
case "index_removed":
|
|
1066
|
+
return `drop-index-from-${toKebab(change.table)}`;
|
|
1067
|
+
case "enum_added":
|
|
1068
|
+
return `add-${toKebab(change.enumName)}-enum`;
|
|
1069
|
+
case "enum_removed":
|
|
1070
|
+
return `drop-${toKebab(change.enumName)}-enum`;
|
|
1071
|
+
case "enum_altered":
|
|
1072
|
+
return `alter-${toKebab(change.enumName)}-enum`;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
const tables = new Set(changes.map((c) => c.table).filter(Boolean));
|
|
1076
|
+
const types = new Set(changes.map((c) => c.type));
|
|
1077
|
+
if (tables.size === 1 && types.size === 1) {
|
|
1078
|
+
const table = [...tables][0];
|
|
1079
|
+
const type = [...types][0];
|
|
1080
|
+
switch (type) {
|
|
1081
|
+
case "column_added":
|
|
1082
|
+
return `add-columns-to-${toKebab(table)}`;
|
|
1083
|
+
case "column_removed":
|
|
1084
|
+
return `drop-columns-from-${toKebab(table)}`;
|
|
1085
|
+
default:
|
|
1086
|
+
return `update-${toKebab(table)}`;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return "update-schema";
|
|
1090
|
+
}
|
|
489
1091
|
async function migrateDev(options) {
|
|
490
1092
|
const diff = computeDiff(options.previousSnapshot, options.currentSnapshot);
|
|
491
1093
|
const sql = generateMigrationSql(diff.changes, {
|
|
@@ -498,19 +1100,40 @@ async function migrateDev(options) {
|
|
|
498
1100
|
newColumn: c.newColumn,
|
|
499
1101
|
confidence: c.confidence
|
|
500
1102
|
}));
|
|
1103
|
+
const migrationName = options.migrationName ?? generateMigrationName(diff.changes);
|
|
501
1104
|
const num = nextMigrationNumber(options.existingFiles);
|
|
502
|
-
const filename = formatMigrationFilename(num,
|
|
1105
|
+
const filename = formatMigrationFilename(num, migrationName);
|
|
503
1106
|
const filePath = `${options.migrationsDir}/${filename}`;
|
|
1107
|
+
const journalPath = `${options.migrationsDir}/_journal.json`;
|
|
1108
|
+
let journal;
|
|
1109
|
+
try {
|
|
1110
|
+
const content = options.readFile ? await options.readFile(journalPath) : '{"version":1,"migrations":[]}';
|
|
1111
|
+
journal = JSON.parse(content);
|
|
1112
|
+
} catch {
|
|
1113
|
+
journal = createJournal();
|
|
1114
|
+
}
|
|
1115
|
+
const collisions = detectCollisions(journal, options.existingFiles);
|
|
504
1116
|
if (options.dryRun) {
|
|
505
1117
|
return {
|
|
506
1118
|
migrationFile: filename,
|
|
507
1119
|
sql,
|
|
508
1120
|
dryRun: true,
|
|
509
1121
|
renames: renames.length > 0 ? renames : undefined,
|
|
1122
|
+
collisions: collisions.length > 0 ? collisions : undefined,
|
|
510
1123
|
snapshot: options.currentSnapshot
|
|
511
1124
|
};
|
|
512
1125
|
}
|
|
513
1126
|
await options.writeFile(filePath, sql);
|
|
1127
|
+
const checksum = await computeChecksum(sql);
|
|
1128
|
+
journal = addJournalEntry(journal, {
|
|
1129
|
+
name: filename,
|
|
1130
|
+
description: migrationName,
|
|
1131
|
+
createdAt: new Date().toISOString(),
|
|
1132
|
+
checksum
|
|
1133
|
+
});
|
|
1134
|
+
await options.writeFile(journalPath, JSON.stringify(journal, null, 2));
|
|
1135
|
+
const snapshotPath = `${options.migrationsDir}/_snapshot.json`;
|
|
1136
|
+
await options.writeFile(snapshotPath, JSON.stringify(options.currentSnapshot, null, 2));
|
|
514
1137
|
const runner = createMigrationRunner();
|
|
515
1138
|
await runner.createHistoryTable(options.queryFn);
|
|
516
1139
|
await runner.apply(options.queryFn, sql, filename, { dryRun: false });
|
|
@@ -520,6 +1143,7 @@ async function migrateDev(options) {
|
|
|
520
1143
|
appliedAt: new Date,
|
|
521
1144
|
dryRun: false,
|
|
522
1145
|
renames: renames.length > 0 ? renames : undefined,
|
|
1146
|
+
collisions: collisions.length > 0 ? collisions : undefined,
|
|
523
1147
|
snapshot: options.currentSnapshot
|
|
524
1148
|
};
|
|
525
1149
|
}
|
|
@@ -538,21 +1162,419 @@ async function push(options) {
|
|
|
538
1162
|
];
|
|
539
1163
|
return { sql, tablesAffected };
|
|
540
1164
|
}
|
|
1165
|
+
// src/cli/reset.ts
|
|
1166
|
+
import { createMigrationQueryError as createMigrationQueryError2, err as err2 } from "@vertz/errors";
|
|
1167
|
+
var HISTORY_TABLE2 = "_vertz_migrations";
|
|
1168
|
+
function getUserTablesQuery(dialect) {
|
|
1169
|
+
if (dialect.name === "sqlite") {
|
|
1170
|
+
return `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;
|
|
1171
|
+
}
|
|
1172
|
+
return `SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'`;
|
|
1173
|
+
}
|
|
1174
|
+
function buildDropTableSql(tableName, dialect) {
|
|
1175
|
+
if (dialect.name === "postgres") {
|
|
1176
|
+
return `DROP TABLE IF EXISTS "${tableName}" CASCADE`;
|
|
1177
|
+
}
|
|
1178
|
+
return `DROP TABLE IF EXISTS "${tableName}"`;
|
|
1179
|
+
}
|
|
1180
|
+
async function reset(options) {
|
|
1181
|
+
const dialect = options.dialect ?? defaultPostgresDialect;
|
|
1182
|
+
const runner = createMigrationRunner({ dialect });
|
|
1183
|
+
let tableNames;
|
|
1184
|
+
try {
|
|
1185
|
+
const tablesResult = await options.queryFn(getUserTablesQuery(dialect), []);
|
|
1186
|
+
tableNames = tablesResult.rows.map((row) => row.name);
|
|
1187
|
+
} catch (cause) {
|
|
1188
|
+
return err2(createMigrationQueryError2("Failed to list user tables", { cause }));
|
|
1189
|
+
}
|
|
1190
|
+
const tablesDropped = [];
|
|
1191
|
+
for (const tableName of tableNames) {
|
|
1192
|
+
try {
|
|
1193
|
+
await options.queryFn(buildDropTableSql(tableName, dialect), []);
|
|
1194
|
+
if (tableName !== HISTORY_TABLE2) {
|
|
1195
|
+
tablesDropped.push(tableName);
|
|
1196
|
+
}
|
|
1197
|
+
} catch (cause) {
|
|
1198
|
+
return err2(createMigrationQueryError2(`Failed to drop table: ${tableName}`, { cause }));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
try {
|
|
1202
|
+
await options.queryFn(buildDropTableSql(HISTORY_TABLE2, dialect), []);
|
|
1203
|
+
} catch (cause) {
|
|
1204
|
+
return err2(createMigrationQueryError2("Failed to drop history table", { cause }));
|
|
1205
|
+
}
|
|
1206
|
+
const createResult = await runner.createHistoryTable(options.queryFn);
|
|
1207
|
+
if (!createResult.ok) {
|
|
1208
|
+
return createResult;
|
|
1209
|
+
}
|
|
1210
|
+
const sorted = [...options.migrationFiles].sort((a, b) => a.timestamp - b.timestamp);
|
|
1211
|
+
const migrationsApplied = [];
|
|
1212
|
+
for (const file of sorted) {
|
|
1213
|
+
const applyResult = await runner.apply(options.queryFn, file.sql, file.name);
|
|
1214
|
+
if (!applyResult.ok) {
|
|
1215
|
+
return applyResult;
|
|
1216
|
+
}
|
|
1217
|
+
migrationsApplied.push(file.name);
|
|
1218
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
ok: true,
|
|
1221
|
+
data: { tablesDropped, migrationsApplied }
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
541
1224
|
// src/cli/status.ts
|
|
1225
|
+
function diffChangeToCodeChange(change) {
|
|
1226
|
+
switch (change.type) {
|
|
1227
|
+
case "table_added":
|
|
1228
|
+
return {
|
|
1229
|
+
description: `Added table '${change.table}'`,
|
|
1230
|
+
type: change.type,
|
|
1231
|
+
table: change.table
|
|
1232
|
+
};
|
|
1233
|
+
case "table_removed":
|
|
1234
|
+
return {
|
|
1235
|
+
description: `Removed table '${change.table}'`,
|
|
1236
|
+
type: change.type,
|
|
1237
|
+
table: change.table
|
|
1238
|
+
};
|
|
1239
|
+
case "column_added":
|
|
1240
|
+
return {
|
|
1241
|
+
description: `Added column '${change.column}' to table '${change.table}'`,
|
|
1242
|
+
type: change.type,
|
|
1243
|
+
table: change.table,
|
|
1244
|
+
column: change.column
|
|
1245
|
+
};
|
|
1246
|
+
case "column_removed":
|
|
1247
|
+
return {
|
|
1248
|
+
description: `Removed column '${change.column}' from table '${change.table}'`,
|
|
1249
|
+
type: change.type,
|
|
1250
|
+
table: change.table,
|
|
1251
|
+
column: change.column
|
|
1252
|
+
};
|
|
1253
|
+
case "column_altered":
|
|
1254
|
+
return {
|
|
1255
|
+
description: `Altered column '${change.column}' in table '${change.table}'`,
|
|
1256
|
+
type: change.type,
|
|
1257
|
+
table: change.table,
|
|
1258
|
+
column: change.column
|
|
1259
|
+
};
|
|
1260
|
+
case "column_renamed":
|
|
1261
|
+
return {
|
|
1262
|
+
description: `Renamed column in table '${change.table}'`,
|
|
1263
|
+
type: change.type,
|
|
1264
|
+
table: change.table
|
|
1265
|
+
};
|
|
1266
|
+
case "index_added":
|
|
1267
|
+
return {
|
|
1268
|
+
description: `Added index on table '${change.table}'`,
|
|
1269
|
+
type: change.type,
|
|
1270
|
+
table: change.table
|
|
1271
|
+
};
|
|
1272
|
+
case "index_removed":
|
|
1273
|
+
return {
|
|
1274
|
+
description: `Removed index on table '${change.table}'`,
|
|
1275
|
+
type: change.type,
|
|
1276
|
+
table: change.table
|
|
1277
|
+
};
|
|
1278
|
+
case "enum_added":
|
|
1279
|
+
return {
|
|
1280
|
+
description: `Added enum type`,
|
|
1281
|
+
type: change.type
|
|
1282
|
+
};
|
|
1283
|
+
case "enum_removed":
|
|
1284
|
+
return {
|
|
1285
|
+
description: `Removed enum type`,
|
|
1286
|
+
type: change.type
|
|
1287
|
+
};
|
|
1288
|
+
case "enum_altered":
|
|
1289
|
+
return {
|
|
1290
|
+
description: `Altered enum type`,
|
|
1291
|
+
type: change.type
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
function detectSchemaDrift(expected, actual) {
|
|
1296
|
+
const drift = [];
|
|
1297
|
+
for (const tableName of Object.keys(actual.tables)) {
|
|
1298
|
+
if (!(tableName in expected.tables)) {
|
|
1299
|
+
drift.push({
|
|
1300
|
+
description: `Table '${tableName}' exists in database but not in schema`,
|
|
1301
|
+
type: "extra_table",
|
|
1302
|
+
table: tableName
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
for (const tableName of Object.keys(expected.tables)) {
|
|
1307
|
+
if (!(tableName in actual.tables)) {
|
|
1308
|
+
drift.push({
|
|
1309
|
+
description: `Table '${tableName}' exists in schema but not in database`,
|
|
1310
|
+
type: "missing_table",
|
|
1311
|
+
table: tableName
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
for (const tableName of Object.keys(expected.tables)) {
|
|
1316
|
+
if (!(tableName in actual.tables))
|
|
1317
|
+
continue;
|
|
1318
|
+
const expectedTable = expected.tables[tableName];
|
|
1319
|
+
const actualTable = actual.tables[tableName];
|
|
1320
|
+
if (!expectedTable || !actualTable)
|
|
1321
|
+
continue;
|
|
1322
|
+
for (const colName of Object.keys(actualTable.columns)) {
|
|
1323
|
+
if (!(colName in expectedTable.columns)) {
|
|
1324
|
+
drift.push({
|
|
1325
|
+
description: `Column '${colName}' exists in database table '${tableName}' but not in schema`,
|
|
1326
|
+
type: "extra_column",
|
|
1327
|
+
table: tableName,
|
|
1328
|
+
column: colName
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
for (const colName of Object.keys(expectedTable.columns)) {
|
|
1333
|
+
if (!(colName in actualTable.columns)) {
|
|
1334
|
+
drift.push({
|
|
1335
|
+
description: `Column '${colName}' exists in schema table '${tableName}' but not in database`,
|
|
1336
|
+
type: "missing_column",
|
|
1337
|
+
table: tableName,
|
|
1338
|
+
column: colName
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
for (const colName of Object.keys(expectedTable.columns)) {
|
|
1343
|
+
if (!(colName in actualTable.columns))
|
|
1344
|
+
continue;
|
|
1345
|
+
const expectedCol = expectedTable.columns[colName];
|
|
1346
|
+
const actualCol = actualTable.columns[colName];
|
|
1347
|
+
if (!expectedCol || !actualCol)
|
|
1348
|
+
continue;
|
|
1349
|
+
if (expectedCol.type !== actualCol.type) {
|
|
1350
|
+
drift.push({
|
|
1351
|
+
description: `Column '${colName}' in table '${tableName}' has type '${actualCol.type}' in database but '${expectedCol.type}' in schema`,
|
|
1352
|
+
type: "column_type_mismatch",
|
|
1353
|
+
table: tableName,
|
|
1354
|
+
column: colName
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return drift;
|
|
1360
|
+
}
|
|
542
1361
|
async function migrateStatus(options) {
|
|
543
1362
|
const runner = createMigrationRunner();
|
|
544
|
-
await runner.createHistoryTable(options.queryFn);
|
|
545
|
-
|
|
1363
|
+
const createResult = await runner.createHistoryTable(options.queryFn);
|
|
1364
|
+
if (!createResult.ok) {
|
|
1365
|
+
return createResult;
|
|
1366
|
+
}
|
|
1367
|
+
const appliedResult = await runner.getApplied(options.queryFn);
|
|
1368
|
+
if (!appliedResult.ok) {
|
|
1369
|
+
return appliedResult;
|
|
1370
|
+
}
|
|
1371
|
+
const applied = appliedResult.data;
|
|
546
1372
|
const pending = runner.getPending(options.migrationFiles, applied);
|
|
1373
|
+
let codeChanges = [];
|
|
1374
|
+
if (options.savedSnapshot && options.currentSnapshot) {
|
|
1375
|
+
const diff = computeDiff(options.savedSnapshot, options.currentSnapshot);
|
|
1376
|
+
codeChanges = diff.changes.map(diffChangeToCodeChange);
|
|
1377
|
+
}
|
|
1378
|
+
let drift = [];
|
|
1379
|
+
if (options.dialect) {
|
|
1380
|
+
const introspect = options.dialect.name === "sqlite" ? introspectSqlite : introspectPostgres;
|
|
1381
|
+
const actualSchema = await introspect(options.queryFn);
|
|
1382
|
+
const expectedSchema = options.savedSnapshot ?? options.currentSnapshot;
|
|
1383
|
+
if (expectedSchema) {
|
|
1384
|
+
drift = detectSchemaDrift(expectedSchema, actualSchema);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
547
1387
|
return {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1388
|
+
ok: true,
|
|
1389
|
+
data: {
|
|
1390
|
+
applied: applied.map((a) => ({
|
|
1391
|
+
name: a.name,
|
|
1392
|
+
checksum: a.checksum,
|
|
1393
|
+
appliedAt: a.appliedAt
|
|
1394
|
+
})),
|
|
1395
|
+
pending: pending.map((p) => p.name),
|
|
1396
|
+
codeChanges,
|
|
1397
|
+
drift
|
|
1398
|
+
}
|
|
554
1399
|
};
|
|
555
1400
|
}
|
|
1401
|
+
// src/client/database.ts
|
|
1402
|
+
import { err as err3, ok as ok2 } from "@vertz/schema";
|
|
1403
|
+
// src/errors/error-codes.ts
|
|
1404
|
+
var DbErrorCode = {
|
|
1405
|
+
UNIQUE_VIOLATION: "23505",
|
|
1406
|
+
FOREIGN_KEY_VIOLATION: "23503",
|
|
1407
|
+
NOT_NULL_VIOLATION: "23502",
|
|
1408
|
+
CHECK_VIOLATION: "23514",
|
|
1409
|
+
EXCLUSION_VIOLATION: "23P01",
|
|
1410
|
+
SERIALIZATION_FAILURE: "40001",
|
|
1411
|
+
DEADLOCK_DETECTED: "40P01",
|
|
1412
|
+
CONNECTION_EXCEPTION: "08000",
|
|
1413
|
+
CONNECTION_DOES_NOT_EXIST: "08003",
|
|
1414
|
+
CONNECTION_FAILURE: "08006",
|
|
1415
|
+
NotFound: "NotFound",
|
|
1416
|
+
CONNECTION_ERROR: "CONNECTION_ERROR",
|
|
1417
|
+
POOL_EXHAUSTED: "POOL_EXHAUSTED"
|
|
1418
|
+
};
|
|
1419
|
+
var PgCodeToName = Object.fromEntries(Object.entries(DbErrorCode).map(([name, pgCode]) => [pgCode, name]));
|
|
1420
|
+
function resolveErrorCode(pgCode) {
|
|
1421
|
+
return PgCodeToName[pgCode];
|
|
1422
|
+
}
|
|
1423
|
+
// src/errors/http-adapter.ts
|
|
1424
|
+
function dbErrorToHttpError(error) {
|
|
1425
|
+
const body = error.toJSON();
|
|
1426
|
+
if (error instanceof UniqueConstraintError) {
|
|
1427
|
+
return { status: 409, body };
|
|
1428
|
+
}
|
|
1429
|
+
if (error instanceof NotFoundError) {
|
|
1430
|
+
return { status: 404, body };
|
|
1431
|
+
}
|
|
1432
|
+
if (error instanceof ForeignKeyError) {
|
|
1433
|
+
return { status: 422, body };
|
|
1434
|
+
}
|
|
1435
|
+
if (error instanceof NotNullError) {
|
|
1436
|
+
return { status: 422, body };
|
|
1437
|
+
}
|
|
1438
|
+
if (error instanceof CheckConstraintError) {
|
|
1439
|
+
return { status: 422, body };
|
|
1440
|
+
}
|
|
1441
|
+
if (error instanceof ConnectionError) {
|
|
1442
|
+
return { status: 503, body };
|
|
1443
|
+
}
|
|
1444
|
+
return { status: 500, body };
|
|
1445
|
+
}
|
|
1446
|
+
// src/errors.ts
|
|
1447
|
+
function toReadError(error, query) {
|
|
1448
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
1449
|
+
const errWithCode = error;
|
|
1450
|
+
if (errWithCode.code === "NotFound") {
|
|
1451
|
+
return {
|
|
1452
|
+
code: "NotFound",
|
|
1453
|
+
message: errWithCode.message,
|
|
1454
|
+
table: errWithCode.table ?? "unknown",
|
|
1455
|
+
cause: error
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
if (errWithCode.code.startsWith("08")) {
|
|
1459
|
+
return {
|
|
1460
|
+
code: "CONNECTION_ERROR",
|
|
1461
|
+
message: errWithCode.message,
|
|
1462
|
+
cause: error
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
return {
|
|
1466
|
+
code: "QUERY_ERROR",
|
|
1467
|
+
message: errWithCode.message,
|
|
1468
|
+
sql: query,
|
|
1469
|
+
cause: error
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
if (error instanceof Error) {
|
|
1473
|
+
const message2 = error.message.toLowerCase();
|
|
1474
|
+
if (message2.includes("connection") || message2.includes("ECONNREFUSED") || message2.includes("timeout")) {
|
|
1475
|
+
return {
|
|
1476
|
+
code: "CONNECTION_ERROR",
|
|
1477
|
+
message: error.message,
|
|
1478
|
+
cause: error
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1483
|
+
return {
|
|
1484
|
+
code: "QUERY_ERROR",
|
|
1485
|
+
message,
|
|
1486
|
+
sql: query,
|
|
1487
|
+
cause: error
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function toWriteError(error, query) {
|
|
1491
|
+
if (error instanceof UniqueConstraintError) {
|
|
1492
|
+
return {
|
|
1493
|
+
code: "CONSTRAINT_ERROR",
|
|
1494
|
+
message: error.message,
|
|
1495
|
+
column: error.column,
|
|
1496
|
+
table: error.table,
|
|
1497
|
+
cause: error
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
if (error instanceof ForeignKeyError) {
|
|
1501
|
+
return {
|
|
1502
|
+
code: "CONSTRAINT_ERROR",
|
|
1503
|
+
message: error.message,
|
|
1504
|
+
constraint: error.constraint,
|
|
1505
|
+
table: error.table,
|
|
1506
|
+
cause: error
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
if (error instanceof NotNullError) {
|
|
1510
|
+
return {
|
|
1511
|
+
code: "CONSTRAINT_ERROR",
|
|
1512
|
+
message: error.message,
|
|
1513
|
+
column: error.column,
|
|
1514
|
+
table: error.table,
|
|
1515
|
+
cause: error
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
if (error instanceof CheckConstraintError) {
|
|
1519
|
+
return {
|
|
1520
|
+
code: "CONSTRAINT_ERROR",
|
|
1521
|
+
message: error.message,
|
|
1522
|
+
constraint: error.constraint,
|
|
1523
|
+
table: error.table,
|
|
1524
|
+
cause: error
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
if (error instanceof ConnectionError) {
|
|
1528
|
+
return {
|
|
1529
|
+
code: "CONNECTION_ERROR",
|
|
1530
|
+
message: error.message,
|
|
1531
|
+
cause: error
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
1535
|
+
const pgError = error;
|
|
1536
|
+
if (pgError.code.startsWith("08")) {
|
|
1537
|
+
return {
|
|
1538
|
+
code: "CONNECTION_ERROR",
|
|
1539
|
+
message: pgError.message,
|
|
1540
|
+
cause: error
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
if (pgError.code === "23505" || pgError.code === "23503" || pgError.code === "23502" || pgError.code === "23514") {
|
|
1544
|
+
if (pgError.code === "23505" || pgError.code === "23502") {
|
|
1545
|
+
return {
|
|
1546
|
+
code: "CONSTRAINT_ERROR",
|
|
1547
|
+
message: pgError.message,
|
|
1548
|
+
table: pgError.table,
|
|
1549
|
+
column: pgError.column,
|
|
1550
|
+
cause: error
|
|
1551
|
+
};
|
|
1552
|
+
} else {
|
|
1553
|
+
return {
|
|
1554
|
+
code: "CONSTRAINT_ERROR",
|
|
1555
|
+
message: pgError.message,
|
|
1556
|
+
table: pgError.table,
|
|
1557
|
+
constraint: pgError.constraint,
|
|
1558
|
+
cause: error
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return {
|
|
1563
|
+
code: "QUERY_ERROR",
|
|
1564
|
+
message: pgError.message,
|
|
1565
|
+
sql: query,
|
|
1566
|
+
cause: error
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1570
|
+
return {
|
|
1571
|
+
code: "QUERY_ERROR",
|
|
1572
|
+
message,
|
|
1573
|
+
sql: query,
|
|
1574
|
+
cause: error
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
|
|
556
1578
|
// src/query/aggregate.ts
|
|
557
1579
|
async function count(queryFn, table, options) {
|
|
558
1580
|
const allParams = [];
|
|
@@ -779,19 +1801,33 @@ async function groupBy(queryFn, table, options) {
|
|
|
779
1801
|
}
|
|
780
1802
|
|
|
781
1803
|
// src/query/crud.ts
|
|
1804
|
+
function fillGeneratedIds(table, data) {
|
|
1805
|
+
const filled = { ...data };
|
|
1806
|
+
for (const [name, col] of Object.entries(table._columns)) {
|
|
1807
|
+
const meta = col._meta;
|
|
1808
|
+
if (meta.generate && filled[name] === undefined) {
|
|
1809
|
+
if (meta.sqlType === "integer" || meta.sqlType === "serial" || meta.sqlType === "bigint") {
|
|
1810
|
+
throw new Error(`Column "${name}" has generate: '${meta.generate}' but is type '${meta.sqlType}'. ` + `ID generation is only supported on string column types (text, uuid, varchar).`);
|
|
1811
|
+
}
|
|
1812
|
+
filled[name] = generateId(meta.generate);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
return filled;
|
|
1816
|
+
}
|
|
782
1817
|
function assertNonEmptyWhere(where, operation) {
|
|
783
1818
|
if (Object.keys(where).length === 0) {
|
|
784
1819
|
throw new Error(`${operation} requires a non-empty where clause. ` + "Passing an empty where object would affect all rows.");
|
|
785
1820
|
}
|
|
786
1821
|
}
|
|
787
|
-
async function get(queryFn, table, options) {
|
|
1822
|
+
async function get(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
788
1823
|
const columns = resolveSelectColumns(table, options?.select);
|
|
789
1824
|
const result = buildSelect({
|
|
790
1825
|
table: table._name,
|
|
791
1826
|
columns,
|
|
792
1827
|
where: options?.where,
|
|
793
1828
|
orderBy: options?.orderBy,
|
|
794
|
-
limit: 1
|
|
1829
|
+
limit: 1,
|
|
1830
|
+
dialect
|
|
795
1831
|
});
|
|
796
1832
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
797
1833
|
if (res.rows.length === 0) {
|
|
@@ -799,14 +1835,7 @@ async function get(queryFn, table, options) {
|
|
|
799
1835
|
}
|
|
800
1836
|
return mapRow(res.rows[0]);
|
|
801
1837
|
}
|
|
802
|
-
async function
|
|
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) {
|
|
1838
|
+
async function list(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
810
1839
|
const columns = resolveSelectColumns(table, options?.select);
|
|
811
1840
|
const result = buildSelect({
|
|
812
1841
|
table: table._name,
|
|
@@ -816,12 +1845,13 @@ async function list(queryFn, table, options) {
|
|
|
816
1845
|
limit: options?.limit,
|
|
817
1846
|
offset: options?.offset,
|
|
818
1847
|
cursor: options?.cursor,
|
|
819
|
-
take: options?.take
|
|
1848
|
+
take: options?.take,
|
|
1849
|
+
dialect
|
|
820
1850
|
});
|
|
821
1851
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
822
1852
|
return mapRows(res.rows);
|
|
823
1853
|
}
|
|
824
|
-
async function listAndCount(queryFn, table, options) {
|
|
1854
|
+
async function listAndCount(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
825
1855
|
const columns = resolveSelectColumns(table, options?.select);
|
|
826
1856
|
const result = buildSelect({
|
|
827
1857
|
table: table._name,
|
|
@@ -832,7 +1862,8 @@ async function listAndCount(queryFn, table, options) {
|
|
|
832
1862
|
offset: options?.offset,
|
|
833
1863
|
cursor: options?.cursor,
|
|
834
1864
|
take: options?.take,
|
|
835
|
-
withCount: true
|
|
1865
|
+
withCount: true,
|
|
1866
|
+
dialect
|
|
836
1867
|
});
|
|
837
1868
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
838
1869
|
const rows = res.rows;
|
|
@@ -847,55 +1878,97 @@ async function listAndCount(queryFn, table, options) {
|
|
|
847
1878
|
});
|
|
848
1879
|
return { data, total };
|
|
849
1880
|
}
|
|
850
|
-
async function create(queryFn, table, options) {
|
|
1881
|
+
async function create(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
851
1882
|
const returningColumns = resolveSelectColumns(table, options.select);
|
|
852
1883
|
const nowColumns = getTimestampColumns(table);
|
|
1884
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
1885
|
+
const withIds = fillGeneratedIds(table, options.data);
|
|
1886
|
+
const filteredData = Object.fromEntries(Object.entries(withIds).filter(([key]) => {
|
|
1887
|
+
const col = table._columns[key];
|
|
1888
|
+
const meta = col ? col._meta : undefined;
|
|
1889
|
+
if (meta?.generate)
|
|
1890
|
+
return true;
|
|
1891
|
+
return !readOnlyCols.includes(key);
|
|
1892
|
+
}));
|
|
853
1893
|
const result = buildInsert({
|
|
854
1894
|
table: table._name,
|
|
855
|
-
data:
|
|
1895
|
+
data: filteredData,
|
|
856
1896
|
returning: returningColumns,
|
|
857
|
-
nowColumns
|
|
1897
|
+
nowColumns,
|
|
1898
|
+
dialect
|
|
858
1899
|
});
|
|
859
1900
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
860
1901
|
return mapRow(res.rows[0]);
|
|
861
1902
|
}
|
|
862
|
-
async function createMany(queryFn, table, options) {
|
|
1903
|
+
async function createMany(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
863
1904
|
if (options.data.length === 0) {
|
|
864
1905
|
return { count: 0 };
|
|
865
1906
|
}
|
|
866
1907
|
const nowColumns = getTimestampColumns(table);
|
|
1908
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
1909
|
+
const filteredData = options.data.map((row) => {
|
|
1910
|
+
const withIds = fillGeneratedIds(table, row);
|
|
1911
|
+
return Object.fromEntries(Object.entries(withIds).filter(([key]) => {
|
|
1912
|
+
const col = table._columns[key];
|
|
1913
|
+
const meta = col ? col._meta : undefined;
|
|
1914
|
+
if (meta?.generate)
|
|
1915
|
+
return true;
|
|
1916
|
+
return !readOnlyCols.includes(key);
|
|
1917
|
+
}));
|
|
1918
|
+
});
|
|
867
1919
|
const result = buildInsert({
|
|
868
1920
|
table: table._name,
|
|
869
|
-
data:
|
|
870
|
-
nowColumns
|
|
1921
|
+
data: filteredData,
|
|
1922
|
+
nowColumns,
|
|
1923
|
+
dialect
|
|
871
1924
|
});
|
|
872
1925
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
873
1926
|
return { count: res.rowCount };
|
|
874
1927
|
}
|
|
875
|
-
async function createManyAndReturn(queryFn, table, options) {
|
|
1928
|
+
async function createManyAndReturn(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
876
1929
|
if (options.data.length === 0) {
|
|
877
1930
|
return [];
|
|
878
1931
|
}
|
|
879
1932
|
const returningColumns = resolveSelectColumns(table, options.select);
|
|
880
1933
|
const nowColumns = getTimestampColumns(table);
|
|
1934
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
1935
|
+
const filteredData = options.data.map((row) => {
|
|
1936
|
+
const withIds = fillGeneratedIds(table, row);
|
|
1937
|
+
return Object.fromEntries(Object.entries(withIds).filter(([key]) => {
|
|
1938
|
+
const col = table._columns[key];
|
|
1939
|
+
const meta = col ? col._meta : undefined;
|
|
1940
|
+
if (meta?.generate)
|
|
1941
|
+
return true;
|
|
1942
|
+
return !readOnlyCols.includes(key);
|
|
1943
|
+
}));
|
|
1944
|
+
});
|
|
881
1945
|
const result = buildInsert({
|
|
882
1946
|
table: table._name,
|
|
883
|
-
data:
|
|
1947
|
+
data: filteredData,
|
|
884
1948
|
returning: returningColumns,
|
|
885
|
-
nowColumns
|
|
1949
|
+
nowColumns,
|
|
1950
|
+
dialect
|
|
886
1951
|
});
|
|
887
1952
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
888
1953
|
return mapRows(res.rows);
|
|
889
1954
|
}
|
|
890
|
-
async function update(queryFn, table, options) {
|
|
1955
|
+
async function update(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
891
1956
|
const returningColumns = resolveSelectColumns(table, options.select);
|
|
892
1957
|
const nowColumns = getTimestampColumns(table);
|
|
1958
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
1959
|
+
const autoUpdateCols = getAutoUpdateColumns(table);
|
|
1960
|
+
const filteredData = Object.fromEntries(Object.entries(options.data).filter(([key]) => !readOnlyCols.includes(key)));
|
|
1961
|
+
for (const col of autoUpdateCols) {
|
|
1962
|
+
filteredData[col] = "now";
|
|
1963
|
+
}
|
|
1964
|
+
const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
|
|
893
1965
|
const result = buildUpdate({
|
|
894
1966
|
table: table._name,
|
|
895
|
-
data:
|
|
1967
|
+
data: filteredData,
|
|
896
1968
|
where: options.where,
|
|
897
1969
|
returning: returningColumns,
|
|
898
|
-
nowColumns
|
|
1970
|
+
nowColumns: allNowColumns,
|
|
1971
|
+
dialect
|
|
899
1972
|
});
|
|
900
1973
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
901
1974
|
if (res.rows.length === 0) {
|
|
@@ -903,44 +1976,69 @@ async function update(queryFn, table, options) {
|
|
|
903
1976
|
}
|
|
904
1977
|
return mapRow(res.rows[0]);
|
|
905
1978
|
}
|
|
906
|
-
async function updateMany(queryFn, table, options) {
|
|
1979
|
+
async function updateMany(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
907
1980
|
assertNonEmptyWhere(options.where, "updateMany");
|
|
908
1981
|
const nowColumns = getTimestampColumns(table);
|
|
1982
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
1983
|
+
const autoUpdateCols = getAutoUpdateColumns(table);
|
|
1984
|
+
const filteredData = Object.fromEntries(Object.entries(options.data).filter(([key]) => !readOnlyCols.includes(key)));
|
|
1985
|
+
for (const col of autoUpdateCols) {
|
|
1986
|
+
filteredData[col] = "now";
|
|
1987
|
+
}
|
|
1988
|
+
const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
|
|
909
1989
|
const result = buildUpdate({
|
|
910
1990
|
table: table._name,
|
|
911
|
-
data:
|
|
1991
|
+
data: filteredData,
|
|
912
1992
|
where: options.where,
|
|
913
|
-
nowColumns
|
|
1993
|
+
nowColumns: allNowColumns,
|
|
1994
|
+
dialect
|
|
914
1995
|
});
|
|
915
1996
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
916
1997
|
return { count: res.rowCount };
|
|
917
1998
|
}
|
|
918
|
-
async function upsert(queryFn, table, options) {
|
|
1999
|
+
async function upsert(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
919
2000
|
const returningColumns = resolveSelectColumns(table, options.select);
|
|
920
2001
|
const nowColumns = getTimestampColumns(table);
|
|
2002
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
2003
|
+
const autoUpdateCols = getAutoUpdateColumns(table);
|
|
921
2004
|
const conflictColumns = Object.keys(options.where);
|
|
922
|
-
const
|
|
2005
|
+
const createWithIds = fillGeneratedIds(table, options.create);
|
|
2006
|
+
const filteredCreate = Object.fromEntries(Object.entries(createWithIds).filter(([key]) => {
|
|
2007
|
+
const col = table._columns[key];
|
|
2008
|
+
const meta = col ? col._meta : undefined;
|
|
2009
|
+
if (meta?.generate)
|
|
2010
|
+
return true;
|
|
2011
|
+
return !readOnlyCols.includes(key);
|
|
2012
|
+
}));
|
|
2013
|
+
const filteredUpdate = Object.fromEntries(Object.entries(options.update).filter(([key]) => !readOnlyCols.includes(key)));
|
|
2014
|
+
for (const col of autoUpdateCols) {
|
|
2015
|
+
filteredUpdate[col] = "now";
|
|
2016
|
+
}
|
|
2017
|
+
const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
|
|
2018
|
+
const updateColumns = Object.keys(filteredUpdate);
|
|
923
2019
|
const result = buildInsert({
|
|
924
2020
|
table: table._name,
|
|
925
|
-
data:
|
|
2021
|
+
data: filteredCreate,
|
|
926
2022
|
returning: returningColumns,
|
|
927
|
-
nowColumns,
|
|
2023
|
+
nowColumns: allNowColumns,
|
|
928
2024
|
onConflict: {
|
|
929
2025
|
columns: conflictColumns,
|
|
930
2026
|
action: "update",
|
|
931
2027
|
updateColumns,
|
|
932
|
-
updateValues:
|
|
933
|
-
}
|
|
2028
|
+
updateValues: filteredUpdate
|
|
2029
|
+
},
|
|
2030
|
+
dialect
|
|
934
2031
|
});
|
|
935
2032
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
936
2033
|
return mapRow(res.rows[0]);
|
|
937
2034
|
}
|
|
938
|
-
async function deleteOne(queryFn, table, options) {
|
|
2035
|
+
async function deleteOne(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
939
2036
|
const returningColumns = resolveSelectColumns(table, options.select);
|
|
940
2037
|
const result = buildDelete({
|
|
941
2038
|
table: table._name,
|
|
942
2039
|
where: options.where,
|
|
943
|
-
returning: returningColumns
|
|
2040
|
+
returning: returningColumns,
|
|
2041
|
+
dialect
|
|
944
2042
|
});
|
|
945
2043
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
946
2044
|
if (res.rows.length === 0) {
|
|
@@ -948,11 +2046,12 @@ async function deleteOne(queryFn, table, options) {
|
|
|
948
2046
|
}
|
|
949
2047
|
return mapRow(res.rows[0]);
|
|
950
2048
|
}
|
|
951
|
-
async function deleteMany(queryFn, table, options) {
|
|
2049
|
+
async function deleteMany(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
952
2050
|
assertNonEmptyWhere(options.where, "deleteMany");
|
|
953
2051
|
const result = buildDelete({
|
|
954
2052
|
table: table._name,
|
|
955
|
-
where: options.where
|
|
2053
|
+
where: options.where,
|
|
2054
|
+
dialect
|
|
956
2055
|
});
|
|
957
2056
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
958
2057
|
return { count: res.rowCount };
|
|
@@ -1187,60 +2286,95 @@ async function loadManyToManyRelation(queryFn, primaryRows, def, target, relName
|
|
|
1187
2286
|
}
|
|
1188
2287
|
}
|
|
1189
2288
|
|
|
1190
|
-
// src/client/
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
constraint: error.constraint_name,
|
|
1203
|
-
detail: error.detail
|
|
1204
|
-
});
|
|
1205
|
-
throw adapted;
|
|
2289
|
+
// src/client/sqlite-value-converter.ts
|
|
2290
|
+
function fromSqliteValue(value, columnType) {
|
|
2291
|
+
if (columnType === "boolean") {
|
|
2292
|
+
if (value === 1) {
|
|
2293
|
+
return true;
|
|
2294
|
+
}
|
|
2295
|
+
if (value === 0) {
|
|
2296
|
+
return false;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
if ((columnType === "timestamp" || columnType === "timestamp with time zone") && typeof value === "string") {
|
|
2300
|
+
return new Date(value);
|
|
1206
2301
|
}
|
|
1207
|
-
|
|
2302
|
+
return value;
|
|
1208
2303
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
2304
|
+
|
|
2305
|
+
// src/client/sqlite-driver.ts
|
|
2306
|
+
function buildTableSchema(models) {
|
|
2307
|
+
const registry = new Map;
|
|
2308
|
+
for (const [, entry] of Object.entries(models)) {
|
|
2309
|
+
const tableName = entry.table._name;
|
|
2310
|
+
const columnTypes = {};
|
|
2311
|
+
for (const [colName, colBuilder] of Object.entries(entry.table._columns)) {
|
|
2312
|
+
const meta = colBuilder._meta;
|
|
2313
|
+
columnTypes[colName] = meta.sqlType;
|
|
2314
|
+
}
|
|
2315
|
+
registry.set(tableName, columnTypes);
|
|
2316
|
+
}
|
|
2317
|
+
return registry;
|
|
2318
|
+
}
|
|
2319
|
+
function extractTableName(sql) {
|
|
2320
|
+
const fromMatch = sql.match(/\bFROM\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
|
|
2321
|
+
if (fromMatch) {
|
|
2322
|
+
return fromMatch[1].toLowerCase();
|
|
2323
|
+
}
|
|
2324
|
+
const insertMatch = sql.match(/\bINSERT\s+INTO\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
|
|
2325
|
+
if (insertMatch) {
|
|
2326
|
+
return insertMatch[1].toLowerCase();
|
|
2327
|
+
}
|
|
2328
|
+
const updateMatch = sql.match(/\bUPDATE\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
|
|
2329
|
+
if (updateMatch) {
|
|
2330
|
+
return updateMatch[1].toLowerCase();
|
|
2331
|
+
}
|
|
2332
|
+
const deleteMatch = sql.match(/\bDELETE\s+FROM\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
|
|
2333
|
+
if (deleteMatch) {
|
|
2334
|
+
return deleteMatch[1].toLowerCase();
|
|
2335
|
+
}
|
|
2336
|
+
return null;
|
|
2337
|
+
}
|
|
2338
|
+
function createSqliteDriver(d1, tableSchema) {
|
|
2339
|
+
const query = async (sql, params) => {
|
|
2340
|
+
const prepared = d1.prepare(sql);
|
|
2341
|
+
const bound = params ? prepared.bind(...params) : prepared;
|
|
2342
|
+
const result = await bound.all();
|
|
2343
|
+
if (tableSchema && result.results.length > 0) {
|
|
2344
|
+
const tableName = extractTableName(sql);
|
|
2345
|
+
if (tableName) {
|
|
2346
|
+
const schema = tableSchema.get(tableName);
|
|
2347
|
+
if (schema) {
|
|
2348
|
+
return result.results.map((row) => {
|
|
2349
|
+
const convertedRow = {};
|
|
2350
|
+
for (const [key, value] of Object.entries(row)) {
|
|
2351
|
+
const columnType = schema[key];
|
|
2352
|
+
if (columnType) {
|
|
2353
|
+
convertedRow[key] = fromSqliteValue(value, columnType);
|
|
2354
|
+
} else {
|
|
2355
|
+
convertedRow[key] = value;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
return convertedRow;
|
|
2359
|
+
});
|
|
1223
2360
|
}
|
|
1224
|
-
|
|
1225
|
-
});
|
|
1226
|
-
return {
|
|
1227
|
-
rows,
|
|
1228
|
-
rowCount: result.count ?? rows.length
|
|
1229
|
-
};
|
|
1230
|
-
} catch (error) {
|
|
1231
|
-
adaptPostgresError(error);
|
|
2361
|
+
}
|
|
1232
2362
|
}
|
|
2363
|
+
return result.results;
|
|
2364
|
+
};
|
|
2365
|
+
const execute = async (sql, params) => {
|
|
2366
|
+
const prepared = d1.prepare(sql);
|
|
2367
|
+
const bound = params ? prepared.bind(...params) : prepared;
|
|
2368
|
+
const result = await bound.run();
|
|
2369
|
+
return { rowsAffected: result.meta.changes };
|
|
1233
2370
|
};
|
|
1234
2371
|
return {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
async isHealthy() {
|
|
2372
|
+
query,
|
|
2373
|
+
execute,
|
|
2374
|
+
close: async () => {},
|
|
2375
|
+
isHealthy: async () => {
|
|
1240
2376
|
try {
|
|
1241
|
-
|
|
1242
|
-
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Health check timed out")), healthCheckTimeout));
|
|
1243
|
-
await Promise.race([sql`SELECT 1`, timeout]);
|
|
2377
|
+
await query("SELECT 1");
|
|
1244
2378
|
return true;
|
|
1245
2379
|
} catch {
|
|
1246
2380
|
return false;
|
|
@@ -1248,18 +2382,6 @@ function createPostgresDriver(url, pool) {
|
|
|
1248
2382
|
}
|
|
1249
2383
|
};
|
|
1250
2384
|
}
|
|
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
2385
|
|
|
1264
2386
|
// src/client/tenant-graph.ts
|
|
1265
2387
|
function computeTenantGraph(registry) {
|
|
@@ -1379,16 +2501,31 @@ function isReadQuery(sqlStr) {
|
|
|
1379
2501
|
}
|
|
1380
2502
|
return upper.startsWith("SELECT");
|
|
1381
2503
|
}
|
|
1382
|
-
function
|
|
1383
|
-
const entry =
|
|
2504
|
+
function resolveModel(models, name) {
|
|
2505
|
+
const entry = models[name];
|
|
1384
2506
|
if (!entry) {
|
|
1385
2507
|
throw new Error(`Table "${name}" is not registered in the database.`);
|
|
1386
2508
|
}
|
|
1387
2509
|
return entry;
|
|
1388
2510
|
}
|
|
2511
|
+
var RESERVED_MODEL_NAMES = new Set(["query", "close", "isHealthy", "_internals"]);
|
|
1389
2512
|
function createDb(options) {
|
|
1390
|
-
const {
|
|
1391
|
-
const
|
|
2513
|
+
const { models, log, dialect } = options;
|
|
2514
|
+
for (const key of Object.keys(models)) {
|
|
2515
|
+
if (RESERVED_MODEL_NAMES.has(key)) {
|
|
2516
|
+
throw new Error(`Model name "${key}" is reserved. Choose a different name for this model. ` + `Reserved names: ${[...RESERVED_MODEL_NAMES].join(", ")}`);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
if (dialect === "sqlite") {
|
|
2520
|
+
if (!options.d1) {
|
|
2521
|
+
throw new Error("SQLite dialect requires a D1 binding");
|
|
2522
|
+
}
|
|
2523
|
+
if (options.url) {
|
|
2524
|
+
throw new Error("SQLite dialect uses D1, not a connection URL");
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
const dialectObj = dialect === "sqlite" ? defaultSqliteDialect : defaultPostgresDialect;
|
|
2528
|
+
const tenantGraph = computeTenantGraph(models);
|
|
1392
2529
|
if (log && tenantGraph.root !== null) {
|
|
1393
2530
|
const allScoped = new Set([
|
|
1394
2531
|
...tenantGraph.root !== null ? [tenantGraph.root] : [],
|
|
@@ -1396,28 +2533,51 @@ function createDb(options) {
|
|
|
1396
2533
|
...tenantGraph.indirectlyScoped,
|
|
1397
2534
|
...tenantGraph.shared
|
|
1398
2535
|
]);
|
|
1399
|
-
for (const [key, entry] of Object.entries(
|
|
2536
|
+
for (const [key, entry] of Object.entries(models)) {
|
|
1400
2537
|
if (!allScoped.has(key)) {
|
|
1401
2538
|
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
2539
|
}
|
|
1403
2540
|
}
|
|
1404
2541
|
}
|
|
1405
|
-
const
|
|
2542
|
+
const modelsRegistry = models;
|
|
1406
2543
|
let driver = null;
|
|
2544
|
+
let sqliteDriver = null;
|
|
1407
2545
|
let replicaDrivers = [];
|
|
1408
2546
|
let replicaIndex = 0;
|
|
1409
2547
|
const queryFn = (() => {
|
|
1410
2548
|
if (options._queryFn) {
|
|
1411
2549
|
return options._queryFn;
|
|
1412
2550
|
}
|
|
2551
|
+
if (dialect === "sqlite" && options.d1) {
|
|
2552
|
+
const tableSchema = buildTableSchema(models);
|
|
2553
|
+
sqliteDriver = createSqliteDriver(options.d1, tableSchema);
|
|
2554
|
+
return async (sqlStr, params) => {
|
|
2555
|
+
if (!sqliteDriver) {
|
|
2556
|
+
throw new Error("SQLite driver not initialized");
|
|
2557
|
+
}
|
|
2558
|
+
const rows = await sqliteDriver.query(sqlStr, params);
|
|
2559
|
+
return { rows, rowCount: rows.length };
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
1413
2562
|
if (options.url) {
|
|
1414
|
-
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
2563
|
+
let initialized = false;
|
|
2564
|
+
const initPostgres = async () => {
|
|
2565
|
+
if (initialized)
|
|
2566
|
+
return;
|
|
2567
|
+
const { createPostgresDriver } = await import("./shared/chunk-2gd1fqcw.js");
|
|
2568
|
+
driver = createPostgresDriver(options.url, options.pool);
|
|
2569
|
+
const replicas = options.pool?.replicas;
|
|
2570
|
+
if (replicas && replicas.length > 0) {
|
|
2571
|
+
replicaDrivers = replicas.map((replicaUrl) => createPostgresDriver(replicaUrl, options.pool));
|
|
2572
|
+
}
|
|
2573
|
+
initialized = true;
|
|
2574
|
+
};
|
|
1419
2575
|
return async (sqlStr, params) => {
|
|
2576
|
+
await initPostgres();
|
|
1420
2577
|
if (replicaDrivers.length === 0) {
|
|
2578
|
+
if (!driver) {
|
|
2579
|
+
throw new Error("Database driver not initialized");
|
|
2580
|
+
}
|
|
1421
2581
|
return driver.queryFn(sqlStr, params);
|
|
1422
2582
|
}
|
|
1423
2583
|
if (isReadQuery(sqlStr)) {
|
|
@@ -1425,127 +2585,260 @@ function createDb(options) {
|
|
|
1425
2585
|
replicaIndex = (replicaIndex + 1) % replicaDrivers.length;
|
|
1426
2586
|
try {
|
|
1427
2587
|
return await targetReplica.queryFn(sqlStr, params);
|
|
1428
|
-
} catch (
|
|
1429
|
-
console.warn("[vertz/db] replica query failed, falling back to primary:",
|
|
2588
|
+
} catch (err4) {
|
|
2589
|
+
console.warn("[vertz/db] replica query failed, falling back to primary:", err4.message);
|
|
1430
2590
|
}
|
|
1431
2591
|
}
|
|
2592
|
+
if (!driver) {
|
|
2593
|
+
throw new Error("Database driver not initialized");
|
|
2594
|
+
}
|
|
1432
2595
|
return driver.queryFn(sqlStr, params);
|
|
1433
2596
|
};
|
|
1434
2597
|
}
|
|
1435
2598
|
return async () => {
|
|
1436
|
-
throw new Error("db.query() requires a connected
|
|
2599
|
+
throw new Error("db.query() requires a connected database driver. Provide a `url` to connect to PostgreSQL, a `dialect` with D1 binding for SQLite, or `_queryFn` for testing.");
|
|
1437
2600
|
};
|
|
1438
2601
|
})();
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
2602
|
+
function implGet(name, opts) {
|
|
2603
|
+
return (async () => {
|
|
2604
|
+
try {
|
|
2605
|
+
const entry = resolveModel(models, name);
|
|
2606
|
+
const result = await get(queryFn, entry.table, opts, dialectObj);
|
|
2607
|
+
if (result !== null && opts?.include) {
|
|
2608
|
+
const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2609
|
+
return ok2(rows[0] ?? null);
|
|
2610
|
+
}
|
|
2611
|
+
return ok2(result);
|
|
2612
|
+
} catch (e) {
|
|
2613
|
+
return err3(toReadError(e));
|
|
1448
2614
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
2615
|
+
})();
|
|
2616
|
+
}
|
|
2617
|
+
function implGetRequired(name, opts) {
|
|
2618
|
+
return (async () => {
|
|
2619
|
+
try {
|
|
2620
|
+
const entry = resolveModel(models, name);
|
|
2621
|
+
const result = await get(queryFn, entry.table, opts, dialectObj);
|
|
2622
|
+
if (result === null) {
|
|
2623
|
+
return err3({
|
|
2624
|
+
code: "NotFound",
|
|
2625
|
+
message: `Record not found in table ${name}`,
|
|
2626
|
+
table: name
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
if (opts?.include) {
|
|
2630
|
+
const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2631
|
+
return ok2(rows[0]);
|
|
2632
|
+
}
|
|
2633
|
+
return ok2(result);
|
|
2634
|
+
} catch (e) {
|
|
2635
|
+
return err3(toReadError(e));
|
|
1454
2636
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
const
|
|
1462
|
-
|
|
2637
|
+
})();
|
|
2638
|
+
}
|
|
2639
|
+
function implList(name, opts) {
|
|
2640
|
+
return (async () => {
|
|
2641
|
+
try {
|
|
2642
|
+
const entry = resolveModel(models, name);
|
|
2643
|
+
const results = await list(queryFn, entry.table, opts, dialectObj);
|
|
2644
|
+
if (opts?.include && results.length > 0) {
|
|
2645
|
+
const withRelations = await loadRelations(queryFn, results, entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2646
|
+
return ok2(withRelations);
|
|
2647
|
+
}
|
|
2648
|
+
return ok2(results);
|
|
2649
|
+
} catch (e) {
|
|
2650
|
+
return err3(toReadError(e));
|
|
1463
2651
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
const
|
|
1471
|
-
|
|
2652
|
+
})();
|
|
2653
|
+
}
|
|
2654
|
+
function implListAndCount(name, opts) {
|
|
2655
|
+
return (async () => {
|
|
2656
|
+
try {
|
|
2657
|
+
const entry = resolveModel(models, name);
|
|
2658
|
+
const { data, total } = await listAndCount(queryFn, entry.table, opts, dialectObj);
|
|
2659
|
+
if (opts?.include && data.length > 0) {
|
|
2660
|
+
const withRelations = await loadRelations(queryFn, data, entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2661
|
+
return ok2({ data: withRelations, total });
|
|
2662
|
+
}
|
|
2663
|
+
return ok2({ data, total });
|
|
2664
|
+
} catch (e) {
|
|
2665
|
+
return err3(toReadError(e));
|
|
1472
2666
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
2667
|
+
})();
|
|
2668
|
+
}
|
|
2669
|
+
function implCreate(name, opts) {
|
|
2670
|
+
return (async () => {
|
|
2671
|
+
try {
|
|
2672
|
+
const entry = resolveModel(models, name);
|
|
2673
|
+
const result = await create(queryFn, entry.table, opts, dialectObj);
|
|
2674
|
+
return ok2(result);
|
|
2675
|
+
} catch (e) {
|
|
2676
|
+
return err3(toWriteError(e));
|
|
1480
2677
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
const
|
|
1488
|
-
return
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
async
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
2678
|
+
})();
|
|
2679
|
+
}
|
|
2680
|
+
function implCreateMany(name, opts) {
|
|
2681
|
+
return (async () => {
|
|
2682
|
+
try {
|
|
2683
|
+
const entry = resolveModel(models, name);
|
|
2684
|
+
const result = await createMany(queryFn, entry.table, opts, dialectObj);
|
|
2685
|
+
return ok2(result);
|
|
2686
|
+
} catch (e) {
|
|
2687
|
+
return err3(toWriteError(e));
|
|
2688
|
+
}
|
|
2689
|
+
})();
|
|
2690
|
+
}
|
|
2691
|
+
function implCreateManyAndReturn(name, opts) {
|
|
2692
|
+
return (async () => {
|
|
2693
|
+
try {
|
|
2694
|
+
const entry = resolveModel(models, name);
|
|
2695
|
+
const result = await createManyAndReturn(queryFn, entry.table, opts, dialectObj);
|
|
2696
|
+
return ok2(result);
|
|
2697
|
+
} catch (e) {
|
|
2698
|
+
return err3(toWriteError(e));
|
|
2699
|
+
}
|
|
2700
|
+
})();
|
|
2701
|
+
}
|
|
2702
|
+
function implUpdate(name, opts) {
|
|
2703
|
+
return (async () => {
|
|
2704
|
+
try {
|
|
2705
|
+
const entry = resolveModel(models, name);
|
|
2706
|
+
const result = await update(queryFn, entry.table, opts, dialectObj);
|
|
2707
|
+
return ok2(result);
|
|
2708
|
+
} catch (e) {
|
|
2709
|
+
return err3(toWriteError(e));
|
|
2710
|
+
}
|
|
2711
|
+
})();
|
|
2712
|
+
}
|
|
2713
|
+
function implUpdateMany(name, opts) {
|
|
2714
|
+
return (async () => {
|
|
2715
|
+
try {
|
|
2716
|
+
const entry = resolveModel(models, name);
|
|
2717
|
+
const result = await updateMany(queryFn, entry.table, opts, dialectObj);
|
|
2718
|
+
return ok2(result);
|
|
2719
|
+
} catch (e) {
|
|
2720
|
+
return err3(toWriteError(e));
|
|
2721
|
+
}
|
|
2722
|
+
})();
|
|
2723
|
+
}
|
|
2724
|
+
function implUpsert(name, opts) {
|
|
2725
|
+
return (async () => {
|
|
2726
|
+
try {
|
|
2727
|
+
const entry = resolveModel(models, name);
|
|
2728
|
+
const result = await upsert(queryFn, entry.table, opts, dialectObj);
|
|
2729
|
+
return ok2(result);
|
|
2730
|
+
} catch (e) {
|
|
2731
|
+
return err3(toWriteError(e));
|
|
2732
|
+
}
|
|
2733
|
+
})();
|
|
2734
|
+
}
|
|
2735
|
+
function implDelete(name, opts) {
|
|
2736
|
+
return (async () => {
|
|
2737
|
+
try {
|
|
2738
|
+
const entry = resolveModel(models, name);
|
|
2739
|
+
const result = await deleteOne(queryFn, entry.table, opts, dialectObj);
|
|
2740
|
+
return ok2(result);
|
|
2741
|
+
} catch (e) {
|
|
2742
|
+
return err3(toWriteError(e));
|
|
2743
|
+
}
|
|
2744
|
+
})();
|
|
2745
|
+
}
|
|
2746
|
+
function implDeleteMany(name, opts) {
|
|
2747
|
+
return (async () => {
|
|
2748
|
+
try {
|
|
2749
|
+
const entry = resolveModel(models, name);
|
|
2750
|
+
const result = await deleteMany(queryFn, entry.table, opts, dialectObj);
|
|
2751
|
+
return ok2(result);
|
|
2752
|
+
} catch (e) {
|
|
2753
|
+
return err3(toWriteError(e));
|
|
2754
|
+
}
|
|
2755
|
+
})();
|
|
2756
|
+
}
|
|
2757
|
+
function implCount(name, opts) {
|
|
2758
|
+
return (async () => {
|
|
2759
|
+
try {
|
|
2760
|
+
const entry = resolveModel(models, name);
|
|
2761
|
+
const result = await count(queryFn, entry.table, opts);
|
|
2762
|
+
return ok2(result);
|
|
2763
|
+
} catch (e) {
|
|
2764
|
+
return err3(toReadError(e));
|
|
2765
|
+
}
|
|
2766
|
+
})();
|
|
2767
|
+
}
|
|
2768
|
+
function implAggregate(name, opts) {
|
|
2769
|
+
return (async () => {
|
|
2770
|
+
try {
|
|
2771
|
+
const entry = resolveModel(models, name);
|
|
2772
|
+
const result = await aggregate(queryFn, entry.table, opts);
|
|
2773
|
+
return ok2(result);
|
|
2774
|
+
} catch (e) {
|
|
2775
|
+
return err3(toReadError(e));
|
|
2776
|
+
}
|
|
2777
|
+
})();
|
|
2778
|
+
}
|
|
2779
|
+
function implGroupBy(name, opts) {
|
|
2780
|
+
return (async () => {
|
|
2781
|
+
try {
|
|
2782
|
+
const entry = resolveModel(models, name);
|
|
2783
|
+
const result = await groupBy(queryFn, entry.table, opts);
|
|
2784
|
+
return ok2(result);
|
|
2785
|
+
} catch (e) {
|
|
2786
|
+
return err3(toReadError(e));
|
|
2787
|
+
}
|
|
2788
|
+
})();
|
|
2789
|
+
}
|
|
2790
|
+
const client = {};
|
|
2791
|
+
for (const name of Object.keys(models)) {
|
|
2792
|
+
client[name] = {
|
|
2793
|
+
get: (opts) => implGet(name, opts),
|
|
2794
|
+
getOrThrow: (opts) => implGetRequired(name, opts),
|
|
2795
|
+
list: (opts) => implList(name, opts),
|
|
2796
|
+
listAndCount: (opts) => implListAndCount(name, opts),
|
|
2797
|
+
create: (opts) => implCreate(name, opts),
|
|
2798
|
+
createMany: (opts) => implCreateMany(name, opts),
|
|
2799
|
+
createManyAndReturn: (opts) => implCreateManyAndReturn(name, opts),
|
|
2800
|
+
update: (opts) => implUpdate(name, opts),
|
|
2801
|
+
updateMany: (opts) => implUpdateMany(name, opts),
|
|
2802
|
+
upsert: (opts) => implUpsert(name, opts),
|
|
2803
|
+
delete: (opts) => implDelete(name, opts),
|
|
2804
|
+
deleteMany: (opts) => implDeleteMany(name, opts),
|
|
2805
|
+
count: (opts) => implCount(name, opts),
|
|
2806
|
+
aggregate: (opts) => implAggregate(name, opts),
|
|
2807
|
+
groupBy: (opts) => implGroupBy(name, opts)
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
client.query = async (fragment) => {
|
|
2811
|
+
try {
|
|
2812
|
+
const result = await executeQuery(queryFn, fragment.sql, fragment.params);
|
|
2813
|
+
return ok2(result);
|
|
2814
|
+
} catch (e) {
|
|
2815
|
+
return err3(toReadError(e, fragment.sql));
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
client.close = async () => {
|
|
2819
|
+
if (driver) {
|
|
2820
|
+
await driver.close();
|
|
2821
|
+
}
|
|
2822
|
+
if (sqliteDriver) {
|
|
2823
|
+
await sqliteDriver.close();
|
|
2824
|
+
}
|
|
2825
|
+
await Promise.all(replicaDrivers.map((r) => r.close()));
|
|
2826
|
+
};
|
|
2827
|
+
client.isHealthy = async () => {
|
|
2828
|
+
if (driver) {
|
|
2829
|
+
return driver.isHealthy();
|
|
1547
2830
|
}
|
|
2831
|
+
if (sqliteDriver) {
|
|
2832
|
+
return sqliteDriver.isHealthy();
|
|
2833
|
+
}
|
|
2834
|
+
return true;
|
|
2835
|
+
};
|
|
2836
|
+
client._internals = {
|
|
2837
|
+
models,
|
|
2838
|
+
dialect: dialectObj,
|
|
2839
|
+
tenantGraph
|
|
1548
2840
|
};
|
|
2841
|
+
return client;
|
|
1549
2842
|
}
|
|
1550
2843
|
// src/schema/column.ts
|
|
1551
2844
|
function cloneWith(source, metaOverrides) {
|
|
@@ -1554,8 +2847,12 @@ function cloneWith(source, metaOverrides) {
|
|
|
1554
2847
|
function createColumnWithMeta(meta) {
|
|
1555
2848
|
const col = {
|
|
1556
2849
|
_meta: meta,
|
|
1557
|
-
primary() {
|
|
1558
|
-
|
|
2850
|
+
primary(options) {
|
|
2851
|
+
const meta2 = { primary: true, hasDefault: true };
|
|
2852
|
+
if (options?.generate) {
|
|
2853
|
+
meta2.generate = options.generate;
|
|
2854
|
+
}
|
|
2855
|
+
return cloneWith(this, meta2);
|
|
1559
2856
|
},
|
|
1560
2857
|
unique() {
|
|
1561
2858
|
return cloneWith(this, { unique: true });
|
|
@@ -1566,11 +2863,17 @@ function createColumnWithMeta(meta) {
|
|
|
1566
2863
|
default(value) {
|
|
1567
2864
|
return cloneWith(this, { hasDefault: true, defaultValue: value });
|
|
1568
2865
|
},
|
|
1569
|
-
|
|
1570
|
-
return
|
|
2866
|
+
is(flag) {
|
|
2867
|
+
return createColumnWithMeta({
|
|
2868
|
+
...this._meta,
|
|
2869
|
+
_annotations: { ...this._meta._annotations, [flag]: true }
|
|
2870
|
+
});
|
|
2871
|
+
},
|
|
2872
|
+
readOnly() {
|
|
2873
|
+
return cloneWith(this, { isReadOnly: true });
|
|
1571
2874
|
},
|
|
1572
|
-
|
|
1573
|
-
return cloneWith(this, {
|
|
2875
|
+
autoUpdate() {
|
|
2876
|
+
return cloneWith(this, { isAutoUpdate: true, isReadOnly: true });
|
|
1574
2877
|
},
|
|
1575
2878
|
check(sql) {
|
|
1576
2879
|
return cloneWith(this, { check: sql });
|
|
@@ -1590,8 +2893,9 @@ function defaultMeta(sqlType) {
|
|
|
1590
2893
|
unique: false,
|
|
1591
2894
|
nullable: false,
|
|
1592
2895
|
hasDefault: false,
|
|
1593
|
-
|
|
1594
|
-
|
|
2896
|
+
_annotations: {},
|
|
2897
|
+
isReadOnly: false,
|
|
2898
|
+
isAutoUpdate: false,
|
|
1595
2899
|
isTenant: false,
|
|
1596
2900
|
references: null,
|
|
1597
2901
|
check: null
|
|
@@ -1610,8 +2914,9 @@ function createSerialColumn() {
|
|
|
1610
2914
|
unique: false,
|
|
1611
2915
|
nullable: false,
|
|
1612
2916
|
hasDefault: true,
|
|
1613
|
-
|
|
1614
|
-
|
|
2917
|
+
_annotations: {},
|
|
2918
|
+
isReadOnly: false,
|
|
2919
|
+
isAutoUpdate: false,
|
|
1615
2920
|
isTenant: false,
|
|
1616
2921
|
references: null,
|
|
1617
2922
|
check: null
|
|
@@ -1624,14 +2929,101 @@ function createTenantColumn(targetTableName) {
|
|
|
1624
2929
|
unique: false,
|
|
1625
2930
|
nullable: false,
|
|
1626
2931
|
hasDefault: false,
|
|
1627
|
-
|
|
1628
|
-
|
|
2932
|
+
_annotations: {},
|
|
2933
|
+
isReadOnly: false,
|
|
2934
|
+
isAutoUpdate: false,
|
|
1629
2935
|
isTenant: true,
|
|
1630
2936
|
references: { table: targetTableName, column: "id" },
|
|
1631
2937
|
check: null
|
|
1632
2938
|
});
|
|
1633
2939
|
}
|
|
1634
2940
|
|
|
2941
|
+
// src/schema/model-schemas.ts
|
|
2942
|
+
function deriveSchemas(table) {
|
|
2943
|
+
const hiddenCols = getColumnNamesWithAnnotation(table, "hidden");
|
|
2944
|
+
const readOnlyCols = getColumnNamesWhere(table, "isReadOnly");
|
|
2945
|
+
const primaryCols = getColumnNamesWhere(table, "primary");
|
|
2946
|
+
const allCols = new Set(Object.keys(table._columns));
|
|
2947
|
+
const defaultCols = getColumnNamesWhere(table, "hasDefault");
|
|
2948
|
+
const responseCols = setDifference(allCols, hiddenCols);
|
|
2949
|
+
const inputCols = setDifference(setDifference(allCols, readOnlyCols), primaryCols);
|
|
2950
|
+
const requiredCols = getRequiredInputColumns(table, inputCols, defaultCols);
|
|
2951
|
+
return {
|
|
2952
|
+
response: {
|
|
2953
|
+
parse(value) {
|
|
2954
|
+
return { ok: true, data: pickKeys(value, responseCols) };
|
|
2955
|
+
}
|
|
2956
|
+
},
|
|
2957
|
+
createInput: {
|
|
2958
|
+
parse(value) {
|
|
2959
|
+
const data = value;
|
|
2960
|
+
const missing = requiredCols.filter((col) => !(col in data) || data[col] === undefined);
|
|
2961
|
+
if (missing.length > 0) {
|
|
2962
|
+
return {
|
|
2963
|
+
ok: false,
|
|
2964
|
+
error: new Error(`Missing required fields: ${missing.join(", ")}`)
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
return { ok: true, data: pickKeys(value, inputCols) };
|
|
2968
|
+
}
|
|
2969
|
+
},
|
|
2970
|
+
updateInput: {
|
|
2971
|
+
parse(value) {
|
|
2972
|
+
return { ok: true, data: pickKeys(value, inputCols) };
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
function pickKeys(value, allowed) {
|
|
2978
|
+
const data = value;
|
|
2979
|
+
const result = {};
|
|
2980
|
+
for (const [key, val] of Object.entries(data)) {
|
|
2981
|
+
if (allowed.has(key)) {
|
|
2982
|
+
result[key] = val;
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
return result;
|
|
2986
|
+
}
|
|
2987
|
+
function setDifference(a, b) {
|
|
2988
|
+
const result = new Set;
|
|
2989
|
+
for (const item of a) {
|
|
2990
|
+
if (!b.has(item)) {
|
|
2991
|
+
result.add(item);
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
return result;
|
|
2995
|
+
}
|
|
2996
|
+
function getRequiredInputColumns(_table, allowed, defaults) {
|
|
2997
|
+
return [...allowed].filter((key) => !defaults.has(key));
|
|
2998
|
+
}
|
|
2999
|
+
function getColumnNamesWhere(table, flag) {
|
|
3000
|
+
const result = new Set;
|
|
3001
|
+
for (const [key, col] of Object.entries(table._columns)) {
|
|
3002
|
+
if (col._meta[flag]) {
|
|
3003
|
+
result.add(key);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
return result;
|
|
3007
|
+
}
|
|
3008
|
+
function getColumnNamesWithAnnotation(table, annotation) {
|
|
3009
|
+
const result = new Set;
|
|
3010
|
+
for (const [key, col] of Object.entries(table._columns)) {
|
|
3011
|
+
if (col._meta._annotations[annotation]) {
|
|
3012
|
+
result.add(key);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
return result;
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// src/schema/model.ts
|
|
3019
|
+
function createModel(table, relations) {
|
|
3020
|
+
return {
|
|
3021
|
+
table,
|
|
3022
|
+
relations: relations ?? {},
|
|
3023
|
+
schemas: deriveSchemas(table)
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
|
|
1635
3027
|
// src/schema/relation.ts
|
|
1636
3028
|
function createOneRelation(target, foreignKey) {
|
|
1637
3029
|
return {
|
|
@@ -1663,9 +3055,10 @@ function createManyRelation(target, foreignKey) {
|
|
|
1663
3055
|
}
|
|
1664
3056
|
|
|
1665
3057
|
// src/schema/table.ts
|
|
1666
|
-
function createIndex(columns) {
|
|
3058
|
+
function createIndex(columns, options) {
|
|
1667
3059
|
return {
|
|
1668
|
-
columns: Array.isArray(columns) ? columns : [columns]
|
|
3060
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
3061
|
+
...options
|
|
1669
3062
|
};
|
|
1670
3063
|
}
|
|
1671
3064
|
function createTable(name, columns, options) {
|
|
@@ -1689,10 +3082,13 @@ function createTableInternal(name, columns, indexes, shared) {
|
|
|
1689
3082
|
get $update() {
|
|
1690
3083
|
return;
|
|
1691
3084
|
},
|
|
1692
|
-
get $
|
|
3085
|
+
get $response() {
|
|
3086
|
+
return;
|
|
3087
|
+
},
|
|
3088
|
+
get $create_input() {
|
|
1693
3089
|
return;
|
|
1694
3090
|
},
|
|
1695
|
-
get $
|
|
3091
|
+
get $update_input() {
|
|
1696
3092
|
return;
|
|
1697
3093
|
},
|
|
1698
3094
|
shared() {
|
|
@@ -1718,7 +3114,13 @@ var d = {
|
|
|
1718
3114
|
timestamp: () => createColumn("timestamp with time zone"),
|
|
1719
3115
|
date: () => createColumn("date"),
|
|
1720
3116
|
time: () => createColumn("time"),
|
|
1721
|
-
jsonb: (
|
|
3117
|
+
jsonb: (schemaOrOpts) => {
|
|
3118
|
+
if (schemaOrOpts && "parse" in schemaOrOpts && !("validator" in schemaOrOpts)) {
|
|
3119
|
+
return createColumn("jsonb", { validator: schemaOrOpts });
|
|
3120
|
+
}
|
|
3121
|
+
const opts = schemaOrOpts;
|
|
3122
|
+
return createColumn("jsonb", opts?.validator ? { validator: opts.validator } : {});
|
|
3123
|
+
},
|
|
1722
3124
|
textArray: () => createColumn("text[]"),
|
|
1723
3125
|
integerArray: () => createColumn("integer[]"),
|
|
1724
3126
|
enum: (name, valuesOrSchema) => {
|
|
@@ -1738,50 +3140,24 @@ var d = {
|
|
|
1738
3140
|
entry: (table, relations = {}) => ({
|
|
1739
3141
|
table,
|
|
1740
3142
|
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"
|
|
3143
|
+
}),
|
|
3144
|
+
model: (table, relations = {}) => createModel(table, relations)
|
|
1758
3145
|
};
|
|
1759
|
-
|
|
1760
|
-
function
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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 };
|
|
3146
|
+
// src/schema/define-annotations.ts
|
|
3147
|
+
function defineAnnotations(...annotations) {
|
|
3148
|
+
const result = {};
|
|
3149
|
+
for (const annotation of annotations) {
|
|
3150
|
+
result[annotation] = annotation;
|
|
1780
3151
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
3152
|
+
return Object.freeze(result);
|
|
3153
|
+
}
|
|
3154
|
+
// src/schema/enum-registry.ts
|
|
3155
|
+
function createEnumRegistry(definitions) {
|
|
3156
|
+
const registry = {};
|
|
3157
|
+
for (const [name, values] of Object.entries(definitions)) {
|
|
3158
|
+
registry[name] = { name, values };
|
|
1783
3159
|
}
|
|
1784
|
-
return
|
|
3160
|
+
return registry;
|
|
1785
3161
|
}
|
|
1786
3162
|
// src/schema/registry.ts
|
|
1787
3163
|
function createRegistry(tables, relationsCallback) {
|
|
@@ -1809,195 +3185,39 @@ function createRegistry(tables, relationsCallback) {
|
|
|
1809
3185
|
}
|
|
1810
3186
|
return result;
|
|
1811
3187
|
}
|
|
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
3188
|
export {
|
|
3189
|
+
toWriteError,
|
|
3190
|
+
toReadError,
|
|
1982
3191
|
resolveErrorCode,
|
|
3192
|
+
reset,
|
|
1983
3193
|
push,
|
|
1984
3194
|
parsePgError,
|
|
3195
|
+
parseMigrationName,
|
|
1985
3196
|
migrateStatus,
|
|
1986
3197
|
migrateDev,
|
|
1987
3198
|
migrateDeploy,
|
|
1988
|
-
|
|
1989
|
-
generateClient,
|
|
3199
|
+
generateId,
|
|
1990
3200
|
formatDiagnostic,
|
|
1991
3201
|
explainError,
|
|
1992
3202
|
diagnoseError,
|
|
1993
|
-
|
|
3203
|
+
detectSchemaDrift,
|
|
3204
|
+
defineAnnotations,
|
|
3205
|
+
defaultSqliteDialect,
|
|
3206
|
+
defaultPostgresDialect,
|
|
1994
3207
|
dbErrorToHttpError,
|
|
1995
3208
|
d,
|
|
3209
|
+
createSnapshot,
|
|
1996
3210
|
createRegistry,
|
|
1997
3211
|
createEnumRegistry,
|
|
1998
3212
|
createDb,
|
|
3213
|
+
createDatabaseBridgeAdapter,
|
|
3214
|
+
createD1Driver,
|
|
3215
|
+
createD1Adapter,
|
|
1999
3216
|
computeTenantGraph,
|
|
3217
|
+
baseline,
|
|
2000
3218
|
UniqueConstraintError,
|
|
3219
|
+
SqliteDialect,
|
|
3220
|
+
PostgresDialect,
|
|
2001
3221
|
PgCodeToName,
|
|
2002
3222
|
NotNullError,
|
|
2003
3223
|
NotFoundError,
|