turbine-orm 0.40.0 → 0.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -16
- package/dist/cjs/cli/index.js +149 -36
- package/dist/cjs/cli/migrate.js +82 -26
- package/dist/cjs/schema-sql.js +12 -3
- package/dist/cjs/seed.js +104 -12
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +151 -38
- package/dist/cli/migrate.d.ts +40 -14
- package/dist/cli/migrate.js +80 -27
- package/dist/schema-sql.js +12 -3
- package/dist/seed.d.ts +11 -0
- package/dist/seed.js +105 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -602,22 +602,22 @@ Operators for Postgres array columns (`text[]`, `int[]`, etc.).
|
|
|
602
602
|
npx turbine <command> [options]
|
|
603
603
|
|
|
604
604
|
Commands:
|
|
605
|
-
init
|
|
606
|
-
generate | pull
|
|
607
|
-
push
|
|
608
|
-
migrate create <name>
|
|
609
|
-
migrate create <name> --auto
|
|
610
|
-
migrate create <name> --from-diff
|
|
611
|
-
migrate up
|
|
612
|
-
migrate deploy
|
|
613
|
-
migrate down
|
|
614
|
-
migrate status
|
|
615
|
-
seed
|
|
616
|
-
status
|
|
617
|
-
doctor
|
|
618
|
-
studio
|
|
619
|
-
mcp
|
|
620
|
-
observe
|
|
605
|
+
init Initialize a Turbine project (creates config, dirs, templates)
|
|
606
|
+
generate | pull Introspect database and generate TypeScript types + client
|
|
607
|
+
push Apply schema-builder definitions to database
|
|
608
|
+
migrate create <name> Create a new SQL migration file
|
|
609
|
+
migrate create <name> --auto Auto-generate from schema diff
|
|
610
|
+
migrate create <name> --from-diff Like --auto, but flag destructive statements
|
|
611
|
+
migrate up Apply pending migrations
|
|
612
|
+
migrate deploy Apply pending migrations without prompts
|
|
613
|
+
migrate down Rollback last migration (--step N for last N)
|
|
614
|
+
migrate status Show applied/pending migrations
|
|
615
|
+
seed Run seed file
|
|
616
|
+
status Show database schema summary
|
|
617
|
+
doctor Check relations for missing FK indexes (--fix emits migration)
|
|
618
|
+
studio Launch local Studio web UI (read-only; --write for writes, --demo for a sample DB)
|
|
619
|
+
mcp Start read-only MCP server over JSON-RPC stdio
|
|
620
|
+
observe Launch local metrics dashboard (requires TURBINE_OBSERVE_URL)
|
|
621
621
|
|
|
622
622
|
Options:
|
|
623
623
|
--url, -u <url> Postgres connection string
|
|
@@ -625,6 +625,10 @@ Options:
|
|
|
625
625
|
--schema, -s <name> Postgres schema (default: public)
|
|
626
626
|
--auto Auto-generate migration from schema diff
|
|
627
627
|
--from-diff Like --auto, but flag destructive statements (migrate create)
|
|
628
|
+
--recipe <name> Scaffold a sanctioned pattern, e.g. backfill (migrate create)
|
|
629
|
+
--step, -n <N> Apply/rollback only N migrations (migrate up / down)
|
|
630
|
+
--allow-drift Bypass checksum validation (migrate up / deploy)
|
|
631
|
+
--allow-destructive Run data-destroying statements without confirmation (up / down / push)
|
|
628
632
|
--dry-run Show SQL without executing
|
|
629
633
|
--verbose, -v Detailed output
|
|
630
634
|
```
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -67,6 +67,7 @@ exports.buildMigrateDeployOptions = buildMigrateDeployOptions;
|
|
|
67
67
|
exports.getSeedExecutionPlan = getSeedExecutionPlan;
|
|
68
68
|
exports.isLoopbackHost = isLoopbackHost;
|
|
69
69
|
const node_fs_1 = require("node:fs");
|
|
70
|
+
const node_os_1 = require("node:os");
|
|
70
71
|
const node_path_1 = require("node:path");
|
|
71
72
|
const node_url_1 = require("node:url");
|
|
72
73
|
const generate_js_1 = require("../generate.js");
|
|
@@ -74,6 +75,7 @@ const index_advisor_js_1 = require("../index-advisor.js");
|
|
|
74
75
|
const introspect_js_1 = require("../introspect.js");
|
|
75
76
|
const schema_sql_js_1 = require("../schema-sql.js");
|
|
76
77
|
const config_js_1 = require("./config.js");
|
|
78
|
+
const destructive_js_1 = require("./destructive.js");
|
|
77
79
|
const loader_js_1 = require("./loader.js");
|
|
78
80
|
const mcp_js_1 = require("./mcp.js");
|
|
79
81
|
const migrate_js_1 = require("./migrate.js");
|
|
@@ -1129,12 +1131,13 @@ async function cmdMigrate(args, config) {
|
|
|
1129
1131
|
console.log(` ${(0, ui_js_1.cyan)('status')} Show migration status`);
|
|
1130
1132
|
(0, ui_js_1.newline)();
|
|
1131
1133
|
console.log(` ${(0, ui_js_1.bold)('Options:')}`);
|
|
1132
|
-
console.log(` ${(0, ui_js_1.cyan)('--auto')} Auto-generate UP/DOWN SQL from schema diff`);
|
|
1133
|
-
console.log(` ${(0, ui_js_1.cyan)('--from-diff')}
|
|
1134
|
+
console.log(` ${(0, ui_js_1.cyan)('--auto')} Auto-generate UP/DOWN SQL from schema diff (destructive flagged)`);
|
|
1135
|
+
console.log(` ${(0, ui_js_1.cyan)('--from-diff')} Generate from schema diff, destructive statements flagged inline`);
|
|
1134
1136
|
console.log(` ${(0, ui_js_1.cyan)('--recipe <name>')} Scaffold a sanctioned migration pattern`);
|
|
1135
1137
|
console.log(` ${(0, ui_js_1.cyan)('--step, -n')} Number of migrations to apply/rollback`);
|
|
1136
1138
|
console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
|
|
1137
|
-
console.log(` ${(0, ui_js_1.cyan)('--allow-
|
|
1139
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying statements without prompting`);
|
|
1140
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Bypass checksum validation on ${(0, ui_js_1.cyan)('up')} / ${(0, ui_js_1.cyan)('deploy')} ${(0, ui_js_1.dim)('(advanced)')}`);
|
|
1138
1141
|
(0, ui_js_1.newline)();
|
|
1139
1142
|
console.log(` ${(0, ui_js_1.bold)('Recipes')} ${(0, ui_js_1.dim)('(--recipe):')}`);
|
|
1140
1143
|
for (const [key, recipe] of Object.entries(migrate_js_1.MIGRATION_RECIPES)) {
|
|
@@ -1217,9 +1220,13 @@ async function cmdMigrateCreate(args, config) {
|
|
|
1217
1220
|
}
|
|
1218
1221
|
diffSpinner.succeed(`Found ${(0, ui_js_1.bold)(String(diff.statements.length))} change(s)`);
|
|
1219
1222
|
(0, ui_js_1.newline)();
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
+
// Route through buildDiffMigrationBody so any destructive statement (a lossy
|
|
1224
|
+
// ALTER COLUMN ... TYPE, or a DROP COLUMN for a column removed from the
|
|
1225
|
+
// schema) is flagged inline in the file, matching --from-diff. A
|
|
1226
|
+
// destructive-only diff therefore produces a real, flagged migration instead
|
|
1227
|
+
// of the old "already in sync" false negative.
|
|
1228
|
+
const body = (0, migrate_js_1.buildDiffMigrationBody)(diff);
|
|
1229
|
+
const file = (0, migrate_js_1.createMigration)(config.migrationsDir, name, { up: body.up, down: body.down });
|
|
1223
1230
|
const relPath = (0, node_path_1.relative)(process.cwd(), file.path);
|
|
1224
1231
|
(0, ui_js_1.success)(`Created auto-migration: ${(0, ui_js_1.bold)(file.filename)}`);
|
|
1225
1232
|
(0, ui_js_1.newline)();
|
|
@@ -1247,6 +1254,27 @@ async function cmdMigrateCreate(args, config) {
|
|
|
1247
1254
|
}
|
|
1248
1255
|
}
|
|
1249
1256
|
(0, ui_js_1.newline)();
|
|
1257
|
+
// Loudly flag any destructive statements written into the file (same as
|
|
1258
|
+
// --from-diff). They stay intact so `migrate up` still refuses them by
|
|
1259
|
+
// default, but the operator must know they are there before running.
|
|
1260
|
+
const destructiveCount = body.destructiveUp.length + body.destructiveDown.length;
|
|
1261
|
+
if (destructiveCount > 0) {
|
|
1262
|
+
(0, ui_js_1.warn)(`This migration contains ${(0, ui_js_1.bold)(String(destructiveCount))} DESTRUCTIVE statement(s), flagged in the file.`);
|
|
1263
|
+
for (const h of body.destructiveUp) {
|
|
1264
|
+
console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.dim)('UP')} [${h.kind}] ${h.target}`);
|
|
1265
|
+
}
|
|
1266
|
+
for (const h of body.destructiveDown) {
|
|
1267
|
+
console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${(0, ui_js_1.dim)('DOWN')} [${h.kind}] ${h.target}`);
|
|
1268
|
+
}
|
|
1269
|
+
(0, ui_js_1.newline)();
|
|
1270
|
+
console.log(` ${(0, ui_js_1.dim)('`migrate up` refuses destructive statements by default: confirm interactively or pass')} ${(0, ui_js_1.cyan)('--allow-destructive')}${(0, ui_js_1.dim)('.')}`);
|
|
1271
|
+
(0, ui_js_1.newline)();
|
|
1272
|
+
}
|
|
1273
|
+
if (diff.warnings && diff.warnings.length > 0) {
|
|
1274
|
+
for (const w of diff.warnings)
|
|
1275
|
+
(0, ui_js_1.warn)(w);
|
|
1276
|
+
(0, ui_js_1.newline)();
|
|
1277
|
+
}
|
|
1250
1278
|
console.log(` ${(0, ui_js_1.dim)('Review the migration, then run:')}`);
|
|
1251
1279
|
console.log(` ${(0, ui_js_1.cyan)('npx turbine migrate up')}`);
|
|
1252
1280
|
(0, ui_js_1.newline)();
|
|
@@ -1417,6 +1445,7 @@ async function cmdMigrateUp(args, config) {
|
|
|
1417
1445
|
console.log(` ${(0, ui_js_1.green)(ui_js_1.symbols.check)} ${file.filename}`);
|
|
1418
1446
|
}
|
|
1419
1447
|
}
|
|
1448
|
+
warnOutOfOrder(result.outOfOrder);
|
|
1420
1449
|
if (result.errors.length > 0) {
|
|
1421
1450
|
spinner.fail('Migration failed');
|
|
1422
1451
|
for (const { file, error: msg } of result.errors) {
|
|
@@ -1428,58 +1457,101 @@ async function cmdMigrateUp(args, config) {
|
|
|
1428
1457
|
}
|
|
1429
1458
|
(0, ui_js_1.newline)();
|
|
1430
1459
|
}
|
|
1431
|
-
|
|
1460
|
+
/** Print a one-line warning for each migration applied out of timestamp order. */
|
|
1461
|
+
function warnOutOfOrder(outOfOrder) {
|
|
1462
|
+
if (outOfOrder.length === 0)
|
|
1463
|
+
return;
|
|
1464
|
+
(0, ui_js_1.newline)();
|
|
1465
|
+
for (const o of outOfOrder) {
|
|
1466
|
+
(0, ui_js_1.warn)(`Applied ${(0, ui_js_1.bold)(o.applied)} out of order (older than already-applied ${(0, ui_js_1.bold)(o.newestPrior)}).`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
function buildMigrateDeployOptions(args) {
|
|
1432
1470
|
return {
|
|
1433
|
-
allowDrift:
|
|
1471
|
+
allowDrift: args.allowDrift === true,
|
|
1434
1472
|
allowDestructive: true,
|
|
1435
1473
|
step: undefined,
|
|
1436
1474
|
};
|
|
1437
1475
|
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Print the itemized, classified destructive-statement report as a NOTICE.
|
|
1478
|
+
* `deploy` proceeds by design (the gate ran at author time), but it must not run
|
|
1479
|
+
* data-destroying SQL in total silence; the notice ends that zero-ceremony hole.
|
|
1480
|
+
*/
|
|
1481
|
+
function printDestructiveNotice(offenders) {
|
|
1482
|
+
(0, ui_js_1.warn)('NOTICE: this deploy runs DESTRUCTIVE statement(s):');
|
|
1483
|
+
for (const o of offenders) {
|
|
1484
|
+
console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.warning)} ${o.file}`);
|
|
1485
|
+
for (const h of o.hits) {
|
|
1486
|
+
console.log(` ${(0, ui_js_1.dim)('-')} [${h.kind}] ${h.target} ${(0, ui_js_1.dim)(destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind])}`);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
(0, ui_js_1.newline)();
|
|
1490
|
+
}
|
|
1438
1491
|
async function cmdMigrateDeploy(args, config) {
|
|
1439
1492
|
(0, ui_js_1.banner)();
|
|
1440
1493
|
const url = requireUrl(config);
|
|
1441
1494
|
(0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
|
|
1442
1495
|
(0, ui_js_1.label)('Migrations', config.migrationsDir);
|
|
1443
1496
|
(0, ui_js_1.newline)();
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1497
|
+
const spinner = new ui_js_1.Spinner('Checking pending migrations').start();
|
|
1498
|
+
const plan = await (0, migrate_js_1.inspectMigrationDeploy)(url, config.migrationsDir);
|
|
1499
|
+
spinner.stop();
|
|
1500
|
+
// Drift handling: honor --allow-drift exactly like `up`. Without it, block;
|
|
1501
|
+
// with it, warn loudly and proceed.
|
|
1502
|
+
if (plan.mismatches.length > 0) {
|
|
1503
|
+
if (!args.allowDrift) {
|
|
1504
|
+
(0, ui_js_1.error)('Deploy blocked by migration drift');
|
|
1505
|
+
(0, ui_js_1.newline)();
|
|
1506
|
+
for (const line of (0, migrate_js_1.formatChecksumMismatchError)(plan.mismatches).split('\n')) {
|
|
1507
|
+
console.log(` ${line.replace('[turbine] ', '')}`);
|
|
1452
1508
|
}
|
|
1453
1509
|
(0, ui_js_1.newline)();
|
|
1454
1510
|
process.exit(1);
|
|
1455
1511
|
}
|
|
1512
|
+
(0, ui_js_1.warn)('--allow-drift is set: checksum validation is DISABLED for this deploy.');
|
|
1513
|
+
console.log(` ${(0, ui_js_1.dim)('Applied migrations may have been modified or deleted on disk.')}`);
|
|
1514
|
+
(0, ui_js_1.newline)();
|
|
1515
|
+
}
|
|
1516
|
+
if (args.dryRun) {
|
|
1456
1517
|
if (plan.pending.length === 0) {
|
|
1457
|
-
|
|
1518
|
+
(0, ui_js_1.info)('No pending migrations');
|
|
1458
1519
|
(0, ui_js_1.newline)();
|
|
1459
1520
|
return;
|
|
1460
1521
|
}
|
|
1461
|
-
|
|
1522
|
+
(0, ui_js_1.info)(`${(0, ui_js_1.bold)(String(plan.pending.length))} pending migration(s)`);
|
|
1462
1523
|
for (const file of plan.pending) {
|
|
1463
1524
|
console.log(` ${(0, ui_js_1.yellow)(ui_js_1.symbols.dot)} ${file.filename}`);
|
|
1464
1525
|
}
|
|
1526
|
+
// Surface destructive statements even in a dry run so CI can see them.
|
|
1527
|
+
const destructive = (0, migrate_js_1.collectUpDestructive)(plan.pending);
|
|
1528
|
+
if (destructive.length > 0) {
|
|
1529
|
+
(0, ui_js_1.newline)();
|
|
1530
|
+
printDestructiveNotice(destructive);
|
|
1531
|
+
}
|
|
1465
1532
|
(0, ui_js_1.newline)();
|
|
1466
1533
|
return;
|
|
1467
1534
|
}
|
|
1468
|
-
|
|
1469
|
-
const
|
|
1535
|
+
// Destructive notice before applying (deploy still proceeds by design).
|
|
1536
|
+
const destructive = (0, migrate_js_1.collectUpDestructive)(plan.pending);
|
|
1537
|
+
if (destructive.length > 0)
|
|
1538
|
+
printDestructiveNotice(destructive);
|
|
1539
|
+
const runSpinner = new ui_js_1.Spinner('Deploying migrations').start();
|
|
1540
|
+
const result = await (0, migrate_js_1.migrateDeploy)(url, config.migrationsDir, { allowDrift: args.allowDrift });
|
|
1470
1541
|
if (result.applied.length === 0 && result.errors.length === 0) {
|
|
1471
|
-
|
|
1542
|
+
runSpinner.succeed('0 applied, all migrations are up to date');
|
|
1472
1543
|
(0, ui_js_1.newline)();
|
|
1473
1544
|
return;
|
|
1474
1545
|
}
|
|
1475
1546
|
if (result.applied.length > 0) {
|
|
1476
|
-
|
|
1547
|
+
runSpinner.succeed(`${(0, ui_js_1.bold)(String(result.applied.length))} applied`);
|
|
1477
1548
|
for (const file of result.applied) {
|
|
1478
1549
|
console.log(` ${(0, ui_js_1.green)(ui_js_1.symbols.check)} ${file.filename}`);
|
|
1479
1550
|
}
|
|
1480
1551
|
}
|
|
1552
|
+
warnOutOfOrder(result.outOfOrder);
|
|
1481
1553
|
if (result.errors.length > 0) {
|
|
1482
|
-
|
|
1554
|
+
runSpinner.fail('Deploy failed');
|
|
1483
1555
|
for (const { file, error: msg } of result.errors) {
|
|
1484
1556
|
console.log(` ${(0, ui_js_1.red)(ui_js_1.symbols.cross)} ${file.filename}`);
|
|
1485
1557
|
console.log(` ${(0, ui_js_1.dim)(msg)}`);
|
|
@@ -1596,8 +1668,17 @@ async function cmdMigrateStatus(_args, config) {
|
|
|
1596
1668
|
const pendingCount = statuses.filter((s) => !s.applied).length;
|
|
1597
1669
|
(0, ui_js_1.info)(`${(0, ui_js_1.bold)(String(appliedCount))} applied, ${pendingCount > 0 ? (0, ui_js_1.yellow)((0, ui_js_1.bold)(String(pendingCount))) : (0, ui_js_1.bold)(String(pendingCount))} pending`);
|
|
1598
1670
|
(0, ui_js_1.newline)();
|
|
1599
|
-
//
|
|
1600
|
-
|
|
1671
|
+
// Applied migrations whose file was deleted from disk (distinct from an
|
|
1672
|
+
// on-disk edit). Counted in "applied" above; surfaced with their own banner.
|
|
1673
|
+
const missingCount = statuses.filter((s) => s.missingFile).length;
|
|
1674
|
+
if (missingCount > 0) {
|
|
1675
|
+
(0, ui_js_1.warn)(`${(0, ui_js_1.bold)(String(missingCount))} applied migration(s) are missing from disk!`);
|
|
1676
|
+
console.log(` ${(0, ui_js_1.dim)('The history table records them, but their .sql file is gone.')}`);
|
|
1677
|
+
console.log(` ${(0, ui_js_1.dim)('Restore the file(s) before running')} ${(0, ui_js_1.cyan)('migrate up')} ${(0, ui_js_1.dim)('or')} ${(0, ui_js_1.cyan)('migrate deploy')}${(0, ui_js_1.dim)('.')}`);
|
|
1678
|
+
(0, ui_js_1.newline)();
|
|
1679
|
+
}
|
|
1680
|
+
// Check for checksum mismatches (on-disk edits only; missing files above).
|
|
1681
|
+
const driftCount = statuses.filter((s) => s.checksumValid === false && !s.missingFile).length;
|
|
1601
1682
|
if (driftCount > 0) {
|
|
1602
1683
|
(0, ui_js_1.warn)(`${(0, ui_js_1.bold)(String(driftCount))} migration(s) have been modified after application!`);
|
|
1603
1684
|
console.log(` ${(0, ui_js_1.dim)('Applied migrations should be immutable. Modifying them can cause drift.')}`);
|
|
@@ -1607,7 +1688,10 @@ async function cmdMigrateStatus(_args, config) {
|
|
|
1607
1688
|
const headers = ['Status', 'Migration', 'Applied at'];
|
|
1608
1689
|
const rows = statuses.map((s) => {
|
|
1609
1690
|
let status;
|
|
1610
|
-
if (s.
|
|
1691
|
+
if (s.missingFile) {
|
|
1692
|
+
status = (0, ui_js_1.red)(`${ui_js_1.symbols.warning} Missing file`);
|
|
1693
|
+
}
|
|
1694
|
+
else if (s.applied && s.checksumValid === false) {
|
|
1611
1695
|
status = (0, ui_js_1.red)(`${ui_js_1.symbols.warning} Drifted`);
|
|
1612
1696
|
}
|
|
1613
1697
|
else if (s.applied) {
|
|
@@ -1656,21 +1740,40 @@ async function runSeedPlan(plan, config) {
|
|
|
1656
1740
|
if (!(0, loader_js_1.canResolveTsx)()) {
|
|
1657
1741
|
throw new Error('TypeScript seed files require tsx — install tsx or use seed.js/seed.sql.');
|
|
1658
1742
|
}
|
|
1743
|
+
// The seed runs in a child process, so we cannot observe its callback
|
|
1744
|
+
// directly. Hand it a sentinel path: `defineSeed`'s runner writes the file
|
|
1745
|
+
// only after a callback executes to completion. If the child exits cleanly
|
|
1746
|
+
// but the sentinel never appears, the seed module loaded without running
|
|
1747
|
+
// anything: a silent no-op we must report as a failure, not success.
|
|
1748
|
+
const sentinelDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'turbine-seed-'));
|
|
1749
|
+
const sentinel = (0, node_path_1.join)(sentinelDir, 'ran');
|
|
1659
1750
|
const { execFileSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1751
|
+
try {
|
|
1752
|
+
execFileSync(plan.command, plan.args, {
|
|
1753
|
+
stdio: 'inherit',
|
|
1754
|
+
env: {
|
|
1755
|
+
...process.env,
|
|
1756
|
+
DATABASE_URL: config.url || process.env.DATABASE_URL,
|
|
1757
|
+
TURBINE_SEED_SENTINEL: sentinel,
|
|
1758
|
+
},
|
|
1759
|
+
});
|
|
1760
|
+
if (!(0, node_fs_1.existsSync)(sentinel)) {
|
|
1761
|
+
throw new Error('The seed file completed without running a seed callback. ' +
|
|
1762
|
+
'Make sure it calls defineSeed(async (db) => { ... }) at the top level.');
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
finally {
|
|
1766
|
+
(0, node_fs_1.rmSync)(sentinelDir, { recursive: true, force: true });
|
|
1767
|
+
}
|
|
1667
1768
|
return;
|
|
1668
1769
|
}
|
|
1669
1770
|
if (plan.kind === 'js') {
|
|
1670
1771
|
const mod = await Promise.resolve(`${(0, node_url_1.pathToFileURL)(plan.file).href}`).then(s => __importStar(require(s)));
|
|
1671
|
-
if (typeof mod.default
|
|
1672
|
-
|
|
1772
|
+
if (typeof mod.default !== 'function') {
|
|
1773
|
+
throw new Error('The seed file has no callable default export. ' +
|
|
1774
|
+
'Export your seed with `export default defineSeed(async (db) => { ... })`.');
|
|
1673
1775
|
}
|
|
1776
|
+
await mod.default();
|
|
1674
1777
|
return;
|
|
1675
1778
|
}
|
|
1676
1779
|
const url = requireUrl(config);
|
|
@@ -2263,8 +2366,18 @@ function showHelp() {
|
|
|
2263
2366
|
(0, ui_js_1.newline)();
|
|
2264
2367
|
console.log(` ${(0, ui_js_1.bold)('Migrate options:')}`);
|
|
2265
2368
|
console.log(` ${(0, ui_js_1.cyan)('--auto')} Auto-generate UP/DOWN SQL from schema diff ${(0, ui_js_1.dim)('(create)')}`);
|
|
2369
|
+
console.log(` ${(0, ui_js_1.cyan)('--from-diff')} Generate from schema diff, destructive statements flagged ${(0, ui_js_1.dim)('(create)')}`);
|
|
2370
|
+
console.log(` ${(0, ui_js_1.cyan)('--recipe')} ${(0, ui_js_1.dim)('<name>')} Scaffold a named migration recipe, e.g. backfill ${(0, ui_js_1.dim)('(create)')}`);
|
|
2266
2371
|
console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
|
|
2267
|
-
console.log(` ${(0, ui_js_1.cyan)('--allow-
|
|
2372
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying statements without prompting ${(0, ui_js_1.dim)('(up/down/push)')}`);
|
|
2373
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Bypass checksum validation on ${(0, ui_js_1.cyan)('migrate up')} / ${(0, ui_js_1.cyan)('deploy')} ${(0, ui_js_1.dim)('(advanced)')}`);
|
|
2374
|
+
(0, ui_js_1.newline)();
|
|
2375
|
+
console.log(` ${(0, ui_js_1.bold)('Init options:')}`);
|
|
2376
|
+
console.log(` ${(0, ui_js_1.cyan)('--yes, -y')} Accept every step's default (non-interactive)`);
|
|
2377
|
+
console.log(` ${(0, ui_js_1.cyan)('--skip-schema')} Don't scaffold the schema file`);
|
|
2378
|
+
console.log(` ${(0, ui_js_1.cyan)('--skip-seed')} Don't scaffold or run the seed file`);
|
|
2379
|
+
console.log(` ${(0, ui_js_1.cyan)('--skip-push')} Don't offer to push the schema to the database`);
|
|
2380
|
+
console.log(` ${(0, ui_js_1.cyan)('--skip-generate')} Don't offer to generate the typed client`);
|
|
2268
2381
|
(0, ui_js_1.newline)();
|
|
2269
2382
|
console.log(` ${(0, ui_js_1.bold)('Studio / observe options:')}`);
|
|
2270
2383
|
console.log(` ${(0, ui_js_1.cyan)('--port')} ${(0, ui_js_1.dim)('<n>')} HTTP port ${(0, ui_js_1.dim)('(default: 4983 studio, 4984 observe)')}`);
|
package/dist/cjs/cli/migrate.js
CHANGED
|
@@ -17,6 +17,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
17
|
};
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
19
|
exports.MIGRATION_RECIPES = void 0;
|
|
20
|
+
exports.migrationTimestamp = migrationTimestamp;
|
|
21
|
+
exports.collectUpDestructive = collectUpDestructive;
|
|
20
22
|
exports.parseMigrationFilename = parseMigrationFilename;
|
|
21
23
|
exports.sanitizeName = sanitizeName;
|
|
22
24
|
exports.formatTimestamp = formatTimestamp;
|
|
@@ -27,6 +29,7 @@ exports.parseMigrationSQL = parseMigrationSQL;
|
|
|
27
29
|
exports.buildDiffMigrationBody = buildDiffMigrationBody;
|
|
28
30
|
exports.createMigration = createMigration;
|
|
29
31
|
exports.deriveLockId = deriveLockId;
|
|
32
|
+
exports.formatChecksumMismatchError = formatChecksumMismatchError;
|
|
30
33
|
exports.planMigrationDeploy = planMigrationDeploy;
|
|
31
34
|
exports.inspectMigrationDeploy = inspectMigrationDeploy;
|
|
32
35
|
exports.migrateUp = migrateUp;
|
|
@@ -41,6 +44,24 @@ const index_js_1 = require("../adapters/index.js");
|
|
|
41
44
|
const dialect_js_1 = require("../dialect.js");
|
|
42
45
|
const errors_js_1 = require("../errors.js");
|
|
43
46
|
const destructive_js_1 = require("./destructive.js");
|
|
47
|
+
/** Extract the YYYYMMDDHHMMSS timestamp prefix from a migration name, or null. */
|
|
48
|
+
function migrationTimestamp(name) {
|
|
49
|
+
const m = name.match(/^(\d{14})(?:_|$)/);
|
|
50
|
+
return m ? m[1] : null;
|
|
51
|
+
}
|
|
52
|
+
/** Scan a set of migration files' UP sections for data-destroying statements. */
|
|
53
|
+
function collectUpDestructive(files) {
|
|
54
|
+
const offenders = [];
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
const { up } = parseMigrationSQL(file.path);
|
|
57
|
+
if (!up)
|
|
58
|
+
continue;
|
|
59
|
+
const hits = (0, destructive_js_1.scanDestructiveSql)(up);
|
|
60
|
+
if (hits.length > 0)
|
|
61
|
+
offenders.push({ file: file.filename, hits });
|
|
62
|
+
}
|
|
63
|
+
return offenders;
|
|
64
|
+
}
|
|
44
65
|
// ---------------------------------------------------------------------------
|
|
45
66
|
// Tracking table management
|
|
46
67
|
// ---------------------------------------------------------------------------
|
|
@@ -472,7 +493,14 @@ function formatChecksumMismatchError(mismatches) {
|
|
|
472
493
|
lines.push('');
|
|
473
494
|
lines.push('Fix one of these:');
|
|
474
495
|
lines.push(' 1. Restore the file(s) to their original content, OR');
|
|
475
|
-
|
|
496
|
+
if (modified.length > 0) {
|
|
497
|
+
// `migrate down` needs the file on disk to read its DOWN section, so it is
|
|
498
|
+
// only a remedy for MODIFIED files, never for deleted ones.
|
|
499
|
+
lines.push(' 2. Roll back the affected migrations with `npx turbine migrate down` (modified files only), OR');
|
|
500
|
+
}
|
|
501
|
+
if (missing.length > 0) {
|
|
502
|
+
lines.push(' (deleted files cannot be rolled back: restore the file, then run `migrate down` if needed), OR');
|
|
503
|
+
}
|
|
476
504
|
lines.push(' 3. Pass `--allow-drift` to bypass this check (advanced — make sure you know what you are doing).');
|
|
477
505
|
return lines.join('\n');
|
|
478
506
|
}
|
|
@@ -581,42 +609,43 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
581
609
|
}
|
|
582
610
|
const applied = await getAppliedMigrations(client, dialect);
|
|
583
611
|
const appliedNames = new Set(applied.map((m) => m.name));
|
|
612
|
+
// Newest already-applied timestamp. Anything applied below this line is
|
|
613
|
+
// going in out of order (an older migration created/applied after a newer
|
|
614
|
+
// one). Used to surface a warning; never blocks.
|
|
615
|
+
const newestPrior = applied
|
|
616
|
+
.map((m) => ({ ts: migrationTimestamp(m.name), name: m.name }))
|
|
617
|
+
.filter((m) => m.ts !== null)
|
|
618
|
+
.sort((a, b) => (a.ts < b.ts ? 1 : a.ts > b.ts ? -1 : 0))[0];
|
|
584
619
|
const allFiles = listMigrationFiles(migrationsDir);
|
|
585
620
|
let pending = allFiles.filter((f) => !appliedNames.has(f.name));
|
|
586
621
|
if (options?.step != null && options.step > 0) {
|
|
587
622
|
pending = pending.slice(0, options.step);
|
|
588
623
|
}
|
|
624
|
+
// Destructive statements in the pending batch, computed once. Returned in
|
|
625
|
+
// the result regardless of the gate so `deploy` can print a notice even
|
|
626
|
+
// though it proceeds by design.
|
|
627
|
+
const destructive = collectUpDestructive(pending);
|
|
589
628
|
// Data-loss gate: refuse to run pending migrations containing destructive
|
|
590
629
|
// statements unless the caller has EXPLICITLY opted in. The CLI layers an
|
|
591
630
|
// interactive typed confirmation on top of this; programmatic callers must
|
|
592
631
|
// pass `allowDestructive: true`. Safe-by-default is the whole point — a
|
|
593
632
|
// DROP TABLE should never run just because a file exists.
|
|
594
|
-
if (!options?.allowDestructive) {
|
|
595
|
-
const
|
|
596
|
-
for (const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const hits = (0, destructive_js_1.scanDestructiveSql)(up);
|
|
601
|
-
if (hits.length > 0)
|
|
602
|
-
offenders.push({ file: file.filename, hits });
|
|
603
|
-
}
|
|
604
|
-
if (offenders.length > 0) {
|
|
605
|
-
const lines = ['[turbine] Refusing to apply migrations containing DESTRUCTIVE statements:', ''];
|
|
606
|
-
for (const o of offenders) {
|
|
607
|
-
lines.push(` ${o.file}`);
|
|
608
|
-
for (const h of o.hits) {
|
|
609
|
-
lines.push(` - [${h.kind}] ${h.target} — ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
610
|
-
}
|
|
633
|
+
if (!options?.allowDestructive && destructive.length > 0) {
|
|
634
|
+
const lines = ['[turbine] Refusing to apply migrations containing DESTRUCTIVE statements:', ''];
|
|
635
|
+
for (const o of destructive) {
|
|
636
|
+
lines.push(` ${o.file}`);
|
|
637
|
+
for (const h of o.hits) {
|
|
638
|
+
lines.push(` - [${h.kind}] ${h.target}: ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
611
639
|
}
|
|
612
|
-
lines.push('');
|
|
613
|
-
lines.push('Review the statements above. To proceed: run `npx turbine migrate up` interactively');
|
|
614
|
-
lines.push('and confirm, pass --allow-destructive, or set allowDestructive: true programmatically.');
|
|
615
|
-
throw new errors_js_1.MigrationError(lines.join('\n'));
|
|
616
640
|
}
|
|
641
|
+
lines.push('');
|
|
642
|
+
lines.push('Review the statements above. To proceed: run `npx turbine migrate up` interactively');
|
|
643
|
+
lines.push('and confirm, pass --allow-destructive, or set allowDestructive: true programmatically.');
|
|
644
|
+
throw new errors_js_1.MigrationError(lines.join('\n'));
|
|
617
645
|
}
|
|
618
646
|
const results = [];
|
|
619
647
|
const errors = [];
|
|
648
|
+
const outOfOrder = [];
|
|
620
649
|
for (const file of pending) {
|
|
621
650
|
const { up } = parseMigrationSQL(file.path);
|
|
622
651
|
if (!up) {
|
|
@@ -631,6 +660,11 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
631
660
|
await client.query(dialect.buildMigrationInsertApplied(quotedTrackingTable(dialect)), [file.name, hash]);
|
|
632
661
|
await client.query('COMMIT');
|
|
633
662
|
results.push(file);
|
|
663
|
+
// Flag an out-of-order apply: this file's timestamp is older than a
|
|
664
|
+
// migration that was already applied before this run started.
|
|
665
|
+
if (newestPrior && file.timestamp && file.timestamp < newestPrior.ts) {
|
|
666
|
+
outOfOrder.push({ applied: file.filename, newestPrior: `${newestPrior.name}.sql` });
|
|
667
|
+
}
|
|
634
668
|
}
|
|
635
669
|
catch (err) {
|
|
636
670
|
await client.query('ROLLBACK');
|
|
@@ -640,7 +674,7 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
640
674
|
break;
|
|
641
675
|
}
|
|
642
676
|
}
|
|
643
|
-
return { applied: results, errors };
|
|
677
|
+
return { applied: results, errors, destructive, outOfOrder };
|
|
644
678
|
}
|
|
645
679
|
finally {
|
|
646
680
|
await releaseLock(client, lockId, adapter);
|
|
@@ -656,7 +690,9 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
656
690
|
*/
|
|
657
691
|
async function migrateDeploy(connectionString, migrationsDir, options) {
|
|
658
692
|
return migrateUp(connectionString, migrationsDir, {
|
|
659
|
-
|
|
693
|
+
// Honor `--allow-drift` on deploy exactly as `up` does: deploy's own drift
|
|
694
|
+
// error recommends this flag, so it must actually bypass the checksum block.
|
|
695
|
+
allowDrift: options?.allowDrift === true,
|
|
660
696
|
allowDestructive: true,
|
|
661
697
|
adapter: options?.adapter,
|
|
662
698
|
dialect: options?.dialect,
|
|
@@ -715,7 +751,7 @@ async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
715
751
|
for (const o of offenders) {
|
|
716
752
|
lines.push(` ${o.file}`);
|
|
717
753
|
for (const h of o.hits) {
|
|
718
|
-
lines.push(` - [${h.kind}] ${h.target}
|
|
754
|
+
lines.push(` - [${h.kind}] ${h.target}: ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
719
755
|
}
|
|
720
756
|
}
|
|
721
757
|
lines.push('');
|
|
@@ -777,7 +813,8 @@ async function migrateStatus(connectionString, migrationsDir, options) {
|
|
|
777
813
|
const applied = await getAppliedMigrations(client, dialect);
|
|
778
814
|
const appliedMap = new Map(applied.map((m) => [m.name, m]));
|
|
779
815
|
const allFiles = listMigrationFiles(migrationsDir);
|
|
780
|
-
|
|
816
|
+
const fileNames = new Set(allFiles.map((f) => f.name));
|
|
817
|
+
const fromFiles = allFiles.map((file) => {
|
|
781
818
|
const record = appliedMap.get(file.name);
|
|
782
819
|
let checksumValid;
|
|
783
820
|
if (record) {
|
|
@@ -792,6 +829,25 @@ async function migrateStatus(connectionString, migrationsDir, options) {
|
|
|
792
829
|
checksumValid,
|
|
793
830
|
};
|
|
794
831
|
});
|
|
832
|
+
// Applied migrations whose file was deleted from disk. up/deploy already
|
|
833
|
+
// catch this as drift; status must not silently drop them from history.
|
|
834
|
+
const missing = applied
|
|
835
|
+
.filter((m) => !fileNames.has(m.name))
|
|
836
|
+
.map((m) => ({
|
|
837
|
+
file: parseMigrationFilename(`${m.name}.sql`) ?? {
|
|
838
|
+
filename: `${m.name}.sql`,
|
|
839
|
+
path: '',
|
|
840
|
+
name: m.name,
|
|
841
|
+
timestamp: '',
|
|
842
|
+
},
|
|
843
|
+
applied: true,
|
|
844
|
+
appliedAt: m.applied_at,
|
|
845
|
+
checksumValid: false,
|
|
846
|
+
missingFile: true,
|
|
847
|
+
}));
|
|
848
|
+
// Keep the overall list in timestamp order so a deleted entry appears where
|
|
849
|
+
// it belongs in history, not tacked on at the end.
|
|
850
|
+
return [...fromFiles, ...missing].sort((a, b) => a.file.name < b.file.name ? -1 : a.file.name > b.file.name ? 1 : 0);
|
|
795
851
|
}
|
|
796
852
|
finally {
|
|
797
853
|
await client.end();
|
package/dist/cjs/schema-sql.js
CHANGED
|
@@ -839,14 +839,23 @@ async function schemaDiff(schema, connectionString) {
|
|
|
839
839
|
}
|
|
840
840
|
}
|
|
841
841
|
}
|
|
842
|
-
// Check for columns in DB that are not in schema
|
|
842
|
+
// Check for columns in DB that are not in schema.
|
|
843
|
+
// The DROP COLUMN statement stays IN `result.statements` so the destructive
|
|
844
|
+
// gate (findDestructivePushStatements / scanDestructiveSql, and the migrate
|
|
845
|
+
// up gate) can see it and refuse by default. Safety is enforced by that
|
|
846
|
+
// gate, not by hiding the statement: withholding it here previously made
|
|
847
|
+
// `push` report "already in sync" for a destructive-only diff and print a
|
|
848
|
+
// false "Altered" for a mixed one, since the 0.36 guard never received it.
|
|
849
|
+
// No reverse is emitted (a dropped column and its data cannot be recreated
|
|
850
|
+
// automatically), so diff-generated migrations fall back to the documented
|
|
851
|
+
// irreversible-DOWN placeholder for this change.
|
|
843
852
|
for (const dbColName of Object.keys(dbCols)) {
|
|
844
853
|
const hasField = Object.entries(tableDef.columns).some(([fieldName]) => (0, schema_js_1.camelToSnake)(fieldName) === dbColName);
|
|
845
854
|
if (!hasField) {
|
|
846
855
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} DROP COLUMN ${dialect.quoteIdentifier(dbColName)};`;
|
|
847
|
-
const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}"
|
|
856
|
+
const reverseSql = `-- Cannot auto-reverse DROP COLUMN for "${dbColName}"; add it back manually`;
|
|
848
857
|
alterDef.columns.push({ column: dbColName, action: 'drop', sql, reverseSql });
|
|
849
|
-
|
|
858
|
+
result.statements.push(sql);
|
|
850
859
|
}
|
|
851
860
|
}
|
|
852
861
|
if (alterDef.columns.length > 0) {
|