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/dist/cjs/seed.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseStackFramePath = parseStackFramePath;
|
|
3
4
|
exports.defineSeed = defineSeed;
|
|
4
5
|
const node_fs_1 = require("node:fs");
|
|
5
6
|
const node_path_1 = require("node:path");
|
|
@@ -7,29 +8,101 @@ const node_url_1 = require("node:url");
|
|
|
7
8
|
const client_js_1 = require("./client.js");
|
|
8
9
|
const errors_js_1 = require("./errors.js");
|
|
9
10
|
const emptySchema = { tables: {}, enums: {} };
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Extract the filesystem path from a single V8 stack-trace line, regardless of
|
|
13
|
+
* whether the frame is a `file://` URL (ESM), a bare absolute path (CJS / tsx),
|
|
14
|
+
* or a wrapped `(… )` location. The trailing `:line:col` (and any surrounding
|
|
15
|
+
* parens) are peeled from the END so a Windows drive colon or a URL scheme colon
|
|
16
|
+
* inside the path never confuses the match. Non-file frames (`node:internal/…`,
|
|
17
|
+
* `<anonymous>`) return null.
|
|
18
|
+
*
|
|
19
|
+
* Exported for unit testing the frame parser in isolation.
|
|
20
|
+
*/
|
|
21
|
+
function parseStackFramePath(line) {
|
|
22
|
+
// Peel the trailing `:line:col` (with any closing paren) from the END so a
|
|
23
|
+
// Windows drive colon or a `file://` scheme colon earlier in the path is never
|
|
24
|
+
// mistaken for the location separator.
|
|
25
|
+
const loc = line.match(/:(\d+):(\d+)\)?\s*$/);
|
|
26
|
+
if (!loc || loc.index === undefined)
|
|
13
27
|
return null;
|
|
28
|
+
let head = line.slice(0, loc.index);
|
|
29
|
+
const paren = head.lastIndexOf('(');
|
|
30
|
+
if (paren !== -1) {
|
|
31
|
+
// `at fn (PATH:line:col)`: the path is whatever the last "(" wraps.
|
|
32
|
+
head = head.slice(paren + 1);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// `at PATH:line:col`: drop the leading " at " prefix.
|
|
36
|
+
head = head.replace(/^\s*at\s+/, '');
|
|
37
|
+
}
|
|
38
|
+
head = head.trim();
|
|
39
|
+
if (head.startsWith('file://')) {
|
|
40
|
+
try {
|
|
41
|
+
return (0, node_url_1.fileURLToPath)(head);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Accept only absolute filesystem paths (POSIX `/…` or Windows `C:\…` / `C:/…`).
|
|
48
|
+
if (/^(\/|[A-Za-z]:[\\/])/.test(head))
|
|
49
|
+
return head;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/** Best-effort canonicalization so two spellings of the same file compare equal. */
|
|
53
|
+
function canonicalPath(p) {
|
|
14
54
|
try {
|
|
15
|
-
return (0,
|
|
55
|
+
return (0, node_fs_1.realpathSync)(p);
|
|
16
56
|
}
|
|
17
57
|
catch {
|
|
18
|
-
return (0,
|
|
58
|
+
return (0, node_path_1.resolve)(p);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The canonical path of THIS module's own file, captured once at load time from
|
|
63
|
+
* a fresh stack. It is used to skip the library's own frames when locating the
|
|
64
|
+
* caller. This is robust to the src (`seed.ts`) vs published dist (`seed.js`)
|
|
65
|
+
* basename difference AND to the fact that the user's own file is ALSO named
|
|
66
|
+
* `seed.ts`, which a basename skip-list would wrongly exclude. This fixes the
|
|
67
|
+
* silent no-op: previously the plain-path skip-list only knew `src/seed.ts`, so
|
|
68
|
+
* the library's own `dist/seed.js` frame (or a tsx plain-path frame) was
|
|
69
|
+
* mistaken for the caller and the entry===caller self-run check never passed.
|
|
70
|
+
*/
|
|
71
|
+
const SELF_PATH = (() => {
|
|
72
|
+
const stack = new Error().stack;
|
|
73
|
+
if (!stack)
|
|
74
|
+
return null;
|
|
75
|
+
// Frame [1] (after the "Error" header) is this IIFE, i.e. the current module.
|
|
76
|
+
for (const line of stack.split('\n').slice(1)) {
|
|
77
|
+
const p = parseStackFramePath(line);
|
|
78
|
+
if (p)
|
|
79
|
+
return canonicalPath(p);
|
|
19
80
|
}
|
|
81
|
+
return null;
|
|
82
|
+
})();
|
|
83
|
+
function entryUrl() {
|
|
84
|
+
const entry = process.argv[1];
|
|
85
|
+
if (!entry)
|
|
86
|
+
return null;
|
|
87
|
+
return (0, node_url_1.pathToFileURL)(canonicalPath(entry)).href;
|
|
20
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The first stack frame that is NOT part of this module, expressed as a
|
|
91
|
+
* canonical `file://` URL. That frame is whoever invoked `defineSeed`: the
|
|
92
|
+
* user's seed module when the file is run directly.
|
|
93
|
+
*/
|
|
21
94
|
function callerUrl() {
|
|
22
95
|
const stack = new Error().stack;
|
|
23
96
|
if (!stack)
|
|
24
97
|
return null;
|
|
25
98
|
for (const line of stack.split('\n').slice(2)) {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
99
|
+
const p = parseStackFramePath(line);
|
|
100
|
+
if (!p)
|
|
101
|
+
continue;
|
|
102
|
+
const canonical = canonicalPath(p);
|
|
103
|
+
if (SELF_PATH && canonical === SELF_PATH)
|
|
104
|
+
continue; // skip the library's own frames
|
|
105
|
+
return (0, node_url_1.pathToFileURL)(canonical).href;
|
|
33
106
|
}
|
|
34
107
|
return null;
|
|
35
108
|
}
|
|
@@ -38,6 +111,24 @@ function isDirectSeedModule() {
|
|
|
38
111
|
const caller = callerUrl();
|
|
39
112
|
return process.env.NODE_TEST_CONTEXT === undefined && !!entry && !!caller && entry === caller;
|
|
40
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Signal, to a parent `turbine seed` process, that a defineSeed callback
|
|
116
|
+
* actually executed to completion. The CLI sets `TURBINE_SEED_SENTINEL` to a
|
|
117
|
+
* temp path before spawning the seed; if the file never appears the CLI knows
|
|
118
|
+
* the seed module loaded but no callback ran, and reports that as a failure
|
|
119
|
+
* instead of a false "Seed completed".
|
|
120
|
+
*/
|
|
121
|
+
function markSeedRan() {
|
|
122
|
+
const sentinel = process.env.TURBINE_SEED_SENTINEL;
|
|
123
|
+
if (!sentinel)
|
|
124
|
+
return;
|
|
125
|
+
try {
|
|
126
|
+
(0, node_fs_1.writeFileSync)(sentinel, 'ran');
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Best-effort only: an unwritable sentinel must never fail a good seed run.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
41
132
|
async function runSeed(fn) {
|
|
42
133
|
const connectionString = process.env.DATABASE_URL;
|
|
43
134
|
if (!connectionString) {
|
|
@@ -46,6 +137,7 @@ async function runSeed(fn) {
|
|
|
46
137
|
const db = new client_js_1.TurbineClient({ connectionString }, emptySchema);
|
|
47
138
|
try {
|
|
48
139
|
await fn(db);
|
|
140
|
+
markSeedRan();
|
|
49
141
|
}
|
|
50
142
|
finally {
|
|
51
143
|
await db.disconnect();
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -189,8 +189,8 @@ export interface InitPlanFlags {
|
|
|
189
189
|
* skipped when there is no URL or the database is unreachable.
|
|
190
190
|
*/
|
|
191
191
|
export declare function planInitSteps(state: InitPlanState, flags: InitPlanFlags): InitPlanStep[];
|
|
192
|
-
export declare function buildMigrateDeployOptions(
|
|
193
|
-
allowDrift:
|
|
192
|
+
export declare function buildMigrateDeployOptions(args: CliArgs): {
|
|
193
|
+
allowDrift: boolean;
|
|
194
194
|
allowDestructive: true;
|
|
195
195
|
step: undefined;
|
|
196
196
|
};
|
package/dist/cli/index.js
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
* npx turbine init --url postgres://...
|
|
24
24
|
* npx turbine migrate create add_users_table
|
|
25
25
|
*/
|
|
26
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
27
|
+
import { tmpdir } from 'node:os';
|
|
27
28
|
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
28
29
|
import { pathToFileURL } from 'node:url';
|
|
29
30
|
import { generate } from '../generate.js';
|
|
@@ -31,9 +32,10 @@ import { findMissingRelationIndexes } from '../index-advisor.js';
|
|
|
31
32
|
import { introspect } from '../introspect.js';
|
|
32
33
|
import { DestructivePushRefusal, schemaDiff, schemaPush } from '../schema-sql.js';
|
|
33
34
|
import { configTemplate, findConfigFile, loadConfigResult, looksLikeSchemaFilePath, resolveConfig, resolveSeedFile, unwrapModuleDefault, } from './config.js';
|
|
35
|
+
import { DESTRUCTIVE_KIND_LABEL } from './destructive.js';
|
|
34
36
|
import { canResolveTsx, getTsLoaderError, needsTsLoader, registerTsLoader } from './loader.js';
|
|
35
37
|
import { runMcpServer } from './mcp.js';
|
|
36
|
-
import { buildDiffMigrationBody, createMigration, inspectMigrationDeploy, listMigrationFiles, MIGRATION_RECIPES, migrateDeploy, migrateDown, migrateStatus, migrateUp, } from './migrate.js';
|
|
38
|
+
import { buildDiffMigrationBody, collectUpDestructive, createMigration, formatChecksumMismatchError, inspectMigrationDeploy, listMigrationFiles, MIGRATION_RECIPES, migrateDeploy, migrateDown, migrateStatus, migrateUp, } from './migrate.js';
|
|
37
39
|
import { startObserve } from './observe.js';
|
|
38
40
|
import { startStudio } from './studio.js';
|
|
39
41
|
import { banner, blue, bold, box, cyan, dim, divider, elapsed, error, table as formatTable, gray, green, header, info, label, magenta, newline, red, redactUrl, Spinner, success, symbols, warn, yellow, } from './ui.js';
|
|
@@ -1086,12 +1088,13 @@ async function cmdMigrate(args, config) {
|
|
|
1086
1088
|
console.log(` ${cyan('status')} Show migration status`);
|
|
1087
1089
|
newline();
|
|
1088
1090
|
console.log(` ${bold('Options:')}`);
|
|
1089
|
-
console.log(` ${cyan('--auto')} Auto-generate UP/DOWN SQL from schema diff`);
|
|
1090
|
-
console.log(` ${cyan('--from-diff')}
|
|
1091
|
+
console.log(` ${cyan('--auto')} Auto-generate UP/DOWN SQL from schema diff (destructive flagged)`);
|
|
1092
|
+
console.log(` ${cyan('--from-diff')} Generate from schema diff, destructive statements flagged inline`);
|
|
1091
1093
|
console.log(` ${cyan('--recipe <name>')} Scaffold a sanctioned migration pattern`);
|
|
1092
1094
|
console.log(` ${cyan('--step, -n')} Number of migrations to apply/rollback`);
|
|
1093
1095
|
console.log(` ${cyan('--dry-run')} Show SQL without executing`);
|
|
1094
|
-
console.log(` ${cyan('--allow-
|
|
1096
|
+
console.log(` ${cyan('--allow-destructive')} Run data-destroying statements without prompting`);
|
|
1097
|
+
console.log(` ${cyan('--allow-drift')} Bypass checksum validation on ${cyan('up')} / ${cyan('deploy')} ${dim('(advanced)')}`);
|
|
1095
1098
|
newline();
|
|
1096
1099
|
console.log(` ${bold('Recipes')} ${dim('(--recipe):')}`);
|
|
1097
1100
|
for (const [key, recipe] of Object.entries(MIGRATION_RECIPES)) {
|
|
@@ -1174,9 +1177,13 @@ async function cmdMigrateCreate(args, config) {
|
|
|
1174
1177
|
}
|
|
1175
1178
|
diffSpinner.succeed(`Found ${bold(String(diff.statements.length))} change(s)`);
|
|
1176
1179
|
newline();
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
+
// Route through buildDiffMigrationBody so any destructive statement (a lossy
|
|
1181
|
+
// ALTER COLUMN ... TYPE, or a DROP COLUMN for a column removed from the
|
|
1182
|
+
// schema) is flagged inline in the file, matching --from-diff. A
|
|
1183
|
+
// destructive-only diff therefore produces a real, flagged migration instead
|
|
1184
|
+
// of the old "already in sync" false negative.
|
|
1185
|
+
const body = buildDiffMigrationBody(diff);
|
|
1186
|
+
const file = createMigration(config.migrationsDir, name, { up: body.up, down: body.down });
|
|
1180
1187
|
const relPath = relative(process.cwd(), file.path);
|
|
1181
1188
|
success(`Created auto-migration: ${bold(file.filename)}`);
|
|
1182
1189
|
newline();
|
|
@@ -1204,6 +1211,27 @@ async function cmdMigrateCreate(args, config) {
|
|
|
1204
1211
|
}
|
|
1205
1212
|
}
|
|
1206
1213
|
newline();
|
|
1214
|
+
// Loudly flag any destructive statements written into the file (same as
|
|
1215
|
+
// --from-diff). They stay intact so `migrate up` still refuses them by
|
|
1216
|
+
// default, but the operator must know they are there before running.
|
|
1217
|
+
const destructiveCount = body.destructiveUp.length + body.destructiveDown.length;
|
|
1218
|
+
if (destructiveCount > 0) {
|
|
1219
|
+
warn(`This migration contains ${bold(String(destructiveCount))} DESTRUCTIVE statement(s), flagged in the file.`);
|
|
1220
|
+
for (const h of body.destructiveUp) {
|
|
1221
|
+
console.log(` ${red(symbols.warning)} ${dim('UP')} [${h.kind}] ${h.target}`);
|
|
1222
|
+
}
|
|
1223
|
+
for (const h of body.destructiveDown) {
|
|
1224
|
+
console.log(` ${red(symbols.warning)} ${dim('DOWN')} [${h.kind}] ${h.target}`);
|
|
1225
|
+
}
|
|
1226
|
+
newline();
|
|
1227
|
+
console.log(` ${dim('`migrate up` refuses destructive statements by default: confirm interactively or pass')} ${cyan('--allow-destructive')}${dim('.')}`);
|
|
1228
|
+
newline();
|
|
1229
|
+
}
|
|
1230
|
+
if (diff.warnings && diff.warnings.length > 0) {
|
|
1231
|
+
for (const w of diff.warnings)
|
|
1232
|
+
warn(w);
|
|
1233
|
+
newline();
|
|
1234
|
+
}
|
|
1207
1235
|
console.log(` ${dim('Review the migration, then run:')}`);
|
|
1208
1236
|
console.log(` ${cyan('npx turbine migrate up')}`);
|
|
1209
1237
|
newline();
|
|
@@ -1374,6 +1402,7 @@ async function cmdMigrateUp(args, config) {
|
|
|
1374
1402
|
console.log(` ${green(symbols.check)} ${file.filename}`);
|
|
1375
1403
|
}
|
|
1376
1404
|
}
|
|
1405
|
+
warnOutOfOrder(result.outOfOrder);
|
|
1377
1406
|
if (result.errors.length > 0) {
|
|
1378
1407
|
spinner.fail('Migration failed');
|
|
1379
1408
|
for (const { file, error: msg } of result.errors) {
|
|
@@ -1385,58 +1414,101 @@ async function cmdMigrateUp(args, config) {
|
|
|
1385
1414
|
}
|
|
1386
1415
|
newline();
|
|
1387
1416
|
}
|
|
1388
|
-
|
|
1417
|
+
/** Print a one-line warning for each migration applied out of timestamp order. */
|
|
1418
|
+
function warnOutOfOrder(outOfOrder) {
|
|
1419
|
+
if (outOfOrder.length === 0)
|
|
1420
|
+
return;
|
|
1421
|
+
newline();
|
|
1422
|
+
for (const o of outOfOrder) {
|
|
1423
|
+
warn(`Applied ${bold(o.applied)} out of order (older than already-applied ${bold(o.newestPrior)}).`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
export function buildMigrateDeployOptions(args) {
|
|
1389
1427
|
return {
|
|
1390
|
-
allowDrift:
|
|
1428
|
+
allowDrift: args.allowDrift === true,
|
|
1391
1429
|
allowDestructive: true,
|
|
1392
1430
|
step: undefined,
|
|
1393
1431
|
};
|
|
1394
1432
|
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Print the itemized, classified destructive-statement report as a NOTICE.
|
|
1435
|
+
* `deploy` proceeds by design (the gate ran at author time), but it must not run
|
|
1436
|
+
* data-destroying SQL in total silence; the notice ends that zero-ceremony hole.
|
|
1437
|
+
*/
|
|
1438
|
+
function printDestructiveNotice(offenders) {
|
|
1439
|
+
warn('NOTICE: this deploy runs DESTRUCTIVE statement(s):');
|
|
1440
|
+
for (const o of offenders) {
|
|
1441
|
+
console.log(` ${red(symbols.warning)} ${o.file}`);
|
|
1442
|
+
for (const h of o.hits) {
|
|
1443
|
+
console.log(` ${dim('-')} [${h.kind}] ${h.target} ${dim(DESTRUCTIVE_KIND_LABEL[h.kind])}`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
newline();
|
|
1447
|
+
}
|
|
1395
1448
|
async function cmdMigrateDeploy(args, config) {
|
|
1396
1449
|
banner();
|
|
1397
1450
|
const url = requireUrl(config);
|
|
1398
1451
|
label('Database', redactUrl(url));
|
|
1399
1452
|
label('Migrations', config.migrationsDir);
|
|
1400
1453
|
newline();
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1454
|
+
const spinner = new Spinner('Checking pending migrations').start();
|
|
1455
|
+
const plan = await inspectMigrationDeploy(url, config.migrationsDir);
|
|
1456
|
+
spinner.stop();
|
|
1457
|
+
// Drift handling: honor --allow-drift exactly like `up`. Without it, block;
|
|
1458
|
+
// with it, warn loudly and proceed.
|
|
1459
|
+
if (plan.mismatches.length > 0) {
|
|
1460
|
+
if (!args.allowDrift) {
|
|
1461
|
+
error('Deploy blocked by migration drift');
|
|
1462
|
+
newline();
|
|
1463
|
+
for (const line of formatChecksumMismatchError(plan.mismatches).split('\n')) {
|
|
1464
|
+
console.log(` ${line.replace('[turbine] ', '')}`);
|
|
1409
1465
|
}
|
|
1410
1466
|
newline();
|
|
1411
1467
|
process.exit(1);
|
|
1412
1468
|
}
|
|
1469
|
+
warn('--allow-drift is set: checksum validation is DISABLED for this deploy.');
|
|
1470
|
+
console.log(` ${dim('Applied migrations may have been modified or deleted on disk.')}`);
|
|
1471
|
+
newline();
|
|
1472
|
+
}
|
|
1473
|
+
if (args.dryRun) {
|
|
1413
1474
|
if (plan.pending.length === 0) {
|
|
1414
|
-
|
|
1475
|
+
info('No pending migrations');
|
|
1415
1476
|
newline();
|
|
1416
1477
|
return;
|
|
1417
1478
|
}
|
|
1418
|
-
|
|
1479
|
+
info(`${bold(String(plan.pending.length))} pending migration(s)`);
|
|
1419
1480
|
for (const file of plan.pending) {
|
|
1420
1481
|
console.log(` ${yellow(symbols.dot)} ${file.filename}`);
|
|
1421
1482
|
}
|
|
1483
|
+
// Surface destructive statements even in a dry run so CI can see them.
|
|
1484
|
+
const destructive = collectUpDestructive(plan.pending);
|
|
1485
|
+
if (destructive.length > 0) {
|
|
1486
|
+
newline();
|
|
1487
|
+
printDestructiveNotice(destructive);
|
|
1488
|
+
}
|
|
1422
1489
|
newline();
|
|
1423
1490
|
return;
|
|
1424
1491
|
}
|
|
1425
|
-
|
|
1426
|
-
const
|
|
1492
|
+
// Destructive notice before applying (deploy still proceeds by design).
|
|
1493
|
+
const destructive = collectUpDestructive(plan.pending);
|
|
1494
|
+
if (destructive.length > 0)
|
|
1495
|
+
printDestructiveNotice(destructive);
|
|
1496
|
+
const runSpinner = new Spinner('Deploying migrations').start();
|
|
1497
|
+
const result = await migrateDeploy(url, config.migrationsDir, { allowDrift: args.allowDrift });
|
|
1427
1498
|
if (result.applied.length === 0 && result.errors.length === 0) {
|
|
1428
|
-
|
|
1499
|
+
runSpinner.succeed('0 applied, all migrations are up to date');
|
|
1429
1500
|
newline();
|
|
1430
1501
|
return;
|
|
1431
1502
|
}
|
|
1432
1503
|
if (result.applied.length > 0) {
|
|
1433
|
-
|
|
1504
|
+
runSpinner.succeed(`${bold(String(result.applied.length))} applied`);
|
|
1434
1505
|
for (const file of result.applied) {
|
|
1435
1506
|
console.log(` ${green(symbols.check)} ${file.filename}`);
|
|
1436
1507
|
}
|
|
1437
1508
|
}
|
|
1509
|
+
warnOutOfOrder(result.outOfOrder);
|
|
1438
1510
|
if (result.errors.length > 0) {
|
|
1439
|
-
|
|
1511
|
+
runSpinner.fail('Deploy failed');
|
|
1440
1512
|
for (const { file, error: msg } of result.errors) {
|
|
1441
1513
|
console.log(` ${red(symbols.cross)} ${file.filename}`);
|
|
1442
1514
|
console.log(` ${dim(msg)}`);
|
|
@@ -1553,8 +1625,17 @@ async function cmdMigrateStatus(_args, config) {
|
|
|
1553
1625
|
const pendingCount = statuses.filter((s) => !s.applied).length;
|
|
1554
1626
|
info(`${bold(String(appliedCount))} applied, ${pendingCount > 0 ? yellow(bold(String(pendingCount))) : bold(String(pendingCount))} pending`);
|
|
1555
1627
|
newline();
|
|
1556
|
-
//
|
|
1557
|
-
|
|
1628
|
+
// Applied migrations whose file was deleted from disk (distinct from an
|
|
1629
|
+
// on-disk edit). Counted in "applied" above; surfaced with their own banner.
|
|
1630
|
+
const missingCount = statuses.filter((s) => s.missingFile).length;
|
|
1631
|
+
if (missingCount > 0) {
|
|
1632
|
+
warn(`${bold(String(missingCount))} applied migration(s) are missing from disk!`);
|
|
1633
|
+
console.log(` ${dim('The history table records them, but their .sql file is gone.')}`);
|
|
1634
|
+
console.log(` ${dim('Restore the file(s) before running')} ${cyan('migrate up')} ${dim('or')} ${cyan('migrate deploy')}${dim('.')}`);
|
|
1635
|
+
newline();
|
|
1636
|
+
}
|
|
1637
|
+
// Check for checksum mismatches (on-disk edits only; missing files above).
|
|
1638
|
+
const driftCount = statuses.filter((s) => s.checksumValid === false && !s.missingFile).length;
|
|
1558
1639
|
if (driftCount > 0) {
|
|
1559
1640
|
warn(`${bold(String(driftCount))} migration(s) have been modified after application!`);
|
|
1560
1641
|
console.log(` ${dim('Applied migrations should be immutable. Modifying them can cause drift.')}`);
|
|
@@ -1564,7 +1645,10 @@ async function cmdMigrateStatus(_args, config) {
|
|
|
1564
1645
|
const headers = ['Status', 'Migration', 'Applied at'];
|
|
1565
1646
|
const rows = statuses.map((s) => {
|
|
1566
1647
|
let status;
|
|
1567
|
-
if (s.
|
|
1648
|
+
if (s.missingFile) {
|
|
1649
|
+
status = red(`${symbols.warning} Missing file`);
|
|
1650
|
+
}
|
|
1651
|
+
else if (s.applied && s.checksumValid === false) {
|
|
1568
1652
|
status = red(`${symbols.warning} Drifted`);
|
|
1569
1653
|
}
|
|
1570
1654
|
else if (s.applied) {
|
|
@@ -1613,21 +1697,40 @@ async function runSeedPlan(plan, config) {
|
|
|
1613
1697
|
if (!canResolveTsx()) {
|
|
1614
1698
|
throw new Error('TypeScript seed files require tsx — install tsx or use seed.js/seed.sql.');
|
|
1615
1699
|
}
|
|
1700
|
+
// The seed runs in a child process, so we cannot observe its callback
|
|
1701
|
+
// directly. Hand it a sentinel path: `defineSeed`'s runner writes the file
|
|
1702
|
+
// only after a callback executes to completion. If the child exits cleanly
|
|
1703
|
+
// but the sentinel never appears, the seed module loaded without running
|
|
1704
|
+
// anything: a silent no-op we must report as a failure, not success.
|
|
1705
|
+
const sentinelDir = mkdtempSync(join(tmpdir(), 'turbine-seed-'));
|
|
1706
|
+
const sentinel = join(sentinelDir, 'ran');
|
|
1616
1707
|
const { execFileSync } = await import('node:child_process');
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1708
|
+
try {
|
|
1709
|
+
execFileSync(plan.command, plan.args, {
|
|
1710
|
+
stdio: 'inherit',
|
|
1711
|
+
env: {
|
|
1712
|
+
...process.env,
|
|
1713
|
+
DATABASE_URL: config.url || process.env.DATABASE_URL,
|
|
1714
|
+
TURBINE_SEED_SENTINEL: sentinel,
|
|
1715
|
+
},
|
|
1716
|
+
});
|
|
1717
|
+
if (!existsSync(sentinel)) {
|
|
1718
|
+
throw new Error('The seed file completed without running a seed callback. ' +
|
|
1719
|
+
'Make sure it calls defineSeed(async (db) => { ... }) at the top level.');
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
finally {
|
|
1723
|
+
rmSync(sentinelDir, { recursive: true, force: true });
|
|
1724
|
+
}
|
|
1624
1725
|
return;
|
|
1625
1726
|
}
|
|
1626
1727
|
if (plan.kind === 'js') {
|
|
1627
1728
|
const mod = await import(pathToFileURL(plan.file).href);
|
|
1628
|
-
if (typeof mod.default
|
|
1629
|
-
|
|
1729
|
+
if (typeof mod.default !== 'function') {
|
|
1730
|
+
throw new Error('The seed file has no callable default export. ' +
|
|
1731
|
+
'Export your seed with `export default defineSeed(async (db) => { ... })`.');
|
|
1630
1732
|
}
|
|
1733
|
+
await mod.default();
|
|
1631
1734
|
return;
|
|
1632
1735
|
}
|
|
1633
1736
|
const url = requireUrl(config);
|
|
@@ -2220,8 +2323,18 @@ function showHelp() {
|
|
|
2220
2323
|
newline();
|
|
2221
2324
|
console.log(` ${bold('Migrate options:')}`);
|
|
2222
2325
|
console.log(` ${cyan('--auto')} Auto-generate UP/DOWN SQL from schema diff ${dim('(create)')}`);
|
|
2326
|
+
console.log(` ${cyan('--from-diff')} Generate from schema diff, destructive statements flagged ${dim('(create)')}`);
|
|
2327
|
+
console.log(` ${cyan('--recipe')} ${dim('<name>')} Scaffold a named migration recipe, e.g. backfill ${dim('(create)')}`);
|
|
2223
2328
|
console.log(` ${cyan('--step, -n')} ${dim('<N>')} Number of migrations to apply/rollback`);
|
|
2224
|
-
console.log(` ${cyan('--allow-
|
|
2329
|
+
console.log(` ${cyan('--allow-destructive')} Run data-destroying statements without prompting ${dim('(up/down/push)')}`);
|
|
2330
|
+
console.log(` ${cyan('--allow-drift')} Bypass checksum validation on ${cyan('migrate up')} / ${cyan('deploy')} ${dim('(advanced)')}`);
|
|
2331
|
+
newline();
|
|
2332
|
+
console.log(` ${bold('Init options:')}`);
|
|
2333
|
+
console.log(` ${cyan('--yes, -y')} Accept every step's default (non-interactive)`);
|
|
2334
|
+
console.log(` ${cyan('--skip-schema')} Don't scaffold the schema file`);
|
|
2335
|
+
console.log(` ${cyan('--skip-seed')} Don't scaffold or run the seed file`);
|
|
2336
|
+
console.log(` ${cyan('--skip-push')} Don't offer to push the schema to the database`);
|
|
2337
|
+
console.log(` ${cyan('--skip-generate')} Don't offer to generate the typed client`);
|
|
2225
2338
|
newline();
|
|
2226
2339
|
console.log(` ${bold('Studio / observe options:')}`);
|
|
2227
2340
|
console.log(` ${cyan('--port')} ${dim('<n>')} HTTP port ${dim('(default: 4983 studio, 4984 observe)')}`);
|
package/dist/cli/migrate.d.ts
CHANGED
|
@@ -36,7 +36,43 @@ export interface MigrationStatus {
|
|
|
36
36
|
appliedAt?: Date;
|
|
37
37
|
/** True if the file checksum matches the stored checksum (only set for applied migrations) */
|
|
38
38
|
checksumValid?: boolean;
|
|
39
|
+
/** True when the migration was applied but its file is missing from disk. */
|
|
40
|
+
missingFile?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** A pending migration whose UP section contains data-destroying statements. */
|
|
43
|
+
export interface DestructiveOffender {
|
|
44
|
+
file: string;
|
|
45
|
+
hits: DestructiveStatement[];
|
|
39
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A migration that was applied even though an already-applied migration carries
|
|
49
|
+
* a newer timestamp prefix, i.e. history was written out of order.
|
|
50
|
+
*/
|
|
51
|
+
export interface OutOfOrderApply {
|
|
52
|
+
/** The out-of-order migration that was just applied. */
|
|
53
|
+
applied: string;
|
|
54
|
+
/** The newest previously-applied migration it landed behind. */
|
|
55
|
+
newestPrior: string;
|
|
56
|
+
}
|
|
57
|
+
export interface MigrationRunResult {
|
|
58
|
+
applied: MigrationFile[];
|
|
59
|
+
errors: Array<{
|
|
60
|
+
file: MigrationFile;
|
|
61
|
+
error: string;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Destructive statements found in the pending batch. Populated whether or not
|
|
65
|
+
* the run was allowed to proceed, so a caller (deploy) can print a notice even
|
|
66
|
+
* when it applies them by design.
|
|
67
|
+
*/
|
|
68
|
+
destructive: DestructiveOffender[];
|
|
69
|
+
/** Migrations applied with a timestamp older than an already-applied one. */
|
|
70
|
+
outOfOrder: OutOfOrderApply[];
|
|
71
|
+
}
|
|
72
|
+
/** Extract the YYYYMMDDHHMMSS timestamp prefix from a migration name, or null. */
|
|
73
|
+
export declare function migrationTimestamp(name: string): string | null;
|
|
74
|
+
/** Scan a set of migration files' UP sections for data-destroying statements. */
|
|
75
|
+
export declare function collectUpDestructive(files: MigrationFile[]): DestructiveOffender[];
|
|
40
76
|
/**
|
|
41
77
|
* Parse a migration filename into its components.
|
|
42
78
|
* Expected format: YYYYMMDDHHMMSS_description.sql
|
|
@@ -164,6 +200,7 @@ export interface MigrationDeployPlan {
|
|
|
164
200
|
pending: MigrationFile[];
|
|
165
201
|
mismatches: ChecksumMismatch[];
|
|
166
202
|
}
|
|
203
|
+
export declare function formatChecksumMismatchError(mismatches: ChecksumMismatch[]): string;
|
|
167
204
|
/**
|
|
168
205
|
* Build a deploy plan from local migration files and applied migration rows.
|
|
169
206
|
* This is pure file-system planning; callers with a database connection should
|
|
@@ -198,13 +235,7 @@ export declare function migrateUp(connectionString: string, migrationsDir: strin
|
|
|
198
235
|
allowDestructive?: boolean;
|
|
199
236
|
adapter?: DatabaseAdapter;
|
|
200
237
|
dialect?: Dialect;
|
|
201
|
-
}): Promise<
|
|
202
|
-
applied: MigrationFile[];
|
|
203
|
-
errors: Array<{
|
|
204
|
-
file: MigrationFile;
|
|
205
|
-
error: string;
|
|
206
|
-
}>;
|
|
207
|
-
}>;
|
|
238
|
+
}): Promise<MigrationRunResult>;
|
|
208
239
|
/**
|
|
209
240
|
* Production migration apply. This intentionally applies files as written and
|
|
210
241
|
* never performs interactive destructive confirmation.
|
|
@@ -212,13 +243,8 @@ export declare function migrateUp(connectionString: string, migrationsDir: strin
|
|
|
212
243
|
export declare function migrateDeploy(connectionString: string, migrationsDir: string, options?: {
|
|
213
244
|
adapter?: DatabaseAdapter;
|
|
214
245
|
dialect?: Dialect;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
errors: Array<{
|
|
218
|
-
file: MigrationFile;
|
|
219
|
-
error: string;
|
|
220
|
-
}>;
|
|
221
|
-
}>;
|
|
246
|
+
allowDrift?: boolean;
|
|
247
|
+
}): Promise<MigrationRunResult>;
|
|
222
248
|
/**
|
|
223
249
|
* Rollback the last N migrations (DOWN).
|
|
224
250
|
*
|