@vertz/db 0.2.0 → 0.2.3
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 +1759 -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-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-pnk6yzjv.js +48 -0
- package/dist/shared/chunk-rqe0prft.js +100 -0
- package/dist/shared/chunk-sfmyxz6r.js +306 -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 +32 -5
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-pnk6yzjv.js";
|
|
47
|
+
import {
|
|
48
|
+
generateId
|
|
49
|
+
} from "./shared/chunk-sfmyxz6r.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,397 @@ ${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 validateIdentifier(name) {
|
|
649
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
650
|
+
throw new Error(`Invalid SQL identifier: "${name}"`);
|
|
651
|
+
}
|
|
652
|
+
return name;
|
|
653
|
+
}
|
|
654
|
+
function mapSqliteType(rawType) {
|
|
655
|
+
const upper = rawType.toUpperCase();
|
|
656
|
+
if (upper === "TEXT")
|
|
657
|
+
return "text";
|
|
658
|
+
if (upper === "INTEGER" || upper === "INT")
|
|
659
|
+
return "integer";
|
|
660
|
+
if (upper === "REAL")
|
|
661
|
+
return "float";
|
|
662
|
+
if (upper === "BLOB")
|
|
663
|
+
return "blob";
|
|
664
|
+
return rawType.toLowerCase();
|
|
665
|
+
}
|
|
666
|
+
async function introspectSqlite(queryFn) {
|
|
667
|
+
const snapshot = {
|
|
668
|
+
version: 1,
|
|
669
|
+
tables: {},
|
|
670
|
+
enums: {}
|
|
671
|
+
};
|
|
672
|
+
const { rows: tableRows } = await queryFn("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", []);
|
|
673
|
+
for (const row of tableRows) {
|
|
674
|
+
const tableName = row.name;
|
|
675
|
+
if (SQLITE_EXCLUDED_TABLES.has(tableName))
|
|
676
|
+
continue;
|
|
677
|
+
const columns = {};
|
|
678
|
+
const { rows: colRows } = await queryFn(`PRAGMA table_info("${validateIdentifier(tableName)}")`, []);
|
|
679
|
+
for (const col of colRows) {
|
|
680
|
+
const colName = col.name;
|
|
681
|
+
const colSnap = {
|
|
682
|
+
type: mapSqliteType(col.type),
|
|
683
|
+
nullable: col.notnull === 0 && col.pk === 0,
|
|
684
|
+
primary: col.pk > 0,
|
|
685
|
+
unique: false
|
|
686
|
+
};
|
|
687
|
+
if (col.dflt_value != null) {
|
|
688
|
+
colSnap.default = String(col.dflt_value);
|
|
689
|
+
}
|
|
690
|
+
columns[colName] = colSnap;
|
|
691
|
+
}
|
|
692
|
+
const indexes = [];
|
|
693
|
+
const { rows: indexRows } = await queryFn(`PRAGMA index_list("${validateIdentifier(tableName)}")`, []);
|
|
694
|
+
for (const idx of indexRows) {
|
|
695
|
+
const idxName = idx.name;
|
|
696
|
+
const isUnique = idx.unique === 1;
|
|
697
|
+
const origin = idx.origin;
|
|
698
|
+
const { rows: idxInfoRows } = await queryFn(`PRAGMA index_info("${validateIdentifier(idxName)}")`, []);
|
|
699
|
+
const idxColumns = idxInfoRows.map((r) => r.name);
|
|
700
|
+
if (isUnique && idxColumns.length === 1 && origin === "u") {
|
|
701
|
+
const colName = idxColumns[0];
|
|
702
|
+
if (colName && columns[colName]) {
|
|
703
|
+
columns[colName].unique = true;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (origin === "c") {
|
|
707
|
+
indexes.push({
|
|
708
|
+
columns: idxColumns,
|
|
709
|
+
name: idxName,
|
|
710
|
+
unique: isUnique
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const foreignKeys = [];
|
|
715
|
+
const { rows: fkRows } = await queryFn(`PRAGMA foreign_key_list("${validateIdentifier(tableName)}")`, []);
|
|
716
|
+
for (const fk of fkRows) {
|
|
717
|
+
foreignKeys.push({
|
|
718
|
+
column: fk.from,
|
|
719
|
+
targetTable: fk.table,
|
|
720
|
+
targetColumn: fk.to
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
snapshot.tables[tableName] = {
|
|
724
|
+
columns,
|
|
725
|
+
indexes,
|
|
726
|
+
foreignKeys,
|
|
727
|
+
_metadata: {}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
return snapshot;
|
|
731
|
+
}
|
|
732
|
+
var PG_EXCLUDED_TABLES = new Set(["_vertz_migrations"]);
|
|
733
|
+
async function introspectPostgres(queryFn) {
|
|
734
|
+
const snapshot = {
|
|
735
|
+
version: 1,
|
|
736
|
+
tables: {},
|
|
737
|
+
enums: {}
|
|
738
|
+
};
|
|
739
|
+
const { rows: tableRows } = await queryFn("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'", []);
|
|
740
|
+
const { rows: pkRows } = await queryFn(`SELECT kcu.table_name, kcu.column_name
|
|
741
|
+
FROM information_schema.table_constraints tc
|
|
742
|
+
JOIN information_schema.key_column_usage kcu
|
|
743
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
744
|
+
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public'`, []);
|
|
745
|
+
const pkMap = new Map;
|
|
746
|
+
for (const pk of pkRows) {
|
|
747
|
+
const tbl = pk.table_name;
|
|
748
|
+
if (!pkMap.has(tbl))
|
|
749
|
+
pkMap.set(tbl, new Set);
|
|
750
|
+
pkMap.get(tbl)?.add(pk.column_name);
|
|
751
|
+
}
|
|
752
|
+
const { rows: uniqueRows } = await queryFn(`SELECT tc.table_name, kcu.column_name, tc.constraint_name
|
|
753
|
+
FROM information_schema.table_constraints tc
|
|
754
|
+
JOIN information_schema.key_column_usage kcu
|
|
755
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
756
|
+
WHERE tc.constraint_type = 'UNIQUE' AND tc.table_schema = 'public'`, []);
|
|
757
|
+
const uniqueConstraintCols = new Map;
|
|
758
|
+
for (const u of uniqueRows) {
|
|
759
|
+
const cName = u.constraint_name;
|
|
760
|
+
if (!uniqueConstraintCols.has(cName)) {
|
|
761
|
+
uniqueConstraintCols.set(cName, { table: u.table_name, columns: [] });
|
|
762
|
+
}
|
|
763
|
+
uniqueConstraintCols.get(cName)?.columns.push(u.column_name);
|
|
764
|
+
}
|
|
765
|
+
const uniqueColMap = new Map;
|
|
766
|
+
for (const [, val] of uniqueConstraintCols) {
|
|
767
|
+
if (val.columns.length === 1) {
|
|
768
|
+
if (!uniqueColMap.has(val.table))
|
|
769
|
+
uniqueColMap.set(val.table, new Set);
|
|
770
|
+
uniqueColMap.get(val.table)?.add(val.columns[0]);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
for (const row of tableRows) {
|
|
774
|
+
const tableName = row.table_name;
|
|
775
|
+
if (PG_EXCLUDED_TABLES.has(tableName))
|
|
776
|
+
continue;
|
|
777
|
+
const columns = {};
|
|
778
|
+
const pkCols = pkMap.get(tableName) ?? new Set;
|
|
779
|
+
const uniqueCols = uniqueColMap.get(tableName) ?? new Set;
|
|
780
|
+
const { rows: colRows } = await queryFn(`SELECT column_name, data_type, is_nullable, column_default
|
|
781
|
+
FROM information_schema.columns
|
|
782
|
+
WHERE table_name = $1 AND table_schema = 'public'
|
|
783
|
+
ORDER BY ordinal_position`, [tableName]);
|
|
784
|
+
for (const col of colRows) {
|
|
785
|
+
const colName = col.column_name;
|
|
786
|
+
const isPrimary = pkCols.has(colName);
|
|
787
|
+
const isUnique = uniqueCols.has(colName);
|
|
788
|
+
const colSnap = {
|
|
789
|
+
type: col.data_type,
|
|
790
|
+
nullable: col.is_nullable === "YES",
|
|
791
|
+
primary: isPrimary,
|
|
792
|
+
unique: isUnique
|
|
793
|
+
};
|
|
794
|
+
if (col.column_default != null) {
|
|
795
|
+
colSnap.default = String(col.column_default);
|
|
796
|
+
}
|
|
797
|
+
columns[colName] = colSnap;
|
|
798
|
+
}
|
|
799
|
+
const foreignKeys = [];
|
|
800
|
+
const { rows: fkRows } = await queryFn(`SELECT
|
|
801
|
+
kcu.column_name,
|
|
802
|
+
ccu.table_name AS target_table,
|
|
803
|
+
ccu.column_name AS target_column
|
|
804
|
+
FROM information_schema.table_constraints tc
|
|
805
|
+
JOIN information_schema.key_column_usage kcu
|
|
806
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
807
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
808
|
+
ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
|
|
809
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
810
|
+
AND tc.table_name = $1
|
|
811
|
+
AND tc.table_schema = 'public'`, [tableName]);
|
|
812
|
+
for (const fk of fkRows) {
|
|
813
|
+
foreignKeys.push({
|
|
814
|
+
column: fk.column_name,
|
|
815
|
+
targetTable: fk.target_table,
|
|
816
|
+
targetColumn: fk.target_column
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
const indexes = [];
|
|
820
|
+
const { rows: idxRows } = await queryFn(`SELECT i.relname AS index_name,
|
|
821
|
+
array_agg(a.attname ORDER BY k.n) AS columns,
|
|
822
|
+
ix.indisunique AS is_unique
|
|
823
|
+
FROM pg_index ix
|
|
824
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
825
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
826
|
+
JOIN pg_namespace ns ON ns.oid = t.relnamespace
|
|
827
|
+
JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n) ON true
|
|
828
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
829
|
+
WHERE t.relname = $1
|
|
830
|
+
AND ns.nspname = 'public'
|
|
831
|
+
AND NOT ix.indisprimary
|
|
832
|
+
AND NOT ix.indisunique
|
|
833
|
+
GROUP BY i.relname, ix.indisunique`, [tableName]);
|
|
834
|
+
for (const idx of idxRows) {
|
|
835
|
+
indexes.push({
|
|
836
|
+
columns: idx.columns,
|
|
837
|
+
name: idx.index_name,
|
|
838
|
+
unique: idx.is_unique
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
snapshot.tables[tableName] = {
|
|
842
|
+
columns,
|
|
843
|
+
indexes,
|
|
844
|
+
foreignKeys,
|
|
845
|
+
_metadata: {}
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
const { rows: enumRows } = await queryFn(`SELECT t.typname AS enum_name, e.enumlabel AS enum_value
|
|
849
|
+
FROM pg_type t
|
|
850
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
851
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
852
|
+
WHERE n.nspname = 'public'
|
|
853
|
+
ORDER BY t.typname, e.enumsortorder`, []);
|
|
854
|
+
for (const row of enumRows) {
|
|
855
|
+
const enumName = row.enum_name;
|
|
856
|
+
const enumValue = row.enum_value;
|
|
857
|
+
if (!snapshot.enums[enumName]) {
|
|
858
|
+
snapshot.enums[enumName] = [];
|
|
859
|
+
}
|
|
860
|
+
snapshot.enums[enumName].push(enumValue);
|
|
861
|
+
}
|
|
862
|
+
return snapshot;
|
|
863
|
+
}
|
|
864
|
+
// src/migration/journal.ts
|
|
865
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
866
|
+
import { dirname } from "node:path";
|
|
867
|
+
function createJournal() {
|
|
868
|
+
return {
|
|
869
|
+
version: 1,
|
|
870
|
+
migrations: []
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function addJournalEntry(journal, entry) {
|
|
874
|
+
return {
|
|
875
|
+
...journal,
|
|
876
|
+
migrations: [...journal.migrations, entry]
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function detectCollisions(journal, existingFiles) {
|
|
880
|
+
const journalSeqNumbers = new Map;
|
|
881
|
+
for (const entry of journal.migrations) {
|
|
882
|
+
const parsed = parseMigrationName(entry.name);
|
|
883
|
+
if (parsed) {
|
|
884
|
+
journalSeqNumbers.set(parsed.timestamp, entry.name);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const allUsedSeqNumbers = new Set(journalSeqNumbers.keys());
|
|
888
|
+
for (const file of existingFiles) {
|
|
889
|
+
const parsed = parseMigrationName(file);
|
|
890
|
+
if (parsed) {
|
|
891
|
+
allUsedSeqNumbers.add(parsed.timestamp);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const collisions = [];
|
|
895
|
+
for (const file of existingFiles) {
|
|
896
|
+
const parsed = parseMigrationName(file);
|
|
897
|
+
if (!parsed)
|
|
898
|
+
continue;
|
|
899
|
+
const journalEntry = journalSeqNumbers.get(parsed.timestamp);
|
|
900
|
+
if (journalEntry && journalEntry !== file) {
|
|
901
|
+
let nextSeq = Math.max(...allUsedSeqNumbers) + 1;
|
|
902
|
+
for (const c of collisions) {
|
|
903
|
+
const suggestedParsed = parseMigrationName(c.suggestedName);
|
|
904
|
+
if (suggestedParsed && suggestedParsed.timestamp >= nextSeq) {
|
|
905
|
+
nextSeq = suggestedParsed.timestamp + 1;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const description = file.replace(/^\d+_/, "").replace(/\.sql$/, "");
|
|
909
|
+
collisions.push({
|
|
910
|
+
existingName: journalEntry,
|
|
911
|
+
conflictingName: file,
|
|
912
|
+
sequenceNumber: parsed.timestamp,
|
|
913
|
+
suggestedName: formatMigrationFilename(nextSeq, description)
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return collisions;
|
|
918
|
+
}
|
|
919
|
+
// src/migration/snapshot.ts
|
|
920
|
+
function createSnapshot(tables) {
|
|
921
|
+
const snapshot = {
|
|
922
|
+
version: 1,
|
|
923
|
+
tables: {},
|
|
924
|
+
enums: {}
|
|
925
|
+
};
|
|
926
|
+
for (const table of tables) {
|
|
927
|
+
const columns = {};
|
|
928
|
+
const foreignKeys = [];
|
|
929
|
+
const indexes = [];
|
|
930
|
+
for (const [colName, col] of Object.entries(table._columns)) {
|
|
931
|
+
const meta = col._meta;
|
|
932
|
+
const colSnap = {
|
|
933
|
+
type: meta.sqlType,
|
|
934
|
+
nullable: meta.nullable,
|
|
935
|
+
primary: meta.primary,
|
|
936
|
+
unique: meta.unique
|
|
937
|
+
};
|
|
938
|
+
if (meta.hasDefault && meta.defaultValue !== undefined) {
|
|
939
|
+
const rawDefault = String(meta.defaultValue);
|
|
940
|
+
colSnap.default = rawDefault === "now" ? "now()" : rawDefault;
|
|
941
|
+
}
|
|
942
|
+
const annotationNames = meta._annotations ? Object.keys(meta._annotations) : [];
|
|
943
|
+
if (annotationNames.length > 0) {
|
|
944
|
+
colSnap.annotations = annotationNames;
|
|
945
|
+
}
|
|
946
|
+
columns[colName] = colSnap;
|
|
947
|
+
if (meta.references) {
|
|
948
|
+
foreignKeys.push({
|
|
949
|
+
column: colName,
|
|
950
|
+
targetTable: meta.references.table,
|
|
951
|
+
targetColumn: meta.references.column
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
if (meta.enumName && meta.enumValues) {
|
|
955
|
+
snapshot.enums[meta.enumName] = [...meta.enumValues];
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
for (const idx of table._indexes) {
|
|
959
|
+
indexes.push({ columns: [...idx.columns] });
|
|
960
|
+
}
|
|
961
|
+
snapshot.tables[table._name] = {
|
|
962
|
+
columns,
|
|
963
|
+
indexes,
|
|
964
|
+
foreignKeys,
|
|
965
|
+
_metadata: {}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
return snapshot;
|
|
969
|
+
}
|
|
970
|
+
// src/cli/baseline.ts
|
|
971
|
+
async function baseline(options) {
|
|
972
|
+
const runner = createMigrationRunner({ dialect: options.dialect });
|
|
973
|
+
const createResult = await runner.createHistoryTable(options.queryFn);
|
|
974
|
+
if (!createResult.ok) {
|
|
975
|
+
return createResult;
|
|
976
|
+
}
|
|
977
|
+
const appliedResult = await runner.getApplied(options.queryFn);
|
|
978
|
+
if (!appliedResult.ok) {
|
|
979
|
+
return appliedResult;
|
|
980
|
+
}
|
|
981
|
+
const appliedNames = new Set(appliedResult.data.map((a) => a.name));
|
|
982
|
+
const recorded = [];
|
|
983
|
+
for (const file of options.migrationFiles) {
|
|
984
|
+
if (appliedNames.has(file.name)) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const checksum = await computeChecksum(file.sql);
|
|
988
|
+
const dialect = options.dialect;
|
|
989
|
+
const param1 = dialect ? dialect.param(1) : "$1";
|
|
990
|
+
const param2 = dialect ? dialect.param(2) : "$2";
|
|
991
|
+
await options.queryFn(`INSERT INTO "_vertz_migrations" ("name", "checksum") VALUES (${param1}, ${param2})`, [file.name, checksum]);
|
|
992
|
+
recorded.push(file.name);
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
ok: true,
|
|
996
|
+
data: { recorded }
|
|
997
|
+
};
|
|
998
|
+
}
|
|
454
999
|
// src/cli/migrate-deploy.ts
|
|
455
1000
|
async function migrateDeploy(options) {
|
|
456
1001
|
const runner = createMigrationRunner();
|
|
457
1002
|
const isDryRun = options.dryRun ?? false;
|
|
458
1003
|
if (!isDryRun) {
|
|
459
|
-
await runner.createHistoryTable(options.queryFn);
|
|
1004
|
+
const createResult = await runner.createHistoryTable(options.queryFn);
|
|
1005
|
+
if (!createResult.ok) {
|
|
1006
|
+
return createResult;
|
|
1007
|
+
}
|
|
460
1008
|
}
|
|
461
1009
|
let applied;
|
|
462
1010
|
if (isDryRun) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
} catch {
|
|
1011
|
+
const appliedResult = await runner.getApplied(options.queryFn);
|
|
1012
|
+
if (!appliedResult.ok) {
|
|
466
1013
|
applied = [];
|
|
1014
|
+
} else {
|
|
1015
|
+
applied = appliedResult.data;
|
|
467
1016
|
}
|
|
468
1017
|
} else {
|
|
469
|
-
|
|
1018
|
+
const appliedResult = await runner.getApplied(options.queryFn);
|
|
1019
|
+
if (!appliedResult.ok) {
|
|
1020
|
+
return appliedResult;
|
|
1021
|
+
}
|
|
1022
|
+
applied = appliedResult.data;
|
|
470
1023
|
}
|
|
471
1024
|
const pending = runner.getPending(options.migrationFiles, applied);
|
|
472
1025
|
const appliedNames = [];
|
|
@@ -475,17 +1028,72 @@ async function migrateDeploy(options) {
|
|
|
475
1028
|
const result = await runner.apply(options.queryFn, migration.sql, migration.name, {
|
|
476
1029
|
dryRun: isDryRun
|
|
477
1030
|
});
|
|
1031
|
+
if (!result.ok) {
|
|
1032
|
+
return result;
|
|
1033
|
+
}
|
|
478
1034
|
appliedNames.push(migration.name);
|
|
479
|
-
migrationResults.push(result);
|
|
1035
|
+
migrationResults.push(result.data);
|
|
480
1036
|
}
|
|
481
1037
|
return {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
1038
|
+
ok: true,
|
|
1039
|
+
data: {
|
|
1040
|
+
applied: appliedNames,
|
|
1041
|
+
alreadyApplied: applied.map((a) => a.name),
|
|
1042
|
+
dryRun: isDryRun,
|
|
1043
|
+
migrations: migrationResults.length > 0 ? migrationResults : undefined
|
|
1044
|
+
}
|
|
486
1045
|
};
|
|
487
1046
|
}
|
|
488
1047
|
// src/cli/migrate-dev.ts
|
|
1048
|
+
function toKebab(str) {
|
|
1049
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1050
|
+
}
|
|
1051
|
+
function generateMigrationName(changes) {
|
|
1052
|
+
if (changes.length === 0)
|
|
1053
|
+
return "empty-migration";
|
|
1054
|
+
if (changes.length === 1) {
|
|
1055
|
+
const change = changes[0];
|
|
1056
|
+
switch (change.type) {
|
|
1057
|
+
case "table_added":
|
|
1058
|
+
return `add-${toKebab(change.table)}-table`;
|
|
1059
|
+
case "table_removed":
|
|
1060
|
+
return `drop-${toKebab(change.table)}-table`;
|
|
1061
|
+
case "column_added":
|
|
1062
|
+
return `add-${toKebab(change.column)}-to-${toKebab(change.table)}`;
|
|
1063
|
+
case "column_removed":
|
|
1064
|
+
return `drop-${toKebab(change.column)}-from-${toKebab(change.table)}`;
|
|
1065
|
+
case "column_altered":
|
|
1066
|
+
return `alter-${toKebab(change.column)}-in-${toKebab(change.table)}`;
|
|
1067
|
+
case "column_renamed":
|
|
1068
|
+
return `rename-${toKebab(change.oldColumn)}-to-${toKebab(change.newColumn)}-in-${toKebab(change.table)}`;
|
|
1069
|
+
case "index_added":
|
|
1070
|
+
return `add-index-to-${toKebab(change.table)}`;
|
|
1071
|
+
case "index_removed":
|
|
1072
|
+
return `drop-index-from-${toKebab(change.table)}`;
|
|
1073
|
+
case "enum_added":
|
|
1074
|
+
return `add-${toKebab(change.enumName)}-enum`;
|
|
1075
|
+
case "enum_removed":
|
|
1076
|
+
return `drop-${toKebab(change.enumName)}-enum`;
|
|
1077
|
+
case "enum_altered":
|
|
1078
|
+
return `alter-${toKebab(change.enumName)}-enum`;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const tables = new Set(changes.map((c) => c.table).filter(Boolean));
|
|
1082
|
+
const types = new Set(changes.map((c) => c.type));
|
|
1083
|
+
if (tables.size === 1 && types.size === 1) {
|
|
1084
|
+
const table = [...tables][0];
|
|
1085
|
+
const type = [...types][0];
|
|
1086
|
+
switch (type) {
|
|
1087
|
+
case "column_added":
|
|
1088
|
+
return `add-columns-to-${toKebab(table)}`;
|
|
1089
|
+
case "column_removed":
|
|
1090
|
+
return `drop-columns-from-${toKebab(table)}`;
|
|
1091
|
+
default:
|
|
1092
|
+
return `update-${toKebab(table)}`;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return "update-schema";
|
|
1096
|
+
}
|
|
489
1097
|
async function migrateDev(options) {
|
|
490
1098
|
const diff = computeDiff(options.previousSnapshot, options.currentSnapshot);
|
|
491
1099
|
const sql = generateMigrationSql(diff.changes, {
|
|
@@ -498,19 +1106,40 @@ async function migrateDev(options) {
|
|
|
498
1106
|
newColumn: c.newColumn,
|
|
499
1107
|
confidence: c.confidence
|
|
500
1108
|
}));
|
|
1109
|
+
const migrationName = options.migrationName ?? generateMigrationName(diff.changes);
|
|
501
1110
|
const num = nextMigrationNumber(options.existingFiles);
|
|
502
|
-
const filename = formatMigrationFilename(num,
|
|
1111
|
+
const filename = formatMigrationFilename(num, migrationName);
|
|
503
1112
|
const filePath = `${options.migrationsDir}/${filename}`;
|
|
1113
|
+
const journalPath = `${options.migrationsDir}/_journal.json`;
|
|
1114
|
+
let journal;
|
|
1115
|
+
try {
|
|
1116
|
+
const content = options.readFile ? await options.readFile(journalPath) : '{"version":1,"migrations":[]}';
|
|
1117
|
+
journal = JSON.parse(content);
|
|
1118
|
+
} catch {
|
|
1119
|
+
journal = createJournal();
|
|
1120
|
+
}
|
|
1121
|
+
const collisions = detectCollisions(journal, options.existingFiles);
|
|
504
1122
|
if (options.dryRun) {
|
|
505
1123
|
return {
|
|
506
1124
|
migrationFile: filename,
|
|
507
1125
|
sql,
|
|
508
1126
|
dryRun: true,
|
|
509
1127
|
renames: renames.length > 0 ? renames : undefined,
|
|
1128
|
+
collisions: collisions.length > 0 ? collisions : undefined,
|
|
510
1129
|
snapshot: options.currentSnapshot
|
|
511
1130
|
};
|
|
512
1131
|
}
|
|
513
1132
|
await options.writeFile(filePath, sql);
|
|
1133
|
+
const checksum = await computeChecksum(sql);
|
|
1134
|
+
journal = addJournalEntry(journal, {
|
|
1135
|
+
name: filename,
|
|
1136
|
+
description: migrationName,
|
|
1137
|
+
createdAt: new Date().toISOString(),
|
|
1138
|
+
checksum
|
|
1139
|
+
});
|
|
1140
|
+
await options.writeFile(journalPath, JSON.stringify(journal, null, 2));
|
|
1141
|
+
const snapshotPath = `${options.migrationsDir}/_snapshot.json`;
|
|
1142
|
+
await options.writeFile(snapshotPath, JSON.stringify(options.currentSnapshot, null, 2));
|
|
514
1143
|
const runner = createMigrationRunner();
|
|
515
1144
|
await runner.createHistoryTable(options.queryFn);
|
|
516
1145
|
await runner.apply(options.queryFn, sql, filename, { dryRun: false });
|
|
@@ -520,6 +1149,7 @@ async function migrateDev(options) {
|
|
|
520
1149
|
appliedAt: new Date,
|
|
521
1150
|
dryRun: false,
|
|
522
1151
|
renames: renames.length > 0 ? renames : undefined,
|
|
1152
|
+
collisions: collisions.length > 0 ? collisions : undefined,
|
|
523
1153
|
snapshot: options.currentSnapshot
|
|
524
1154
|
};
|
|
525
1155
|
}
|
|
@@ -538,21 +1168,419 @@ async function push(options) {
|
|
|
538
1168
|
];
|
|
539
1169
|
return { sql, tablesAffected };
|
|
540
1170
|
}
|
|
1171
|
+
// src/cli/reset.ts
|
|
1172
|
+
import { createMigrationQueryError as createMigrationQueryError2, err as err2 } from "@vertz/errors";
|
|
1173
|
+
var HISTORY_TABLE2 = "_vertz_migrations";
|
|
1174
|
+
function getUserTablesQuery(dialect) {
|
|
1175
|
+
if (dialect.name === "sqlite") {
|
|
1176
|
+
return `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;
|
|
1177
|
+
}
|
|
1178
|
+
return `SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'`;
|
|
1179
|
+
}
|
|
1180
|
+
function buildDropTableSql(tableName, dialect) {
|
|
1181
|
+
if (dialect.name === "postgres") {
|
|
1182
|
+
return `DROP TABLE IF EXISTS "${tableName}" CASCADE`;
|
|
1183
|
+
}
|
|
1184
|
+
return `DROP TABLE IF EXISTS "${tableName}"`;
|
|
1185
|
+
}
|
|
1186
|
+
async function reset(options) {
|
|
1187
|
+
const dialect = options.dialect ?? defaultPostgresDialect;
|
|
1188
|
+
const runner = createMigrationRunner({ dialect });
|
|
1189
|
+
let tableNames;
|
|
1190
|
+
try {
|
|
1191
|
+
const tablesResult = await options.queryFn(getUserTablesQuery(dialect), []);
|
|
1192
|
+
tableNames = tablesResult.rows.map((row) => row.name);
|
|
1193
|
+
} catch (cause) {
|
|
1194
|
+
return err2(createMigrationQueryError2("Failed to list user tables", { cause }));
|
|
1195
|
+
}
|
|
1196
|
+
const tablesDropped = [];
|
|
1197
|
+
for (const tableName of tableNames) {
|
|
1198
|
+
try {
|
|
1199
|
+
await options.queryFn(buildDropTableSql(tableName, dialect), []);
|
|
1200
|
+
if (tableName !== HISTORY_TABLE2) {
|
|
1201
|
+
tablesDropped.push(tableName);
|
|
1202
|
+
}
|
|
1203
|
+
} catch (cause) {
|
|
1204
|
+
return err2(createMigrationQueryError2(`Failed to drop table: ${tableName}`, { cause }));
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
try {
|
|
1208
|
+
await options.queryFn(buildDropTableSql(HISTORY_TABLE2, dialect), []);
|
|
1209
|
+
} catch (cause) {
|
|
1210
|
+
return err2(createMigrationQueryError2("Failed to drop history table", { cause }));
|
|
1211
|
+
}
|
|
1212
|
+
const createResult = await runner.createHistoryTable(options.queryFn);
|
|
1213
|
+
if (!createResult.ok) {
|
|
1214
|
+
return createResult;
|
|
1215
|
+
}
|
|
1216
|
+
const sorted = [...options.migrationFiles].sort((a, b) => a.timestamp - b.timestamp);
|
|
1217
|
+
const migrationsApplied = [];
|
|
1218
|
+
for (const file of sorted) {
|
|
1219
|
+
const applyResult = await runner.apply(options.queryFn, file.sql, file.name);
|
|
1220
|
+
if (!applyResult.ok) {
|
|
1221
|
+
return applyResult;
|
|
1222
|
+
}
|
|
1223
|
+
migrationsApplied.push(file.name);
|
|
1224
|
+
}
|
|
1225
|
+
return {
|
|
1226
|
+
ok: true,
|
|
1227
|
+
data: { tablesDropped, migrationsApplied }
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
541
1230
|
// src/cli/status.ts
|
|
1231
|
+
function diffChangeToCodeChange(change) {
|
|
1232
|
+
switch (change.type) {
|
|
1233
|
+
case "table_added":
|
|
1234
|
+
return {
|
|
1235
|
+
description: `Added table '${change.table}'`,
|
|
1236
|
+
type: change.type,
|
|
1237
|
+
table: change.table
|
|
1238
|
+
};
|
|
1239
|
+
case "table_removed":
|
|
1240
|
+
return {
|
|
1241
|
+
description: `Removed table '${change.table}'`,
|
|
1242
|
+
type: change.type,
|
|
1243
|
+
table: change.table
|
|
1244
|
+
};
|
|
1245
|
+
case "column_added":
|
|
1246
|
+
return {
|
|
1247
|
+
description: `Added column '${change.column}' to table '${change.table}'`,
|
|
1248
|
+
type: change.type,
|
|
1249
|
+
table: change.table,
|
|
1250
|
+
column: change.column
|
|
1251
|
+
};
|
|
1252
|
+
case "column_removed":
|
|
1253
|
+
return {
|
|
1254
|
+
description: `Removed column '${change.column}' from table '${change.table}'`,
|
|
1255
|
+
type: change.type,
|
|
1256
|
+
table: change.table,
|
|
1257
|
+
column: change.column
|
|
1258
|
+
};
|
|
1259
|
+
case "column_altered":
|
|
1260
|
+
return {
|
|
1261
|
+
description: `Altered column '${change.column}' in table '${change.table}'`,
|
|
1262
|
+
type: change.type,
|
|
1263
|
+
table: change.table,
|
|
1264
|
+
column: change.column
|
|
1265
|
+
};
|
|
1266
|
+
case "column_renamed":
|
|
1267
|
+
return {
|
|
1268
|
+
description: `Renamed column in table '${change.table}'`,
|
|
1269
|
+
type: change.type,
|
|
1270
|
+
table: change.table
|
|
1271
|
+
};
|
|
1272
|
+
case "index_added":
|
|
1273
|
+
return {
|
|
1274
|
+
description: `Added index on table '${change.table}'`,
|
|
1275
|
+
type: change.type,
|
|
1276
|
+
table: change.table
|
|
1277
|
+
};
|
|
1278
|
+
case "index_removed":
|
|
1279
|
+
return {
|
|
1280
|
+
description: `Removed index on table '${change.table}'`,
|
|
1281
|
+
type: change.type,
|
|
1282
|
+
table: change.table
|
|
1283
|
+
};
|
|
1284
|
+
case "enum_added":
|
|
1285
|
+
return {
|
|
1286
|
+
description: `Added enum type`,
|
|
1287
|
+
type: change.type
|
|
1288
|
+
};
|
|
1289
|
+
case "enum_removed":
|
|
1290
|
+
return {
|
|
1291
|
+
description: `Removed enum type`,
|
|
1292
|
+
type: change.type
|
|
1293
|
+
};
|
|
1294
|
+
case "enum_altered":
|
|
1295
|
+
return {
|
|
1296
|
+
description: `Altered enum type`,
|
|
1297
|
+
type: change.type
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
function detectSchemaDrift(expected, actual) {
|
|
1302
|
+
const drift = [];
|
|
1303
|
+
for (const tableName of Object.keys(actual.tables)) {
|
|
1304
|
+
if (!(tableName in expected.tables)) {
|
|
1305
|
+
drift.push({
|
|
1306
|
+
description: `Table '${tableName}' exists in database but not in schema`,
|
|
1307
|
+
type: "extra_table",
|
|
1308
|
+
table: tableName
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
for (const tableName of Object.keys(expected.tables)) {
|
|
1313
|
+
if (!(tableName in actual.tables)) {
|
|
1314
|
+
drift.push({
|
|
1315
|
+
description: `Table '${tableName}' exists in schema but not in database`,
|
|
1316
|
+
type: "missing_table",
|
|
1317
|
+
table: tableName
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
for (const tableName of Object.keys(expected.tables)) {
|
|
1322
|
+
if (!(tableName in actual.tables))
|
|
1323
|
+
continue;
|
|
1324
|
+
const expectedTable = expected.tables[tableName];
|
|
1325
|
+
const actualTable = actual.tables[tableName];
|
|
1326
|
+
if (!expectedTable || !actualTable)
|
|
1327
|
+
continue;
|
|
1328
|
+
for (const colName of Object.keys(actualTable.columns)) {
|
|
1329
|
+
if (!(colName in expectedTable.columns)) {
|
|
1330
|
+
drift.push({
|
|
1331
|
+
description: `Column '${colName}' exists in database table '${tableName}' but not in schema`,
|
|
1332
|
+
type: "extra_column",
|
|
1333
|
+
table: tableName,
|
|
1334
|
+
column: colName
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
for (const colName of Object.keys(expectedTable.columns)) {
|
|
1339
|
+
if (!(colName in actualTable.columns)) {
|
|
1340
|
+
drift.push({
|
|
1341
|
+
description: `Column '${colName}' exists in schema table '${tableName}' but not in database`,
|
|
1342
|
+
type: "missing_column",
|
|
1343
|
+
table: tableName,
|
|
1344
|
+
column: colName
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
for (const colName of Object.keys(expectedTable.columns)) {
|
|
1349
|
+
if (!(colName in actualTable.columns))
|
|
1350
|
+
continue;
|
|
1351
|
+
const expectedCol = expectedTable.columns[colName];
|
|
1352
|
+
const actualCol = actualTable.columns[colName];
|
|
1353
|
+
if (!expectedCol || !actualCol)
|
|
1354
|
+
continue;
|
|
1355
|
+
if (expectedCol.type !== actualCol.type) {
|
|
1356
|
+
drift.push({
|
|
1357
|
+
description: `Column '${colName}' in table '${tableName}' has type '${actualCol.type}' in database but '${expectedCol.type}' in schema`,
|
|
1358
|
+
type: "column_type_mismatch",
|
|
1359
|
+
table: tableName,
|
|
1360
|
+
column: colName
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return drift;
|
|
1366
|
+
}
|
|
542
1367
|
async function migrateStatus(options) {
|
|
543
1368
|
const runner = createMigrationRunner();
|
|
544
|
-
await runner.createHistoryTable(options.queryFn);
|
|
545
|
-
|
|
1369
|
+
const createResult = await runner.createHistoryTable(options.queryFn);
|
|
1370
|
+
if (!createResult.ok) {
|
|
1371
|
+
return createResult;
|
|
1372
|
+
}
|
|
1373
|
+
const appliedResult = await runner.getApplied(options.queryFn);
|
|
1374
|
+
if (!appliedResult.ok) {
|
|
1375
|
+
return appliedResult;
|
|
1376
|
+
}
|
|
1377
|
+
const applied = appliedResult.data;
|
|
546
1378
|
const pending = runner.getPending(options.migrationFiles, applied);
|
|
1379
|
+
let codeChanges = [];
|
|
1380
|
+
if (options.savedSnapshot && options.currentSnapshot) {
|
|
1381
|
+
const diff = computeDiff(options.savedSnapshot, options.currentSnapshot);
|
|
1382
|
+
codeChanges = diff.changes.map(diffChangeToCodeChange);
|
|
1383
|
+
}
|
|
1384
|
+
let drift = [];
|
|
1385
|
+
if (options.dialect) {
|
|
1386
|
+
const introspect = options.dialect.name === "sqlite" ? introspectSqlite : introspectPostgres;
|
|
1387
|
+
const actualSchema = await introspect(options.queryFn);
|
|
1388
|
+
const expectedSchema = options.savedSnapshot ?? options.currentSnapshot;
|
|
1389
|
+
if (expectedSchema) {
|
|
1390
|
+
drift = detectSchemaDrift(expectedSchema, actualSchema);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
547
1393
|
return {
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1394
|
+
ok: true,
|
|
1395
|
+
data: {
|
|
1396
|
+
applied: applied.map((a) => ({
|
|
1397
|
+
name: a.name,
|
|
1398
|
+
checksum: a.checksum,
|
|
1399
|
+
appliedAt: a.appliedAt
|
|
1400
|
+
})),
|
|
1401
|
+
pending: pending.map((p) => p.name),
|
|
1402
|
+
codeChanges,
|
|
1403
|
+
drift
|
|
1404
|
+
}
|
|
554
1405
|
};
|
|
555
1406
|
}
|
|
1407
|
+
// src/client/database.ts
|
|
1408
|
+
import { err as err3, ok as ok2 } from "@vertz/schema";
|
|
1409
|
+
// src/errors/error-codes.ts
|
|
1410
|
+
var DbErrorCode = {
|
|
1411
|
+
UNIQUE_VIOLATION: "23505",
|
|
1412
|
+
FOREIGN_KEY_VIOLATION: "23503",
|
|
1413
|
+
NOT_NULL_VIOLATION: "23502",
|
|
1414
|
+
CHECK_VIOLATION: "23514",
|
|
1415
|
+
EXCLUSION_VIOLATION: "23P01",
|
|
1416
|
+
SERIALIZATION_FAILURE: "40001",
|
|
1417
|
+
DEADLOCK_DETECTED: "40P01",
|
|
1418
|
+
CONNECTION_EXCEPTION: "08000",
|
|
1419
|
+
CONNECTION_DOES_NOT_EXIST: "08003",
|
|
1420
|
+
CONNECTION_FAILURE: "08006",
|
|
1421
|
+
NotFound: "NotFound",
|
|
1422
|
+
CONNECTION_ERROR: "CONNECTION_ERROR",
|
|
1423
|
+
POOL_EXHAUSTED: "POOL_EXHAUSTED"
|
|
1424
|
+
};
|
|
1425
|
+
var PgCodeToName = Object.fromEntries(Object.entries(DbErrorCode).map(([name, pgCode]) => [pgCode, name]));
|
|
1426
|
+
function resolveErrorCode(pgCode) {
|
|
1427
|
+
return PgCodeToName[pgCode];
|
|
1428
|
+
}
|
|
1429
|
+
// src/errors/http-adapter.ts
|
|
1430
|
+
function dbErrorToHttpError(error) {
|
|
1431
|
+
const body = error.toJSON();
|
|
1432
|
+
if (error instanceof UniqueConstraintError) {
|
|
1433
|
+
return { status: 409, body };
|
|
1434
|
+
}
|
|
1435
|
+
if (error instanceof NotFoundError) {
|
|
1436
|
+
return { status: 404, body };
|
|
1437
|
+
}
|
|
1438
|
+
if (error instanceof ForeignKeyError) {
|
|
1439
|
+
return { status: 422, body };
|
|
1440
|
+
}
|
|
1441
|
+
if (error instanceof NotNullError) {
|
|
1442
|
+
return { status: 422, body };
|
|
1443
|
+
}
|
|
1444
|
+
if (error instanceof CheckConstraintError) {
|
|
1445
|
+
return { status: 422, body };
|
|
1446
|
+
}
|
|
1447
|
+
if (error instanceof ConnectionError) {
|
|
1448
|
+
return { status: 503, body };
|
|
1449
|
+
}
|
|
1450
|
+
return { status: 500, body };
|
|
1451
|
+
}
|
|
1452
|
+
// src/errors.ts
|
|
1453
|
+
function toReadError(error, query) {
|
|
1454
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
1455
|
+
const errWithCode = error;
|
|
1456
|
+
if (errWithCode.code === "NotFound") {
|
|
1457
|
+
return {
|
|
1458
|
+
code: "NotFound",
|
|
1459
|
+
message: errWithCode.message,
|
|
1460
|
+
table: errWithCode.table ?? "unknown",
|
|
1461
|
+
cause: error
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
if (errWithCode.code.startsWith("08")) {
|
|
1465
|
+
return {
|
|
1466
|
+
code: "CONNECTION_ERROR",
|
|
1467
|
+
message: errWithCode.message,
|
|
1468
|
+
cause: error
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
return {
|
|
1472
|
+
code: "QUERY_ERROR",
|
|
1473
|
+
message: errWithCode.message,
|
|
1474
|
+
sql: query,
|
|
1475
|
+
cause: error
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
if (error instanceof Error) {
|
|
1479
|
+
const message2 = error.message.toLowerCase();
|
|
1480
|
+
if (message2.includes("connection") || message2.includes("ECONNREFUSED") || message2.includes("timeout")) {
|
|
1481
|
+
return {
|
|
1482
|
+
code: "CONNECTION_ERROR",
|
|
1483
|
+
message: error.message,
|
|
1484
|
+
cause: error
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1489
|
+
return {
|
|
1490
|
+
code: "QUERY_ERROR",
|
|
1491
|
+
message,
|
|
1492
|
+
sql: query,
|
|
1493
|
+
cause: error
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
function toWriteError(error, query) {
|
|
1497
|
+
if (error instanceof UniqueConstraintError) {
|
|
1498
|
+
return {
|
|
1499
|
+
code: "CONSTRAINT_ERROR",
|
|
1500
|
+
message: error.message,
|
|
1501
|
+
column: error.column,
|
|
1502
|
+
table: error.table,
|
|
1503
|
+
cause: error
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
if (error instanceof ForeignKeyError) {
|
|
1507
|
+
return {
|
|
1508
|
+
code: "CONSTRAINT_ERROR",
|
|
1509
|
+
message: error.message,
|
|
1510
|
+
constraint: error.constraint,
|
|
1511
|
+
table: error.table,
|
|
1512
|
+
cause: error
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
if (error instanceof NotNullError) {
|
|
1516
|
+
return {
|
|
1517
|
+
code: "CONSTRAINT_ERROR",
|
|
1518
|
+
message: error.message,
|
|
1519
|
+
column: error.column,
|
|
1520
|
+
table: error.table,
|
|
1521
|
+
cause: error
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
if (error instanceof CheckConstraintError) {
|
|
1525
|
+
return {
|
|
1526
|
+
code: "CONSTRAINT_ERROR",
|
|
1527
|
+
message: error.message,
|
|
1528
|
+
constraint: error.constraint,
|
|
1529
|
+
table: error.table,
|
|
1530
|
+
cause: error
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
if (error instanceof ConnectionError) {
|
|
1534
|
+
return {
|
|
1535
|
+
code: "CONNECTION_ERROR",
|
|
1536
|
+
message: error.message,
|
|
1537
|
+
cause: error
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
if (typeof error === "object" && error !== null && "code" in error) {
|
|
1541
|
+
const pgError = error;
|
|
1542
|
+
if (pgError.code.startsWith("08")) {
|
|
1543
|
+
return {
|
|
1544
|
+
code: "CONNECTION_ERROR",
|
|
1545
|
+
message: pgError.message,
|
|
1546
|
+
cause: error
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
if (pgError.code === "23505" || pgError.code === "23503" || pgError.code === "23502" || pgError.code === "23514") {
|
|
1550
|
+
if (pgError.code === "23505" || pgError.code === "23502") {
|
|
1551
|
+
return {
|
|
1552
|
+
code: "CONSTRAINT_ERROR",
|
|
1553
|
+
message: pgError.message,
|
|
1554
|
+
table: pgError.table,
|
|
1555
|
+
column: pgError.column,
|
|
1556
|
+
cause: error
|
|
1557
|
+
};
|
|
1558
|
+
} else {
|
|
1559
|
+
return {
|
|
1560
|
+
code: "CONSTRAINT_ERROR",
|
|
1561
|
+
message: pgError.message,
|
|
1562
|
+
table: pgError.table,
|
|
1563
|
+
constraint: pgError.constraint,
|
|
1564
|
+
cause: error
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
return {
|
|
1569
|
+
code: "QUERY_ERROR",
|
|
1570
|
+
message: pgError.message,
|
|
1571
|
+
sql: query,
|
|
1572
|
+
cause: error
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1576
|
+
return {
|
|
1577
|
+
code: "QUERY_ERROR",
|
|
1578
|
+
message,
|
|
1579
|
+
sql: query,
|
|
1580
|
+
cause: error
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
|
|
556
1584
|
// src/query/aggregate.ts
|
|
557
1585
|
async function count(queryFn, table, options) {
|
|
558
1586
|
const allParams = [];
|
|
@@ -779,19 +1807,33 @@ async function groupBy(queryFn, table, options) {
|
|
|
779
1807
|
}
|
|
780
1808
|
|
|
781
1809
|
// src/query/crud.ts
|
|
1810
|
+
function fillGeneratedIds(table, data) {
|
|
1811
|
+
const filled = { ...data };
|
|
1812
|
+
for (const [name, col] of Object.entries(table._columns)) {
|
|
1813
|
+
const meta = col._meta;
|
|
1814
|
+
if (meta.generate && filled[name] === undefined) {
|
|
1815
|
+
if (meta.sqlType === "integer" || meta.sqlType === "serial" || meta.sqlType === "bigint") {
|
|
1816
|
+
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).`);
|
|
1817
|
+
}
|
|
1818
|
+
filled[name] = generateId(meta.generate);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return filled;
|
|
1822
|
+
}
|
|
782
1823
|
function assertNonEmptyWhere(where, operation) {
|
|
783
1824
|
if (Object.keys(where).length === 0) {
|
|
784
1825
|
throw new Error(`${operation} requires a non-empty where clause. ` + "Passing an empty where object would affect all rows.");
|
|
785
1826
|
}
|
|
786
1827
|
}
|
|
787
|
-
async function get(queryFn, table, options) {
|
|
1828
|
+
async function get(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
788
1829
|
const columns = resolveSelectColumns(table, options?.select);
|
|
789
1830
|
const result = buildSelect({
|
|
790
1831
|
table: table._name,
|
|
791
1832
|
columns,
|
|
792
1833
|
where: options?.where,
|
|
793
1834
|
orderBy: options?.orderBy,
|
|
794
|
-
limit: 1
|
|
1835
|
+
limit: 1,
|
|
1836
|
+
dialect
|
|
795
1837
|
});
|
|
796
1838
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
797
1839
|
if (res.rows.length === 0) {
|
|
@@ -799,14 +1841,7 @@ async function get(queryFn, table, options) {
|
|
|
799
1841
|
}
|
|
800
1842
|
return mapRow(res.rows[0]);
|
|
801
1843
|
}
|
|
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) {
|
|
1844
|
+
async function list(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
810
1845
|
const columns = resolveSelectColumns(table, options?.select);
|
|
811
1846
|
const result = buildSelect({
|
|
812
1847
|
table: table._name,
|
|
@@ -816,12 +1851,13 @@ async function list(queryFn, table, options) {
|
|
|
816
1851
|
limit: options?.limit,
|
|
817
1852
|
offset: options?.offset,
|
|
818
1853
|
cursor: options?.cursor,
|
|
819
|
-
take: options?.take
|
|
1854
|
+
take: options?.take,
|
|
1855
|
+
dialect
|
|
820
1856
|
});
|
|
821
1857
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
822
1858
|
return mapRows(res.rows);
|
|
823
1859
|
}
|
|
824
|
-
async function listAndCount(queryFn, table, options) {
|
|
1860
|
+
async function listAndCount(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
825
1861
|
const columns = resolveSelectColumns(table, options?.select);
|
|
826
1862
|
const result = buildSelect({
|
|
827
1863
|
table: table._name,
|
|
@@ -832,7 +1868,8 @@ async function listAndCount(queryFn, table, options) {
|
|
|
832
1868
|
offset: options?.offset,
|
|
833
1869
|
cursor: options?.cursor,
|
|
834
1870
|
take: options?.take,
|
|
835
|
-
withCount: true
|
|
1871
|
+
withCount: true,
|
|
1872
|
+
dialect
|
|
836
1873
|
});
|
|
837
1874
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
838
1875
|
const rows = res.rows;
|
|
@@ -847,55 +1884,97 @@ async function listAndCount(queryFn, table, options) {
|
|
|
847
1884
|
});
|
|
848
1885
|
return { data, total };
|
|
849
1886
|
}
|
|
850
|
-
async function create(queryFn, table, options) {
|
|
1887
|
+
async function create(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
851
1888
|
const returningColumns = resolveSelectColumns(table, options.select);
|
|
852
1889
|
const nowColumns = getTimestampColumns(table);
|
|
1890
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
1891
|
+
const withIds = fillGeneratedIds(table, options.data);
|
|
1892
|
+
const filteredData = Object.fromEntries(Object.entries(withIds).filter(([key]) => {
|
|
1893
|
+
const col = table._columns[key];
|
|
1894
|
+
const meta = col ? col._meta : undefined;
|
|
1895
|
+
if (meta?.generate)
|
|
1896
|
+
return true;
|
|
1897
|
+
return !readOnlyCols.includes(key);
|
|
1898
|
+
}));
|
|
853
1899
|
const result = buildInsert({
|
|
854
1900
|
table: table._name,
|
|
855
|
-
data:
|
|
1901
|
+
data: filteredData,
|
|
856
1902
|
returning: returningColumns,
|
|
857
|
-
nowColumns
|
|
1903
|
+
nowColumns,
|
|
1904
|
+
dialect
|
|
858
1905
|
});
|
|
859
1906
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
860
1907
|
return mapRow(res.rows[0]);
|
|
861
1908
|
}
|
|
862
|
-
async function createMany(queryFn, table, options) {
|
|
1909
|
+
async function createMany(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
863
1910
|
if (options.data.length === 0) {
|
|
864
1911
|
return { count: 0 };
|
|
865
1912
|
}
|
|
866
1913
|
const nowColumns = getTimestampColumns(table);
|
|
1914
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
1915
|
+
const filteredData = options.data.map((row) => {
|
|
1916
|
+
const withIds = fillGeneratedIds(table, row);
|
|
1917
|
+
return Object.fromEntries(Object.entries(withIds).filter(([key]) => {
|
|
1918
|
+
const col = table._columns[key];
|
|
1919
|
+
const meta = col ? col._meta : undefined;
|
|
1920
|
+
if (meta?.generate)
|
|
1921
|
+
return true;
|
|
1922
|
+
return !readOnlyCols.includes(key);
|
|
1923
|
+
}));
|
|
1924
|
+
});
|
|
867
1925
|
const result = buildInsert({
|
|
868
1926
|
table: table._name,
|
|
869
|
-
data:
|
|
870
|
-
nowColumns
|
|
1927
|
+
data: filteredData,
|
|
1928
|
+
nowColumns,
|
|
1929
|
+
dialect
|
|
871
1930
|
});
|
|
872
1931
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
873
1932
|
return { count: res.rowCount };
|
|
874
1933
|
}
|
|
875
|
-
async function createManyAndReturn(queryFn, table, options) {
|
|
1934
|
+
async function createManyAndReturn(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
876
1935
|
if (options.data.length === 0) {
|
|
877
1936
|
return [];
|
|
878
1937
|
}
|
|
879
1938
|
const returningColumns = resolveSelectColumns(table, options.select);
|
|
880
1939
|
const nowColumns = getTimestampColumns(table);
|
|
1940
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
1941
|
+
const filteredData = options.data.map((row) => {
|
|
1942
|
+
const withIds = fillGeneratedIds(table, row);
|
|
1943
|
+
return Object.fromEntries(Object.entries(withIds).filter(([key]) => {
|
|
1944
|
+
const col = table._columns[key];
|
|
1945
|
+
const meta = col ? col._meta : undefined;
|
|
1946
|
+
if (meta?.generate)
|
|
1947
|
+
return true;
|
|
1948
|
+
return !readOnlyCols.includes(key);
|
|
1949
|
+
}));
|
|
1950
|
+
});
|
|
881
1951
|
const result = buildInsert({
|
|
882
1952
|
table: table._name,
|
|
883
|
-
data:
|
|
1953
|
+
data: filteredData,
|
|
884
1954
|
returning: returningColumns,
|
|
885
|
-
nowColumns
|
|
1955
|
+
nowColumns,
|
|
1956
|
+
dialect
|
|
886
1957
|
});
|
|
887
1958
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
888
1959
|
return mapRows(res.rows);
|
|
889
1960
|
}
|
|
890
|
-
async function update(queryFn, table, options) {
|
|
1961
|
+
async function update(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
891
1962
|
const returningColumns = resolveSelectColumns(table, options.select);
|
|
892
1963
|
const nowColumns = getTimestampColumns(table);
|
|
1964
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
1965
|
+
const autoUpdateCols = getAutoUpdateColumns(table);
|
|
1966
|
+
const filteredData = Object.fromEntries(Object.entries(options.data).filter(([key]) => !readOnlyCols.includes(key)));
|
|
1967
|
+
for (const col of autoUpdateCols) {
|
|
1968
|
+
filteredData[col] = "now";
|
|
1969
|
+
}
|
|
1970
|
+
const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
|
|
893
1971
|
const result = buildUpdate({
|
|
894
1972
|
table: table._name,
|
|
895
|
-
data:
|
|
1973
|
+
data: filteredData,
|
|
896
1974
|
where: options.where,
|
|
897
1975
|
returning: returningColumns,
|
|
898
|
-
nowColumns
|
|
1976
|
+
nowColumns: allNowColumns,
|
|
1977
|
+
dialect
|
|
899
1978
|
});
|
|
900
1979
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
901
1980
|
if (res.rows.length === 0) {
|
|
@@ -903,44 +1982,69 @@ async function update(queryFn, table, options) {
|
|
|
903
1982
|
}
|
|
904
1983
|
return mapRow(res.rows[0]);
|
|
905
1984
|
}
|
|
906
|
-
async function updateMany(queryFn, table, options) {
|
|
1985
|
+
async function updateMany(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
907
1986
|
assertNonEmptyWhere(options.where, "updateMany");
|
|
908
1987
|
const nowColumns = getTimestampColumns(table);
|
|
1988
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
1989
|
+
const autoUpdateCols = getAutoUpdateColumns(table);
|
|
1990
|
+
const filteredData = Object.fromEntries(Object.entries(options.data).filter(([key]) => !readOnlyCols.includes(key)));
|
|
1991
|
+
for (const col of autoUpdateCols) {
|
|
1992
|
+
filteredData[col] = "now";
|
|
1993
|
+
}
|
|
1994
|
+
const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
|
|
909
1995
|
const result = buildUpdate({
|
|
910
1996
|
table: table._name,
|
|
911
|
-
data:
|
|
1997
|
+
data: filteredData,
|
|
912
1998
|
where: options.where,
|
|
913
|
-
nowColumns
|
|
1999
|
+
nowColumns: allNowColumns,
|
|
2000
|
+
dialect
|
|
914
2001
|
});
|
|
915
2002
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
916
2003
|
return { count: res.rowCount };
|
|
917
2004
|
}
|
|
918
|
-
async function upsert(queryFn, table, options) {
|
|
2005
|
+
async function upsert(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
919
2006
|
const returningColumns = resolveSelectColumns(table, options.select);
|
|
920
2007
|
const nowColumns = getTimestampColumns(table);
|
|
2008
|
+
const readOnlyCols = getReadOnlyColumns(table);
|
|
2009
|
+
const autoUpdateCols = getAutoUpdateColumns(table);
|
|
921
2010
|
const conflictColumns = Object.keys(options.where);
|
|
922
|
-
const
|
|
2011
|
+
const createWithIds = fillGeneratedIds(table, options.create);
|
|
2012
|
+
const filteredCreate = Object.fromEntries(Object.entries(createWithIds).filter(([key]) => {
|
|
2013
|
+
const col = table._columns[key];
|
|
2014
|
+
const meta = col ? col._meta : undefined;
|
|
2015
|
+
if (meta?.generate)
|
|
2016
|
+
return true;
|
|
2017
|
+
return !readOnlyCols.includes(key);
|
|
2018
|
+
}));
|
|
2019
|
+
const filteredUpdate = Object.fromEntries(Object.entries(options.update).filter(([key]) => !readOnlyCols.includes(key)));
|
|
2020
|
+
for (const col of autoUpdateCols) {
|
|
2021
|
+
filteredUpdate[col] = "now";
|
|
2022
|
+
}
|
|
2023
|
+
const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
|
|
2024
|
+
const updateColumns = Object.keys(filteredUpdate);
|
|
923
2025
|
const result = buildInsert({
|
|
924
2026
|
table: table._name,
|
|
925
|
-
data:
|
|
2027
|
+
data: filteredCreate,
|
|
926
2028
|
returning: returningColumns,
|
|
927
|
-
nowColumns,
|
|
2029
|
+
nowColumns: allNowColumns,
|
|
928
2030
|
onConflict: {
|
|
929
2031
|
columns: conflictColumns,
|
|
930
2032
|
action: "update",
|
|
931
2033
|
updateColumns,
|
|
932
|
-
updateValues:
|
|
933
|
-
}
|
|
2034
|
+
updateValues: filteredUpdate
|
|
2035
|
+
},
|
|
2036
|
+
dialect
|
|
934
2037
|
});
|
|
935
2038
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
936
2039
|
return mapRow(res.rows[0]);
|
|
937
2040
|
}
|
|
938
|
-
async function deleteOne(queryFn, table, options) {
|
|
2041
|
+
async function deleteOne(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
939
2042
|
const returningColumns = resolveSelectColumns(table, options.select);
|
|
940
2043
|
const result = buildDelete({
|
|
941
2044
|
table: table._name,
|
|
942
2045
|
where: options.where,
|
|
943
|
-
returning: returningColumns
|
|
2046
|
+
returning: returningColumns,
|
|
2047
|
+
dialect
|
|
944
2048
|
});
|
|
945
2049
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
946
2050
|
if (res.rows.length === 0) {
|
|
@@ -948,11 +2052,12 @@ async function deleteOne(queryFn, table, options) {
|
|
|
948
2052
|
}
|
|
949
2053
|
return mapRow(res.rows[0]);
|
|
950
2054
|
}
|
|
951
|
-
async function deleteMany(queryFn, table, options) {
|
|
2055
|
+
async function deleteMany(queryFn, table, options, dialect = defaultPostgresDialect) {
|
|
952
2056
|
assertNonEmptyWhere(options.where, "deleteMany");
|
|
953
2057
|
const result = buildDelete({
|
|
954
2058
|
table: table._name,
|
|
955
|
-
where: options.where
|
|
2059
|
+
where: options.where,
|
|
2060
|
+
dialect
|
|
956
2061
|
});
|
|
957
2062
|
const res = await executeQuery(queryFn, result.sql, result.params);
|
|
958
2063
|
return { count: res.rowCount };
|
|
@@ -1187,60 +2292,95 @@ async function loadManyToManyRelation(queryFn, primaryRows, def, target, relName
|
|
|
1187
2292
|
}
|
|
1188
2293
|
}
|
|
1189
2294
|
|
|
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;
|
|
2295
|
+
// src/client/sqlite-value-converter.ts
|
|
2296
|
+
function fromSqliteValue(value, columnType) {
|
|
2297
|
+
if (columnType === "boolean") {
|
|
2298
|
+
if (value === 1) {
|
|
2299
|
+
return true;
|
|
2300
|
+
}
|
|
2301
|
+
if (value === 0) {
|
|
2302
|
+
return false;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
if ((columnType === "timestamp" || columnType === "timestamp with time zone") && typeof value === "string") {
|
|
2306
|
+
return new Date(value);
|
|
1206
2307
|
}
|
|
1207
|
-
|
|
2308
|
+
return value;
|
|
1208
2309
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
2310
|
+
|
|
2311
|
+
// src/client/sqlite-driver.ts
|
|
2312
|
+
function buildTableSchema(models) {
|
|
2313
|
+
const registry = new Map;
|
|
2314
|
+
for (const [, entry] of Object.entries(models)) {
|
|
2315
|
+
const tableName = entry.table._name;
|
|
2316
|
+
const columnTypes = {};
|
|
2317
|
+
for (const [colName, colBuilder] of Object.entries(entry.table._columns)) {
|
|
2318
|
+
const meta = colBuilder._meta;
|
|
2319
|
+
columnTypes[colName] = meta.sqlType;
|
|
2320
|
+
}
|
|
2321
|
+
registry.set(tableName, columnTypes);
|
|
2322
|
+
}
|
|
2323
|
+
return registry;
|
|
2324
|
+
}
|
|
2325
|
+
function extractTableName(sql) {
|
|
2326
|
+
const fromMatch = sql.match(/\bFROM\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
|
|
2327
|
+
if (fromMatch) {
|
|
2328
|
+
return fromMatch[1].toLowerCase();
|
|
2329
|
+
}
|
|
2330
|
+
const insertMatch = sql.match(/\bINSERT\s+INTO\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
|
|
2331
|
+
if (insertMatch) {
|
|
2332
|
+
return insertMatch[1].toLowerCase();
|
|
2333
|
+
}
|
|
2334
|
+
const updateMatch = sql.match(/\bUPDATE\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
|
|
2335
|
+
if (updateMatch) {
|
|
2336
|
+
return updateMatch[1].toLowerCase();
|
|
2337
|
+
}
|
|
2338
|
+
const deleteMatch = sql.match(/\bDELETE\s+FROM\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
|
|
2339
|
+
if (deleteMatch) {
|
|
2340
|
+
return deleteMatch[1].toLowerCase();
|
|
2341
|
+
}
|
|
2342
|
+
return null;
|
|
2343
|
+
}
|
|
2344
|
+
function createSqliteDriver(d1, tableSchema) {
|
|
2345
|
+
const query = async (sql, params) => {
|
|
2346
|
+
const prepared = d1.prepare(sql);
|
|
2347
|
+
const bound = params ? prepared.bind(...params) : prepared;
|
|
2348
|
+
const result = await bound.all();
|
|
2349
|
+
if (tableSchema && result.results.length > 0) {
|
|
2350
|
+
const tableName = extractTableName(sql);
|
|
2351
|
+
if (tableName) {
|
|
2352
|
+
const schema = tableSchema.get(tableName);
|
|
2353
|
+
if (schema) {
|
|
2354
|
+
return result.results.map((row) => {
|
|
2355
|
+
const convertedRow = {};
|
|
2356
|
+
for (const [key, value] of Object.entries(row)) {
|
|
2357
|
+
const columnType = schema[key];
|
|
2358
|
+
if (columnType) {
|
|
2359
|
+
convertedRow[key] = fromSqliteValue(value, columnType);
|
|
2360
|
+
} else {
|
|
2361
|
+
convertedRow[key] = value;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
return convertedRow;
|
|
2365
|
+
});
|
|
1223
2366
|
}
|
|
1224
|
-
|
|
1225
|
-
});
|
|
1226
|
-
return {
|
|
1227
|
-
rows,
|
|
1228
|
-
rowCount: result.count ?? rows.length
|
|
1229
|
-
};
|
|
1230
|
-
} catch (error) {
|
|
1231
|
-
adaptPostgresError(error);
|
|
2367
|
+
}
|
|
1232
2368
|
}
|
|
2369
|
+
return result.results;
|
|
2370
|
+
};
|
|
2371
|
+
const execute = async (sql, params) => {
|
|
2372
|
+
const prepared = d1.prepare(sql);
|
|
2373
|
+
const bound = params ? prepared.bind(...params) : prepared;
|
|
2374
|
+
const result = await bound.run();
|
|
2375
|
+
return { rowsAffected: result.meta.changes };
|
|
1233
2376
|
};
|
|
1234
2377
|
return {
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
async isHealthy() {
|
|
2378
|
+
query,
|
|
2379
|
+
execute,
|
|
2380
|
+
close: async () => {},
|
|
2381
|
+
isHealthy: async () => {
|
|
1240
2382
|
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]);
|
|
2383
|
+
await query("SELECT 1");
|
|
1244
2384
|
return true;
|
|
1245
2385
|
} catch {
|
|
1246
2386
|
return false;
|
|
@@ -1248,18 +2388,6 @@ function createPostgresDriver(url, pool) {
|
|
|
1248
2388
|
}
|
|
1249
2389
|
};
|
|
1250
2390
|
}
|
|
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
2391
|
|
|
1264
2392
|
// src/client/tenant-graph.ts
|
|
1265
2393
|
function computeTenantGraph(registry) {
|
|
@@ -1379,16 +2507,31 @@ function isReadQuery(sqlStr) {
|
|
|
1379
2507
|
}
|
|
1380
2508
|
return upper.startsWith("SELECT");
|
|
1381
2509
|
}
|
|
1382
|
-
function
|
|
1383
|
-
const entry =
|
|
2510
|
+
function resolveModel(models, name) {
|
|
2511
|
+
const entry = models[name];
|
|
1384
2512
|
if (!entry) {
|
|
1385
2513
|
throw new Error(`Table "${name}" is not registered in the database.`);
|
|
1386
2514
|
}
|
|
1387
2515
|
return entry;
|
|
1388
2516
|
}
|
|
2517
|
+
var RESERVED_MODEL_NAMES = new Set(["query", "close", "isHealthy", "_internals"]);
|
|
1389
2518
|
function createDb(options) {
|
|
1390
|
-
const {
|
|
1391
|
-
const
|
|
2519
|
+
const { models, log, dialect } = options;
|
|
2520
|
+
for (const key of Object.keys(models)) {
|
|
2521
|
+
if (RESERVED_MODEL_NAMES.has(key)) {
|
|
2522
|
+
throw new Error(`Model name "${key}" is reserved. Choose a different name for this model. ` + `Reserved names: ${[...RESERVED_MODEL_NAMES].join(", ")}`);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
if (dialect === "sqlite") {
|
|
2526
|
+
if (!options.d1) {
|
|
2527
|
+
throw new Error("SQLite dialect requires a D1 binding");
|
|
2528
|
+
}
|
|
2529
|
+
if (options.url) {
|
|
2530
|
+
throw new Error("SQLite dialect uses D1, not a connection URL");
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
const dialectObj = dialect === "sqlite" ? defaultSqliteDialect : defaultPostgresDialect;
|
|
2534
|
+
const tenantGraph = computeTenantGraph(models);
|
|
1392
2535
|
if (log && tenantGraph.root !== null) {
|
|
1393
2536
|
const allScoped = new Set([
|
|
1394
2537
|
...tenantGraph.root !== null ? [tenantGraph.root] : [],
|
|
@@ -1396,28 +2539,51 @@ function createDb(options) {
|
|
|
1396
2539
|
...tenantGraph.indirectlyScoped,
|
|
1397
2540
|
...tenantGraph.shared
|
|
1398
2541
|
]);
|
|
1399
|
-
for (const [key, entry] of Object.entries(
|
|
2542
|
+
for (const [key, entry] of Object.entries(models)) {
|
|
1400
2543
|
if (!allScoped.has(key)) {
|
|
1401
2544
|
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
2545
|
}
|
|
1403
2546
|
}
|
|
1404
2547
|
}
|
|
1405
|
-
const
|
|
2548
|
+
const modelsRegistry = models;
|
|
1406
2549
|
let driver = null;
|
|
2550
|
+
let sqliteDriver = null;
|
|
1407
2551
|
let replicaDrivers = [];
|
|
1408
2552
|
let replicaIndex = 0;
|
|
1409
2553
|
const queryFn = (() => {
|
|
1410
2554
|
if (options._queryFn) {
|
|
1411
2555
|
return options._queryFn;
|
|
1412
2556
|
}
|
|
2557
|
+
if (dialect === "sqlite" && options.d1) {
|
|
2558
|
+
const tableSchema = buildTableSchema(models);
|
|
2559
|
+
sqliteDriver = createSqliteDriver(options.d1, tableSchema);
|
|
2560
|
+
return async (sqlStr, params) => {
|
|
2561
|
+
if (!sqliteDriver) {
|
|
2562
|
+
throw new Error("SQLite driver not initialized");
|
|
2563
|
+
}
|
|
2564
|
+
const rows = await sqliteDriver.query(sqlStr, params);
|
|
2565
|
+
return { rows, rowCount: rows.length };
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
1413
2568
|
if (options.url) {
|
|
1414
|
-
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
2569
|
+
let initialized = false;
|
|
2570
|
+
const initPostgres = async () => {
|
|
2571
|
+
if (initialized)
|
|
2572
|
+
return;
|
|
2573
|
+
const { createPostgresDriver } = await import("./shared/chunk-2gd1fqcw.js");
|
|
2574
|
+
driver = createPostgresDriver(options.url, options.pool);
|
|
2575
|
+
const replicas = options.pool?.replicas;
|
|
2576
|
+
if (replicas && replicas.length > 0) {
|
|
2577
|
+
replicaDrivers = replicas.map((replicaUrl) => createPostgresDriver(replicaUrl, options.pool));
|
|
2578
|
+
}
|
|
2579
|
+
initialized = true;
|
|
2580
|
+
};
|
|
1419
2581
|
return async (sqlStr, params) => {
|
|
2582
|
+
await initPostgres();
|
|
1420
2583
|
if (replicaDrivers.length === 0) {
|
|
2584
|
+
if (!driver) {
|
|
2585
|
+
throw new Error("Database driver not initialized");
|
|
2586
|
+
}
|
|
1421
2587
|
return driver.queryFn(sqlStr, params);
|
|
1422
2588
|
}
|
|
1423
2589
|
if (isReadQuery(sqlStr)) {
|
|
@@ -1425,127 +2591,260 @@ function createDb(options) {
|
|
|
1425
2591
|
replicaIndex = (replicaIndex + 1) % replicaDrivers.length;
|
|
1426
2592
|
try {
|
|
1427
2593
|
return await targetReplica.queryFn(sqlStr, params);
|
|
1428
|
-
} catch (
|
|
1429
|
-
console.warn("[vertz/db] replica query failed, falling back to primary:",
|
|
2594
|
+
} catch (err4) {
|
|
2595
|
+
console.warn("[vertz/db] replica query failed, falling back to primary:", err4.message);
|
|
1430
2596
|
}
|
|
1431
2597
|
}
|
|
2598
|
+
if (!driver) {
|
|
2599
|
+
throw new Error("Database driver not initialized");
|
|
2600
|
+
}
|
|
1432
2601
|
return driver.queryFn(sqlStr, params);
|
|
1433
2602
|
};
|
|
1434
2603
|
}
|
|
1435
2604
|
return async () => {
|
|
1436
|
-
throw new Error("db.query() requires a connected
|
|
2605
|
+
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
2606
|
};
|
|
1438
2607
|
})();
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
2608
|
+
function implGet(name, opts) {
|
|
2609
|
+
return (async () => {
|
|
2610
|
+
try {
|
|
2611
|
+
const entry = resolveModel(models, name);
|
|
2612
|
+
const result = await get(queryFn, entry.table, opts, dialectObj);
|
|
2613
|
+
if (result !== null && opts?.include) {
|
|
2614
|
+
const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2615
|
+
return ok2(rows[0] ?? null);
|
|
2616
|
+
}
|
|
2617
|
+
return ok2(result);
|
|
2618
|
+
} catch (e) {
|
|
2619
|
+
return err3(toReadError(e));
|
|
1448
2620
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
2621
|
+
})();
|
|
2622
|
+
}
|
|
2623
|
+
function implGetRequired(name, opts) {
|
|
2624
|
+
return (async () => {
|
|
2625
|
+
try {
|
|
2626
|
+
const entry = resolveModel(models, name);
|
|
2627
|
+
const result = await get(queryFn, entry.table, opts, dialectObj);
|
|
2628
|
+
if (result === null) {
|
|
2629
|
+
return err3({
|
|
2630
|
+
code: "NotFound",
|
|
2631
|
+
message: `Record not found in table ${name}`,
|
|
2632
|
+
table: name
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
if (opts?.include) {
|
|
2636
|
+
const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2637
|
+
return ok2(rows[0]);
|
|
2638
|
+
}
|
|
2639
|
+
return ok2(result);
|
|
2640
|
+
} catch (e) {
|
|
2641
|
+
return err3(toReadError(e));
|
|
1454
2642
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
const
|
|
1462
|
-
|
|
2643
|
+
})();
|
|
2644
|
+
}
|
|
2645
|
+
function implList(name, opts) {
|
|
2646
|
+
return (async () => {
|
|
2647
|
+
try {
|
|
2648
|
+
const entry = resolveModel(models, name);
|
|
2649
|
+
const results = await list(queryFn, entry.table, opts, dialectObj);
|
|
2650
|
+
if (opts?.include && results.length > 0) {
|
|
2651
|
+
const withRelations = await loadRelations(queryFn, results, entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2652
|
+
return ok2(withRelations);
|
|
2653
|
+
}
|
|
2654
|
+
return ok2(results);
|
|
2655
|
+
} catch (e) {
|
|
2656
|
+
return err3(toReadError(e));
|
|
1463
2657
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
const
|
|
1471
|
-
|
|
2658
|
+
})();
|
|
2659
|
+
}
|
|
2660
|
+
function implListAndCount(name, opts) {
|
|
2661
|
+
return (async () => {
|
|
2662
|
+
try {
|
|
2663
|
+
const entry = resolveModel(models, name);
|
|
2664
|
+
const { data, total } = await listAndCount(queryFn, entry.table, opts, dialectObj);
|
|
2665
|
+
if (opts?.include && data.length > 0) {
|
|
2666
|
+
const withRelations = await loadRelations(queryFn, data, entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2667
|
+
return ok2({ data: withRelations, total });
|
|
2668
|
+
}
|
|
2669
|
+
return ok2({ data, total });
|
|
2670
|
+
} catch (e) {
|
|
2671
|
+
return err3(toReadError(e));
|
|
1472
2672
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
2673
|
+
})();
|
|
2674
|
+
}
|
|
2675
|
+
function implCreate(name, opts) {
|
|
2676
|
+
return (async () => {
|
|
2677
|
+
try {
|
|
2678
|
+
const entry = resolveModel(models, name);
|
|
2679
|
+
const result = await create(queryFn, entry.table, opts, dialectObj);
|
|
2680
|
+
return ok2(result);
|
|
2681
|
+
} catch (e) {
|
|
2682
|
+
return err3(toWriteError(e));
|
|
1480
2683
|
}
|
|
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
|
-
|
|
2684
|
+
})();
|
|
2685
|
+
}
|
|
2686
|
+
function implCreateMany(name, opts) {
|
|
2687
|
+
return (async () => {
|
|
2688
|
+
try {
|
|
2689
|
+
const entry = resolveModel(models, name);
|
|
2690
|
+
const result = await createMany(queryFn, entry.table, opts, dialectObj);
|
|
2691
|
+
return ok2(result);
|
|
2692
|
+
} catch (e) {
|
|
2693
|
+
return err3(toWriteError(e));
|
|
2694
|
+
}
|
|
2695
|
+
})();
|
|
2696
|
+
}
|
|
2697
|
+
function implCreateManyAndReturn(name, opts) {
|
|
2698
|
+
return (async () => {
|
|
2699
|
+
try {
|
|
2700
|
+
const entry = resolveModel(models, name);
|
|
2701
|
+
const result = await createManyAndReturn(queryFn, entry.table, opts, dialectObj);
|
|
2702
|
+
return ok2(result);
|
|
2703
|
+
} catch (e) {
|
|
2704
|
+
return err3(toWriteError(e));
|
|
2705
|
+
}
|
|
2706
|
+
})();
|
|
2707
|
+
}
|
|
2708
|
+
function implUpdate(name, opts) {
|
|
2709
|
+
return (async () => {
|
|
2710
|
+
try {
|
|
2711
|
+
const entry = resolveModel(models, name);
|
|
2712
|
+
const result = await update(queryFn, entry.table, opts, dialectObj);
|
|
2713
|
+
return ok2(result);
|
|
2714
|
+
} catch (e) {
|
|
2715
|
+
return err3(toWriteError(e));
|
|
2716
|
+
}
|
|
2717
|
+
})();
|
|
2718
|
+
}
|
|
2719
|
+
function implUpdateMany(name, opts) {
|
|
2720
|
+
return (async () => {
|
|
2721
|
+
try {
|
|
2722
|
+
const entry = resolveModel(models, name);
|
|
2723
|
+
const result = await updateMany(queryFn, entry.table, opts, dialectObj);
|
|
2724
|
+
return ok2(result);
|
|
2725
|
+
} catch (e) {
|
|
2726
|
+
return err3(toWriteError(e));
|
|
2727
|
+
}
|
|
2728
|
+
})();
|
|
2729
|
+
}
|
|
2730
|
+
function implUpsert(name, opts) {
|
|
2731
|
+
return (async () => {
|
|
2732
|
+
try {
|
|
2733
|
+
const entry = resolveModel(models, name);
|
|
2734
|
+
const result = await upsert(queryFn, entry.table, opts, dialectObj);
|
|
2735
|
+
return ok2(result);
|
|
2736
|
+
} catch (e) {
|
|
2737
|
+
return err3(toWriteError(e));
|
|
2738
|
+
}
|
|
2739
|
+
})();
|
|
2740
|
+
}
|
|
2741
|
+
function implDelete(name, opts) {
|
|
2742
|
+
return (async () => {
|
|
2743
|
+
try {
|
|
2744
|
+
const entry = resolveModel(models, name);
|
|
2745
|
+
const result = await deleteOne(queryFn, entry.table, opts, dialectObj);
|
|
2746
|
+
return ok2(result);
|
|
2747
|
+
} catch (e) {
|
|
2748
|
+
return err3(toWriteError(e));
|
|
2749
|
+
}
|
|
2750
|
+
})();
|
|
2751
|
+
}
|
|
2752
|
+
function implDeleteMany(name, opts) {
|
|
2753
|
+
return (async () => {
|
|
2754
|
+
try {
|
|
2755
|
+
const entry = resolveModel(models, name);
|
|
2756
|
+
const result = await deleteMany(queryFn, entry.table, opts, dialectObj);
|
|
2757
|
+
return ok2(result);
|
|
2758
|
+
} catch (e) {
|
|
2759
|
+
return err3(toWriteError(e));
|
|
2760
|
+
}
|
|
2761
|
+
})();
|
|
2762
|
+
}
|
|
2763
|
+
function implCount(name, opts) {
|
|
2764
|
+
return (async () => {
|
|
2765
|
+
try {
|
|
2766
|
+
const entry = resolveModel(models, name);
|
|
2767
|
+
const result = await count(queryFn, entry.table, opts);
|
|
2768
|
+
return ok2(result);
|
|
2769
|
+
} catch (e) {
|
|
2770
|
+
return err3(toReadError(e));
|
|
2771
|
+
}
|
|
2772
|
+
})();
|
|
2773
|
+
}
|
|
2774
|
+
function implAggregate(name, opts) {
|
|
2775
|
+
return (async () => {
|
|
2776
|
+
try {
|
|
2777
|
+
const entry = resolveModel(models, name);
|
|
2778
|
+
const result = await aggregate(queryFn, entry.table, opts);
|
|
2779
|
+
return ok2(result);
|
|
2780
|
+
} catch (e) {
|
|
2781
|
+
return err3(toReadError(e));
|
|
2782
|
+
}
|
|
2783
|
+
})();
|
|
2784
|
+
}
|
|
2785
|
+
function implGroupBy(name, opts) {
|
|
2786
|
+
return (async () => {
|
|
2787
|
+
try {
|
|
2788
|
+
const entry = resolveModel(models, name);
|
|
2789
|
+
const result = await groupBy(queryFn, entry.table, opts);
|
|
2790
|
+
return ok2(result);
|
|
2791
|
+
} catch (e) {
|
|
2792
|
+
return err3(toReadError(e));
|
|
2793
|
+
}
|
|
2794
|
+
})();
|
|
2795
|
+
}
|
|
2796
|
+
const client = {};
|
|
2797
|
+
for (const name of Object.keys(models)) {
|
|
2798
|
+
client[name] = {
|
|
2799
|
+
get: (opts) => implGet(name, opts),
|
|
2800
|
+
getOrThrow: (opts) => implGetRequired(name, opts),
|
|
2801
|
+
list: (opts) => implList(name, opts),
|
|
2802
|
+
listAndCount: (opts) => implListAndCount(name, opts),
|
|
2803
|
+
create: (opts) => implCreate(name, opts),
|
|
2804
|
+
createMany: (opts) => implCreateMany(name, opts),
|
|
2805
|
+
createManyAndReturn: (opts) => implCreateManyAndReturn(name, opts),
|
|
2806
|
+
update: (opts) => implUpdate(name, opts),
|
|
2807
|
+
updateMany: (opts) => implUpdateMany(name, opts),
|
|
2808
|
+
upsert: (opts) => implUpsert(name, opts),
|
|
2809
|
+
delete: (opts) => implDelete(name, opts),
|
|
2810
|
+
deleteMany: (opts) => implDeleteMany(name, opts),
|
|
2811
|
+
count: (opts) => implCount(name, opts),
|
|
2812
|
+
aggregate: (opts) => implAggregate(name, opts),
|
|
2813
|
+
groupBy: (opts) => implGroupBy(name, opts)
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
client.query = async (fragment) => {
|
|
2817
|
+
try {
|
|
2818
|
+
const result = await executeQuery(queryFn, fragment.sql, fragment.params);
|
|
2819
|
+
return ok2(result);
|
|
2820
|
+
} catch (e) {
|
|
2821
|
+
return err3(toReadError(e, fragment.sql));
|
|
2822
|
+
}
|
|
2823
|
+
};
|
|
2824
|
+
client.close = async () => {
|
|
2825
|
+
if (driver) {
|
|
2826
|
+
await driver.close();
|
|
2827
|
+
}
|
|
2828
|
+
if (sqliteDriver) {
|
|
2829
|
+
await sqliteDriver.close();
|
|
2830
|
+
}
|
|
2831
|
+
await Promise.all(replicaDrivers.map((r) => r.close()));
|
|
2832
|
+
};
|
|
2833
|
+
client.isHealthy = async () => {
|
|
2834
|
+
if (driver) {
|
|
2835
|
+
return driver.isHealthy();
|
|
1547
2836
|
}
|
|
2837
|
+
if (sqliteDriver) {
|
|
2838
|
+
return sqliteDriver.isHealthy();
|
|
2839
|
+
}
|
|
2840
|
+
return true;
|
|
2841
|
+
};
|
|
2842
|
+
client._internals = {
|
|
2843
|
+
models,
|
|
2844
|
+
dialect: dialectObj,
|
|
2845
|
+
tenantGraph
|
|
1548
2846
|
};
|
|
2847
|
+
return client;
|
|
1549
2848
|
}
|
|
1550
2849
|
// src/schema/column.ts
|
|
1551
2850
|
function cloneWith(source, metaOverrides) {
|
|
@@ -1554,8 +2853,12 @@ function cloneWith(source, metaOverrides) {
|
|
|
1554
2853
|
function createColumnWithMeta(meta) {
|
|
1555
2854
|
const col = {
|
|
1556
2855
|
_meta: meta,
|
|
1557
|
-
primary() {
|
|
1558
|
-
|
|
2856
|
+
primary(options) {
|
|
2857
|
+
const meta2 = { primary: true, hasDefault: true };
|
|
2858
|
+
if (options?.generate) {
|
|
2859
|
+
meta2.generate = options.generate;
|
|
2860
|
+
}
|
|
2861
|
+
return cloneWith(this, meta2);
|
|
1559
2862
|
},
|
|
1560
2863
|
unique() {
|
|
1561
2864
|
return cloneWith(this, { unique: true });
|
|
@@ -1566,11 +2869,17 @@ function createColumnWithMeta(meta) {
|
|
|
1566
2869
|
default(value) {
|
|
1567
2870
|
return cloneWith(this, { hasDefault: true, defaultValue: value });
|
|
1568
2871
|
},
|
|
1569
|
-
|
|
1570
|
-
return
|
|
2872
|
+
is(flag) {
|
|
2873
|
+
return createColumnWithMeta({
|
|
2874
|
+
...this._meta,
|
|
2875
|
+
_annotations: { ...this._meta._annotations, [flag]: true }
|
|
2876
|
+
});
|
|
2877
|
+
},
|
|
2878
|
+
readOnly() {
|
|
2879
|
+
return cloneWith(this, { isReadOnly: true });
|
|
1571
2880
|
},
|
|
1572
|
-
|
|
1573
|
-
return cloneWith(this, {
|
|
2881
|
+
autoUpdate() {
|
|
2882
|
+
return cloneWith(this, { isAutoUpdate: true, isReadOnly: true });
|
|
1574
2883
|
},
|
|
1575
2884
|
check(sql) {
|
|
1576
2885
|
return cloneWith(this, { check: sql });
|
|
@@ -1590,8 +2899,9 @@ function defaultMeta(sqlType) {
|
|
|
1590
2899
|
unique: false,
|
|
1591
2900
|
nullable: false,
|
|
1592
2901
|
hasDefault: false,
|
|
1593
|
-
|
|
1594
|
-
|
|
2902
|
+
_annotations: {},
|
|
2903
|
+
isReadOnly: false,
|
|
2904
|
+
isAutoUpdate: false,
|
|
1595
2905
|
isTenant: false,
|
|
1596
2906
|
references: null,
|
|
1597
2907
|
check: null
|
|
@@ -1610,8 +2920,9 @@ function createSerialColumn() {
|
|
|
1610
2920
|
unique: false,
|
|
1611
2921
|
nullable: false,
|
|
1612
2922
|
hasDefault: true,
|
|
1613
|
-
|
|
1614
|
-
|
|
2923
|
+
_annotations: {},
|
|
2924
|
+
isReadOnly: false,
|
|
2925
|
+
isAutoUpdate: false,
|
|
1615
2926
|
isTenant: false,
|
|
1616
2927
|
references: null,
|
|
1617
2928
|
check: null
|
|
@@ -1624,14 +2935,101 @@ function createTenantColumn(targetTableName) {
|
|
|
1624
2935
|
unique: false,
|
|
1625
2936
|
nullable: false,
|
|
1626
2937
|
hasDefault: false,
|
|
1627
|
-
|
|
1628
|
-
|
|
2938
|
+
_annotations: {},
|
|
2939
|
+
isReadOnly: false,
|
|
2940
|
+
isAutoUpdate: false,
|
|
1629
2941
|
isTenant: true,
|
|
1630
2942
|
references: { table: targetTableName, column: "id" },
|
|
1631
2943
|
check: null
|
|
1632
2944
|
});
|
|
1633
2945
|
}
|
|
1634
2946
|
|
|
2947
|
+
// src/schema/model-schemas.ts
|
|
2948
|
+
function deriveSchemas(table) {
|
|
2949
|
+
const hiddenCols = getColumnNamesWithAnnotation(table, "hidden");
|
|
2950
|
+
const readOnlyCols = getColumnNamesWhere(table, "isReadOnly");
|
|
2951
|
+
const primaryCols = getColumnNamesWhere(table, "primary");
|
|
2952
|
+
const allCols = new Set(Object.keys(table._columns));
|
|
2953
|
+
const defaultCols = getColumnNamesWhere(table, "hasDefault");
|
|
2954
|
+
const responseCols = setDifference(allCols, hiddenCols);
|
|
2955
|
+
const inputCols = setDifference(setDifference(allCols, readOnlyCols), primaryCols);
|
|
2956
|
+
const requiredCols = getRequiredInputColumns(table, inputCols, defaultCols);
|
|
2957
|
+
return {
|
|
2958
|
+
response: {
|
|
2959
|
+
parse(value) {
|
|
2960
|
+
return { ok: true, data: pickKeys(value, responseCols) };
|
|
2961
|
+
}
|
|
2962
|
+
},
|
|
2963
|
+
createInput: {
|
|
2964
|
+
parse(value) {
|
|
2965
|
+
const data = value;
|
|
2966
|
+
const missing = requiredCols.filter((col) => !(col in data) || data[col] === undefined);
|
|
2967
|
+
if (missing.length > 0) {
|
|
2968
|
+
return {
|
|
2969
|
+
ok: false,
|
|
2970
|
+
error: new Error(`Missing required fields: ${missing.join(", ")}`)
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
return { ok: true, data: pickKeys(value, inputCols) };
|
|
2974
|
+
}
|
|
2975
|
+
},
|
|
2976
|
+
updateInput: {
|
|
2977
|
+
parse(value) {
|
|
2978
|
+
return { ok: true, data: pickKeys(value, inputCols) };
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
function pickKeys(value, allowed) {
|
|
2984
|
+
const data = value;
|
|
2985
|
+
const result = {};
|
|
2986
|
+
for (const [key, val] of Object.entries(data)) {
|
|
2987
|
+
if (allowed.has(key)) {
|
|
2988
|
+
result[key] = val;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
return result;
|
|
2992
|
+
}
|
|
2993
|
+
function setDifference(a, b) {
|
|
2994
|
+
const result = new Set;
|
|
2995
|
+
for (const item of a) {
|
|
2996
|
+
if (!b.has(item)) {
|
|
2997
|
+
result.add(item);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
return result;
|
|
3001
|
+
}
|
|
3002
|
+
function getRequiredInputColumns(_table, allowed, defaults) {
|
|
3003
|
+
return [...allowed].filter((key) => !defaults.has(key));
|
|
3004
|
+
}
|
|
3005
|
+
function getColumnNamesWhere(table, flag) {
|
|
3006
|
+
const result = new Set;
|
|
3007
|
+
for (const [key, col] of Object.entries(table._columns)) {
|
|
3008
|
+
if (col._meta[flag]) {
|
|
3009
|
+
result.add(key);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
return result;
|
|
3013
|
+
}
|
|
3014
|
+
function getColumnNamesWithAnnotation(table, annotation) {
|
|
3015
|
+
const result = new Set;
|
|
3016
|
+
for (const [key, col] of Object.entries(table._columns)) {
|
|
3017
|
+
if (col._meta._annotations[annotation]) {
|
|
3018
|
+
result.add(key);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
return result;
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
// src/schema/model.ts
|
|
3025
|
+
function createModel(table, relations) {
|
|
3026
|
+
return {
|
|
3027
|
+
table,
|
|
3028
|
+
relations: relations ?? {},
|
|
3029
|
+
schemas: deriveSchemas(table)
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
|
|
1635
3033
|
// src/schema/relation.ts
|
|
1636
3034
|
function createOneRelation(target, foreignKey) {
|
|
1637
3035
|
return {
|
|
@@ -1663,9 +3061,10 @@ function createManyRelation(target, foreignKey) {
|
|
|
1663
3061
|
}
|
|
1664
3062
|
|
|
1665
3063
|
// src/schema/table.ts
|
|
1666
|
-
function createIndex(columns) {
|
|
3064
|
+
function createIndex(columns, options) {
|
|
1667
3065
|
return {
|
|
1668
|
-
columns: Array.isArray(columns) ? columns : [columns]
|
|
3066
|
+
columns: Array.isArray(columns) ? columns : [columns],
|
|
3067
|
+
...options
|
|
1669
3068
|
};
|
|
1670
3069
|
}
|
|
1671
3070
|
function createTable(name, columns, options) {
|
|
@@ -1689,10 +3088,13 @@ function createTableInternal(name, columns, indexes, shared) {
|
|
|
1689
3088
|
get $update() {
|
|
1690
3089
|
return;
|
|
1691
3090
|
},
|
|
1692
|
-
get $
|
|
3091
|
+
get $response() {
|
|
3092
|
+
return;
|
|
3093
|
+
},
|
|
3094
|
+
get $create_input() {
|
|
1693
3095
|
return;
|
|
1694
3096
|
},
|
|
1695
|
-
get $
|
|
3097
|
+
get $update_input() {
|
|
1696
3098
|
return;
|
|
1697
3099
|
},
|
|
1698
3100
|
shared() {
|
|
@@ -1718,7 +3120,13 @@ var d = {
|
|
|
1718
3120
|
timestamp: () => createColumn("timestamp with time zone"),
|
|
1719
3121
|
date: () => createColumn("date"),
|
|
1720
3122
|
time: () => createColumn("time"),
|
|
1721
|
-
jsonb: (
|
|
3123
|
+
jsonb: (schemaOrOpts) => {
|
|
3124
|
+
if (schemaOrOpts && "parse" in schemaOrOpts && !("validator" in schemaOrOpts)) {
|
|
3125
|
+
return createColumn("jsonb", { validator: schemaOrOpts });
|
|
3126
|
+
}
|
|
3127
|
+
const opts = schemaOrOpts;
|
|
3128
|
+
return createColumn("jsonb", opts?.validator ? { validator: opts.validator } : {});
|
|
3129
|
+
},
|
|
1722
3130
|
textArray: () => createColumn("text[]"),
|
|
1723
3131
|
integerArray: () => createColumn("integer[]"),
|
|
1724
3132
|
enum: (name, valuesOrSchema) => {
|
|
@@ -1738,50 +3146,24 @@ var d = {
|
|
|
1738
3146
|
entry: (table, relations = {}) => ({
|
|
1739
3147
|
table,
|
|
1740
3148
|
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"
|
|
3149
|
+
}),
|
|
3150
|
+
model: (table, relations = {}) => createModel(table, relations)
|
|
1758
3151
|
};
|
|
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 };
|
|
3152
|
+
// src/schema/define-annotations.ts
|
|
3153
|
+
function defineAnnotations(...annotations) {
|
|
3154
|
+
const result = {};
|
|
3155
|
+
for (const annotation of annotations) {
|
|
3156
|
+
result[annotation] = annotation;
|
|
1780
3157
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
3158
|
+
return Object.freeze(result);
|
|
3159
|
+
}
|
|
3160
|
+
// src/schema/enum-registry.ts
|
|
3161
|
+
function createEnumRegistry(definitions) {
|
|
3162
|
+
const registry = {};
|
|
3163
|
+
for (const [name, values] of Object.entries(definitions)) {
|
|
3164
|
+
registry[name] = { name, values };
|
|
1783
3165
|
}
|
|
1784
|
-
return
|
|
3166
|
+
return registry;
|
|
1785
3167
|
}
|
|
1786
3168
|
// src/schema/registry.ts
|
|
1787
3169
|
function createRegistry(tables, relationsCallback) {
|
|
@@ -1809,195 +3191,39 @@ function createRegistry(tables, relationsCallback) {
|
|
|
1809
3191
|
}
|
|
1810
3192
|
return result;
|
|
1811
3193
|
}
|
|
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
3194
|
export {
|
|
3195
|
+
toWriteError,
|
|
3196
|
+
toReadError,
|
|
1982
3197
|
resolveErrorCode,
|
|
3198
|
+
reset,
|
|
1983
3199
|
push,
|
|
1984
3200
|
parsePgError,
|
|
3201
|
+
parseMigrationName,
|
|
1985
3202
|
migrateStatus,
|
|
1986
3203
|
migrateDev,
|
|
1987
3204
|
migrateDeploy,
|
|
1988
|
-
|
|
1989
|
-
generateClient,
|
|
3205
|
+
generateId,
|
|
1990
3206
|
formatDiagnostic,
|
|
1991
3207
|
explainError,
|
|
1992
3208
|
diagnoseError,
|
|
1993
|
-
|
|
3209
|
+
detectSchemaDrift,
|
|
3210
|
+
defineAnnotations,
|
|
3211
|
+
defaultSqliteDialect,
|
|
3212
|
+
defaultPostgresDialect,
|
|
1994
3213
|
dbErrorToHttpError,
|
|
1995
3214
|
d,
|
|
3215
|
+
createSnapshot,
|
|
1996
3216
|
createRegistry,
|
|
1997
3217
|
createEnumRegistry,
|
|
1998
3218
|
createDb,
|
|
3219
|
+
createDatabaseBridgeAdapter,
|
|
3220
|
+
createD1Driver,
|
|
3221
|
+
createD1Adapter,
|
|
1999
3222
|
computeTenantGraph,
|
|
3223
|
+
baseline,
|
|
2000
3224
|
UniqueConstraintError,
|
|
3225
|
+
SqliteDialect,
|
|
3226
|
+
PostgresDialect,
|
|
2001
3227
|
PgCodeToName,
|
|
2002
3228
|
NotNullError,
|
|
2003
3229
|
NotFoundError,
|