turbine-orm 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cjs/cli/destructive.js +123 -0
- package/dist/cjs/cli/index.js +92 -7
- package/dist/cjs/cli/migrate.js +60 -0
- package/dist/cli/destructive.d.ts +32 -0
- package/dist/cli/destructive.js +119 -0
- package/dist/cli/index.js +92 -7
- package/dist/cli/migrate.d.ts +3 -0
- package/dist/cli/migrate.js +60 -0
- package/dist/cli/ui.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -661,6 +661,12 @@ npx turbine migrate down
|
|
|
661
661
|
npx turbine migrate status
|
|
662
662
|
```
|
|
663
663
|
|
|
664
|
+
**Destructive migrations require explicit confirmation.** If a pending migration (or a DOWN
|
|
665
|
+
section being rolled back) contains data-destroying SQL — `DROP TABLE`, `DROP COLUMN`,
|
|
666
|
+
`TRUNCATE`, `DELETE FROM`, `UPDATE` without `WHERE`, `ALTER COLUMN … TYPE` — Turbine refuses
|
|
667
|
+
to run it and prints an itemized report. Interactively you must type `destroy my data` and
|
|
668
|
+
then `yes`; in CI you must pass `--allow-destructive`. A refused batch applies nothing.
|
|
669
|
+
|
|
664
670
|
## Studio
|
|
665
671
|
|
|
666
672
|
The only Postgres ORM with a Studio your DBA will approve. `turbine studio` launches a local, read-only web UI for exploring your database — no mutations, no writes, and since v0.19 **no raw-SQL surface at all**: every query is composed visually in the ORM and compiled by the same validated query builder your application uses.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Destructive-migration detection.
|
|
4
|
+
*
|
|
5
|
+
* `migrate up`/`down` execute user-authored SQL files verbatim, which is the
|
|
6
|
+
* one place data loss can hide: a `DROP TABLE` or a `DELETE FROM` in a
|
|
7
|
+
* migration runs with no ceremony. This module scans migration SQL for
|
|
8
|
+
* statements that can destroy data so the CLI can force an explicit,
|
|
9
|
+
* interactive confirmation (and the programmatic API can refuse by default).
|
|
10
|
+
*
|
|
11
|
+
* Deliberately conservative in BOTH directions:
|
|
12
|
+
* - comments and string literals are stripped first, so `-- DROP TABLE foo`
|
|
13
|
+
* or `INSERT ... VALUES ('DROP TABLE x')` never false-positive;
|
|
14
|
+
* - anything that removes rows, columns, tables, or schemas — or rewrites a
|
|
15
|
+
* column's type (a potentially lossy cast) — is flagged. `DROP INDEX`,
|
|
16
|
+
* `DROP CONSTRAINT`, and `DROP TRIGGER` are NOT flagged (recreatable
|
|
17
|
+
* structures; no row data lost).
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.DESTRUCTIVE_KIND_LABEL = void 0;
|
|
21
|
+
exports.scanDestructiveSql = scanDestructiveSql;
|
|
22
|
+
/** Human explanation per kind, used in CLI output. */
|
|
23
|
+
exports.DESTRUCTIVE_KIND_LABEL = {
|
|
24
|
+
'drop-table': 'drops a table and ALL its rows',
|
|
25
|
+
'drop-schema': 'drops an entire schema',
|
|
26
|
+
'drop-column': 'drops a column and its data in every row',
|
|
27
|
+
truncate: 'deletes every row',
|
|
28
|
+
delete: 'deletes rows',
|
|
29
|
+
'update-without-where': 'rewrites every row (no WHERE clause)',
|
|
30
|
+
'alter-column-type': 'rewrites a column type (cast may truncate or fail)',
|
|
31
|
+
};
|
|
32
|
+
/** Strip -- line comments, C-style block comments, and quoted literals. */
|
|
33
|
+
function stripCommentsAndStrings(sql) {
|
|
34
|
+
let out = '';
|
|
35
|
+
let i = 0;
|
|
36
|
+
while (i < sql.length) {
|
|
37
|
+
const two = sql.slice(i, i + 2);
|
|
38
|
+
if (two === '--') {
|
|
39
|
+
const nl = sql.indexOf('\n', i);
|
|
40
|
+
i = nl === -1 ? sql.length : nl; // keep the newline
|
|
41
|
+
}
|
|
42
|
+
else if (two === '/*') {
|
|
43
|
+
const end = sql.indexOf('*/', i + 2);
|
|
44
|
+
i = end === -1 ? sql.length : end + 2;
|
|
45
|
+
out += ' ';
|
|
46
|
+
}
|
|
47
|
+
else if (sql[i] === "'") {
|
|
48
|
+
// single-quoted literal ('' escapes a quote)
|
|
49
|
+
let j = i + 1;
|
|
50
|
+
while (j < sql.length) {
|
|
51
|
+
if (sql[j] === "'" && sql[j + 1] === "'")
|
|
52
|
+
j += 2;
|
|
53
|
+
else if (sql[j] === "'")
|
|
54
|
+
break;
|
|
55
|
+
else
|
|
56
|
+
j++;
|
|
57
|
+
}
|
|
58
|
+
i = j + 1;
|
|
59
|
+
out += "''";
|
|
60
|
+
}
|
|
61
|
+
else if (sql[i] === '$' && /^\$[a-zA-Z_]*\$/.test(sql.slice(i))) {
|
|
62
|
+
// dollar-quoted literal ($$...$$ / $tag$...$tag$)
|
|
63
|
+
const tag = sql.slice(i).match(/^\$[a-zA-Z_]*\$/)?.[0] ?? '$$';
|
|
64
|
+
const end = sql.indexOf(tag, i + tag.length);
|
|
65
|
+
i = end === -1 ? sql.length : end + tag.length;
|
|
66
|
+
out += "''";
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
out += sql[i];
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** Unquote a "quoted" identifier for display. */
|
|
76
|
+
const ident = (raw) => (raw ?? '?').replace(/^"|"$/g, '');
|
|
77
|
+
const IDENT = String.raw `("[^"]+"|[a-zA-Z_][\w$]*)(\.("[^"]+"|[a-zA-Z_][\w$]*))?`;
|
|
78
|
+
/**
|
|
79
|
+
* Scan SQL (one file's worth; may contain many `;`-separated statements) and
|
|
80
|
+
* return every statement that can destroy data.
|
|
81
|
+
*/
|
|
82
|
+
function scanDestructiveSql(sql) {
|
|
83
|
+
const found = [];
|
|
84
|
+
const cleaned = stripCommentsAndStrings(sql);
|
|
85
|
+
for (const rawStmt of cleaned.split(';')) {
|
|
86
|
+
const stmt = rawStmt.trim();
|
|
87
|
+
if (!stmt)
|
|
88
|
+
continue;
|
|
89
|
+
const display = stmt.replace(/\s+/g, ' ');
|
|
90
|
+
let m;
|
|
91
|
+
if ((m = stmt.match(new RegExp(String.raw `^DROP\s+TABLE\s+(IF\s+EXISTS\s+)?${IDENT}`, 'i')))) {
|
|
92
|
+
found.push({
|
|
93
|
+
statement: display,
|
|
94
|
+
kind: 'drop-table',
|
|
95
|
+
target: ident(m[4] ? `${ident(m[2])}.${ident(m[4])}` : m[2]),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else if ((m = stmt.match(new RegExp(String.raw `^DROP\s+SCHEMA\s+(IF\s+EXISTS\s+)?${IDENT}`, 'i')))) {
|
|
99
|
+
found.push({ statement: display, kind: 'drop-schema', target: ident(m[2]) });
|
|
100
|
+
}
|
|
101
|
+
else if ((m = stmt.match(new RegExp(String.raw `^TRUNCATE\s+(TABLE\s+)?(ONLY\s+)?${IDENT}`, 'i')))) {
|
|
102
|
+
found.push({
|
|
103
|
+
statement: display,
|
|
104
|
+
kind: 'truncate',
|
|
105
|
+
target: ident(m[5] ? `${ident(m[3])}.${ident(m[5])}` : m[3]),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else if ((m = stmt.match(new RegExp(String.raw `^ALTER\s+TABLE\s+(IF\s+EXISTS\s+)?(ONLY\s+)?${IDENT}[\s\S]*?\bDROP\s+COLUMN\s+(IF\s+EXISTS\s+)?${IDENT}`, 'i')))) {
|
|
109
|
+
found.push({ statement: display, kind: 'drop-column', target: `${ident(m[3])}.${ident(m[7])}` });
|
|
110
|
+
}
|
|
111
|
+
else if ((m = stmt.match(new RegExp(String.raw `^ALTER\s+TABLE\s+(IF\s+EXISTS\s+)?(ONLY\s+)?${IDENT}[\s\S]*?\bALTER\s+(COLUMN\s+)?${IDENT}\s+(SET\s+DATA\s+)?TYPE\b`, 'i')))) {
|
|
112
|
+
found.push({ statement: display, kind: 'alter-column-type', target: `${ident(m[3])}.${ident(m[7])}` });
|
|
113
|
+
}
|
|
114
|
+
else if ((m = stmt.match(new RegExp(String.raw `^DELETE\s+FROM\s+(ONLY\s+)?${IDENT}`, 'i')))) {
|
|
115
|
+
found.push({ statement: display, kind: 'delete', target: ident(m[2]) });
|
|
116
|
+
}
|
|
117
|
+
else if ((m = stmt.match(new RegExp(String.raw `^UPDATE\s+(ONLY\s+)?${IDENT}\b`, 'i'))) &&
|
|
118
|
+
!/\bWHERE\b/i.test(stmt)) {
|
|
119
|
+
found.push({ statement: display, kind: 'update-without-where', target: ident(m[2]) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return found;
|
|
123
|
+
}
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -128,6 +128,9 @@ function parseArgs() {
|
|
|
128
128
|
case '--fix':
|
|
129
129
|
result.fix = true;
|
|
130
130
|
break;
|
|
131
|
+
case '--allow-destructive':
|
|
132
|
+
result.allowDestructive = true;
|
|
133
|
+
break;
|
|
131
134
|
case '--force':
|
|
132
135
|
case '-f':
|
|
133
136
|
result.force = true;
|
|
@@ -784,11 +787,35 @@ async function cmdMigrateUp(args, config) {
|
|
|
784
787
|
console.log(` ${(0, ui_js_1.dim)('Proceed only if you are intentionally rewriting migration history.')}`);
|
|
785
788
|
(0, ui_js_1.newline)();
|
|
786
789
|
}
|
|
790
|
+
if (args.allowDestructive) {
|
|
791
|
+
(0, ui_js_1.warn)('--allow-destructive is set — data-destroying statements in migrations WILL run.');
|
|
792
|
+
(0, ui_js_1.newline)();
|
|
793
|
+
}
|
|
787
794
|
const spinner = new ui_js_1.Spinner('Applying migrations').start();
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
795
|
+
let result;
|
|
796
|
+
try {
|
|
797
|
+
result = await (0, migrate_js_1.migrateUp)(url, config.migrationsDir, {
|
|
798
|
+
step: args.step,
|
|
799
|
+
allowDrift: args.allowDrift,
|
|
800
|
+
allowDestructive: args.allowDestructive,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
if (!isDestructiveRefusal(err))
|
|
805
|
+
throw err;
|
|
806
|
+
spinner.stop();
|
|
807
|
+
if (!(await confirmDestructive(err.message))) {
|
|
808
|
+
(0, ui_js_1.error)('Aborted — no migrations were applied and no data was touched.');
|
|
809
|
+
(0, ui_js_1.newline)();
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
spinner.start();
|
|
813
|
+
result = await (0, migrate_js_1.migrateUp)(url, config.migrationsDir, {
|
|
814
|
+
step: args.step,
|
|
815
|
+
allowDrift: args.allowDrift,
|
|
816
|
+
allowDestructive: true,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
792
819
|
if (result.applied.length === 0 && result.errors.length === 0) {
|
|
793
820
|
spinner.succeed('All migrations are up to date');
|
|
794
821
|
(0, ui_js_1.newline)();
|
|
@@ -811,6 +838,44 @@ async function cmdMigrateUp(args, config) {
|
|
|
811
838
|
}
|
|
812
839
|
(0, ui_js_1.newline)();
|
|
813
840
|
}
|
|
841
|
+
/** True when the error is migrate up/down's destructive-statement refusal. */
|
|
842
|
+
function isDestructiveRefusal(err) {
|
|
843
|
+
return err instanceof Error && err.message.includes('DESTRUCTIVE');
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Triple confirmation for destructive migrations:
|
|
847
|
+
* 1. show the full itemized report (statement kinds + targets),
|
|
848
|
+
* 2. require typing the literal phrase `destroy my data`,
|
|
849
|
+
* 3. require a final explicit `yes`.
|
|
850
|
+
* Non-interactive shells (CI, pipes) can never pass this — they must use the
|
|
851
|
+
* explicit `--allow-destructive` flag instead. Anything but exact answers aborts.
|
|
852
|
+
*/
|
|
853
|
+
async function confirmDestructive(report) {
|
|
854
|
+
(0, ui_js_1.newline)();
|
|
855
|
+
(0, ui_js_1.error)('DESTRUCTIVE MIGRATION DETECTED');
|
|
856
|
+
(0, ui_js_1.newline)();
|
|
857
|
+
for (const line of report.split('\n'))
|
|
858
|
+
console.log(` ${line.includes('[turbine]') ? line.replace('[turbine] ', '') : line}`);
|
|
859
|
+
(0, ui_js_1.newline)();
|
|
860
|
+
if (!process.stdin.isTTY) {
|
|
861
|
+
console.log(` ${(0, ui_js_1.dim)('Non-interactive shell: rerun with')} ${(0, ui_js_1.cyan)('--allow-destructive')} ${(0, ui_js_1.dim)('to proceed.')}`);
|
|
862
|
+
(0, ui_js_1.newline)();
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
const { createInterface } = await Promise.resolve().then(() => __importStar(require('node:readline/promises')));
|
|
866
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
867
|
+
try {
|
|
868
|
+
console.log(` ${(0, ui_js_1.yellow)('This will permanently destroy data. There is no undo.')}`);
|
|
869
|
+
const phrase = await rl.question(` Type ${(0, ui_js_1.bold)('destroy my data')} to continue, anything else to abort: `);
|
|
870
|
+
if (phrase.trim() !== 'destroy my data')
|
|
871
|
+
return false;
|
|
872
|
+
const finalAnswer = await rl.question(` Final confirmation — apply the destructive statements above? Type ${(0, ui_js_1.bold)('yes')}: `);
|
|
873
|
+
return finalAnswer.trim() === 'yes';
|
|
874
|
+
}
|
|
875
|
+
finally {
|
|
876
|
+
rl.close();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
814
879
|
async function cmdMigrateDown(args, config) {
|
|
815
880
|
(0, ui_js_1.banner)();
|
|
816
881
|
const url = requireUrl(config);
|
|
@@ -818,9 +883,28 @@ async function cmdMigrateDown(args, config) {
|
|
|
818
883
|
(0, ui_js_1.label)('Migrations', config.migrationsDir);
|
|
819
884
|
(0, ui_js_1.newline)();
|
|
820
885
|
const spinner = new ui_js_1.Spinner('Rolling back migration(s)').start();
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
886
|
+
let result;
|
|
887
|
+
try {
|
|
888
|
+
result = await (0, migrate_js_1.migrateDown)(url, config.migrationsDir, {
|
|
889
|
+
step: args.step ?? 1,
|
|
890
|
+
allowDestructive: args.allowDestructive,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
catch (err) {
|
|
894
|
+
if (!isDestructiveRefusal(err))
|
|
895
|
+
throw err;
|
|
896
|
+
spinner.stop();
|
|
897
|
+
if (!(await confirmDestructive(err.message))) {
|
|
898
|
+
(0, ui_js_1.error)('Aborted — nothing was rolled back and no data was touched.');
|
|
899
|
+
(0, ui_js_1.newline)();
|
|
900
|
+
process.exit(1);
|
|
901
|
+
}
|
|
902
|
+
spinner.start();
|
|
903
|
+
result = await (0, migrate_js_1.migrateDown)(url, config.migrationsDir, {
|
|
904
|
+
step: args.step ?? 1,
|
|
905
|
+
allowDestructive: true,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
824
908
|
if (result.rolledBack.length === 0 && result.errors.length === 0) {
|
|
825
909
|
spinner.succeed('No migrations to roll back');
|
|
826
910
|
(0, ui_js_1.newline)();
|
|
@@ -1308,6 +1392,7 @@ function showMigrateHelp() {
|
|
|
1308
1392
|
console.log(` ${(0, ui_js_1.cyan)('--step, -n')} ${(0, ui_js_1.dim)('<N>')} Number of migrations to apply/rollback`);
|
|
1309
1393
|
console.log(` ${(0, ui_js_1.cyan)('--dry-run')} Show SQL without executing`);
|
|
1310
1394
|
console.log(` ${(0, ui_js_1.cyan)('--allow-drift')} Bypass checksum validation ${(0, ui_js_1.dim)('(migrate up only — advanced)')}`);
|
|
1395
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-destructive')} Run data-destroying migration statements without the interactive confirm`);
|
|
1311
1396
|
console.log(` ${(0, ui_js_1.cyan)('--verbose, -v')} Show detailed output`);
|
|
1312
1397
|
(0, ui_js_1.newline)();
|
|
1313
1398
|
console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
|
package/dist/cjs/cli/migrate.js
CHANGED
|
@@ -35,6 +35,7 @@ const pg_1 = __importDefault(require("pg"));
|
|
|
35
35
|
const index_js_1 = require("../adapters/index.js");
|
|
36
36
|
const dialect_js_1 = require("../dialect.js");
|
|
37
37
|
const errors_js_1 = require("../errors.js");
|
|
38
|
+
const destructive_js_1 = require("./destructive.js");
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
39
40
|
// Tracking table management
|
|
40
41
|
// ---------------------------------------------------------------------------
|
|
@@ -368,6 +369,35 @@ async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
368
369
|
if (options?.step != null && options.step > 0) {
|
|
369
370
|
pending = pending.slice(0, options.step);
|
|
370
371
|
}
|
|
372
|
+
// Data-loss gate: refuse to run pending migrations containing destructive
|
|
373
|
+
// statements unless the caller has EXPLICITLY opted in. The CLI layers an
|
|
374
|
+
// interactive typed confirmation on top of this; programmatic callers must
|
|
375
|
+
// pass `allowDestructive: true`. Safe-by-default is the whole point — a
|
|
376
|
+
// DROP TABLE should never run just because a file exists.
|
|
377
|
+
if (!options?.allowDestructive) {
|
|
378
|
+
const offenders = [];
|
|
379
|
+
for (const file of pending) {
|
|
380
|
+
const { up } = parseMigrationSQL(file.path);
|
|
381
|
+
if (!up)
|
|
382
|
+
continue;
|
|
383
|
+
const hits = (0, destructive_js_1.scanDestructiveSql)(up);
|
|
384
|
+
if (hits.length > 0)
|
|
385
|
+
offenders.push({ file: file.filename, hits });
|
|
386
|
+
}
|
|
387
|
+
if (offenders.length > 0) {
|
|
388
|
+
const lines = ['[turbine] Refusing to apply migrations containing DESTRUCTIVE statements:', ''];
|
|
389
|
+
for (const o of offenders) {
|
|
390
|
+
lines.push(` ${o.file}`);
|
|
391
|
+
for (const h of o.hits) {
|
|
392
|
+
lines.push(` - [${h.kind}] ${h.target} — ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
lines.push('');
|
|
396
|
+
lines.push('Review the statements above. To proceed: run `npx turbine migrate up` interactively');
|
|
397
|
+
lines.push('and confirm, pass --allow-destructive, or set allowDestructive: true programmatically.');
|
|
398
|
+
throw new errors_js_1.MigrationError(lines.join('\n'));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
371
401
|
const results = [];
|
|
372
402
|
const errors = [];
|
|
373
403
|
for (const file of pending) {
|
|
@@ -435,6 +465,36 @@ async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
435
465
|
const fileMap = new Map(allFiles.map((f) => [f.name, f]));
|
|
436
466
|
// Reverse order — rollback most recent first
|
|
437
467
|
const toRollback = applied.reverse().slice(0, options?.step ?? 1);
|
|
468
|
+
// Same data-loss gate as migrateUp — DOWN sections routinely contain
|
|
469
|
+
// DROP TABLE (the legitimate reverse of a CREATE), which still destroys
|
|
470
|
+
// every row written since the migration ran. Explicit opt-in required.
|
|
471
|
+
if (!options?.allowDestructive) {
|
|
472
|
+
const offenders = [];
|
|
473
|
+
for (const migration of toRollback) {
|
|
474
|
+
const file = fileMap.get(migration.name);
|
|
475
|
+
if (!file)
|
|
476
|
+
continue;
|
|
477
|
+
const { down } = parseMigrationSQL(file.path);
|
|
478
|
+
if (!down)
|
|
479
|
+
continue;
|
|
480
|
+
const hits = (0, destructive_js_1.scanDestructiveSql)(down);
|
|
481
|
+
if (hits.length > 0)
|
|
482
|
+
offenders.push({ file: file.filename, hits });
|
|
483
|
+
}
|
|
484
|
+
if (offenders.length > 0) {
|
|
485
|
+
const lines = ['[turbine] Refusing to roll back migrations whose DOWN sections are DESTRUCTIVE:', ''];
|
|
486
|
+
for (const o of offenders) {
|
|
487
|
+
lines.push(` ${o.file}`);
|
|
488
|
+
for (const h of o.hits) {
|
|
489
|
+
lines.push(` - [${h.kind}] ${h.target} — ${destructive_js_1.DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
lines.push('');
|
|
493
|
+
lines.push('To proceed: run `npx turbine migrate down` interactively and confirm, pass');
|
|
494
|
+
lines.push('--allow-destructive, or set allowDestructive: true programmatically.');
|
|
495
|
+
throw new errors_js_1.MigrationError(lines.join('\n'));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
438
498
|
const results = [];
|
|
439
499
|
const errors = [];
|
|
440
500
|
for (const migration of toRollback) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destructive-migration detection.
|
|
3
|
+
*
|
|
4
|
+
* `migrate up`/`down` execute user-authored SQL files verbatim, which is the
|
|
5
|
+
* one place data loss can hide: a `DROP TABLE` or a `DELETE FROM` in a
|
|
6
|
+
* migration runs with no ceremony. This module scans migration SQL for
|
|
7
|
+
* statements that can destroy data so the CLI can force an explicit,
|
|
8
|
+
* interactive confirmation (and the programmatic API can refuse by default).
|
|
9
|
+
*
|
|
10
|
+
* Deliberately conservative in BOTH directions:
|
|
11
|
+
* - comments and string literals are stripped first, so `-- DROP TABLE foo`
|
|
12
|
+
* or `INSERT ... VALUES ('DROP TABLE x')` never false-positive;
|
|
13
|
+
* - anything that removes rows, columns, tables, or schemas — or rewrites a
|
|
14
|
+
* column's type (a potentially lossy cast) — is flagged. `DROP INDEX`,
|
|
15
|
+
* `DROP CONSTRAINT`, and `DROP TRIGGER` are NOT flagged (recreatable
|
|
16
|
+
* structures; no row data lost).
|
|
17
|
+
*/
|
|
18
|
+
export type DestructiveKind = 'drop-table' | 'drop-schema' | 'drop-column' | 'truncate' | 'delete' | 'update-without-where' | 'alter-column-type';
|
|
19
|
+
export interface DestructiveStatement {
|
|
20
|
+
/** The offending SQL statement (trimmed, possibly long — display truncated) */
|
|
21
|
+
statement: string;
|
|
22
|
+
kind: DestructiveKind;
|
|
23
|
+
/** Best-effort extracted object name (table, schema, or table.column) */
|
|
24
|
+
target: string;
|
|
25
|
+
}
|
|
26
|
+
/** Human explanation per kind, used in CLI output. */
|
|
27
|
+
export declare const DESTRUCTIVE_KIND_LABEL: Record<DestructiveKind, string>;
|
|
28
|
+
/**
|
|
29
|
+
* Scan SQL (one file's worth; may contain many `;`-separated statements) and
|
|
30
|
+
* return every statement that can destroy data.
|
|
31
|
+
*/
|
|
32
|
+
export declare function scanDestructiveSql(sql: string): DestructiveStatement[];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destructive-migration detection.
|
|
3
|
+
*
|
|
4
|
+
* `migrate up`/`down` execute user-authored SQL files verbatim, which is the
|
|
5
|
+
* one place data loss can hide: a `DROP TABLE` or a `DELETE FROM` in a
|
|
6
|
+
* migration runs with no ceremony. This module scans migration SQL for
|
|
7
|
+
* statements that can destroy data so the CLI can force an explicit,
|
|
8
|
+
* interactive confirmation (and the programmatic API can refuse by default).
|
|
9
|
+
*
|
|
10
|
+
* Deliberately conservative in BOTH directions:
|
|
11
|
+
* - comments and string literals are stripped first, so `-- DROP TABLE foo`
|
|
12
|
+
* or `INSERT ... VALUES ('DROP TABLE x')` never false-positive;
|
|
13
|
+
* - anything that removes rows, columns, tables, or schemas — or rewrites a
|
|
14
|
+
* column's type (a potentially lossy cast) — is flagged. `DROP INDEX`,
|
|
15
|
+
* `DROP CONSTRAINT`, and `DROP TRIGGER` are NOT flagged (recreatable
|
|
16
|
+
* structures; no row data lost).
|
|
17
|
+
*/
|
|
18
|
+
/** Human explanation per kind, used in CLI output. */
|
|
19
|
+
export const DESTRUCTIVE_KIND_LABEL = {
|
|
20
|
+
'drop-table': 'drops a table and ALL its rows',
|
|
21
|
+
'drop-schema': 'drops an entire schema',
|
|
22
|
+
'drop-column': 'drops a column and its data in every row',
|
|
23
|
+
truncate: 'deletes every row',
|
|
24
|
+
delete: 'deletes rows',
|
|
25
|
+
'update-without-where': 'rewrites every row (no WHERE clause)',
|
|
26
|
+
'alter-column-type': 'rewrites a column type (cast may truncate or fail)',
|
|
27
|
+
};
|
|
28
|
+
/** Strip -- line comments, C-style block comments, and quoted literals. */
|
|
29
|
+
function stripCommentsAndStrings(sql) {
|
|
30
|
+
let out = '';
|
|
31
|
+
let i = 0;
|
|
32
|
+
while (i < sql.length) {
|
|
33
|
+
const two = sql.slice(i, i + 2);
|
|
34
|
+
if (two === '--') {
|
|
35
|
+
const nl = sql.indexOf('\n', i);
|
|
36
|
+
i = nl === -1 ? sql.length : nl; // keep the newline
|
|
37
|
+
}
|
|
38
|
+
else if (two === '/*') {
|
|
39
|
+
const end = sql.indexOf('*/', i + 2);
|
|
40
|
+
i = end === -1 ? sql.length : end + 2;
|
|
41
|
+
out += ' ';
|
|
42
|
+
}
|
|
43
|
+
else if (sql[i] === "'") {
|
|
44
|
+
// single-quoted literal ('' escapes a quote)
|
|
45
|
+
let j = i + 1;
|
|
46
|
+
while (j < sql.length) {
|
|
47
|
+
if (sql[j] === "'" && sql[j + 1] === "'")
|
|
48
|
+
j += 2;
|
|
49
|
+
else if (sql[j] === "'")
|
|
50
|
+
break;
|
|
51
|
+
else
|
|
52
|
+
j++;
|
|
53
|
+
}
|
|
54
|
+
i = j + 1;
|
|
55
|
+
out += "''";
|
|
56
|
+
}
|
|
57
|
+
else if (sql[i] === '$' && /^\$[a-zA-Z_]*\$/.test(sql.slice(i))) {
|
|
58
|
+
// dollar-quoted literal ($$...$$ / $tag$...$tag$)
|
|
59
|
+
const tag = sql.slice(i).match(/^\$[a-zA-Z_]*\$/)?.[0] ?? '$$';
|
|
60
|
+
const end = sql.indexOf(tag, i + tag.length);
|
|
61
|
+
i = end === -1 ? sql.length : end + tag.length;
|
|
62
|
+
out += "''";
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
out += sql[i];
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** Unquote a "quoted" identifier for display. */
|
|
72
|
+
const ident = (raw) => (raw ?? '?').replace(/^"|"$/g, '');
|
|
73
|
+
const IDENT = String.raw `("[^"]+"|[a-zA-Z_][\w$]*)(\.("[^"]+"|[a-zA-Z_][\w$]*))?`;
|
|
74
|
+
/**
|
|
75
|
+
* Scan SQL (one file's worth; may contain many `;`-separated statements) and
|
|
76
|
+
* return every statement that can destroy data.
|
|
77
|
+
*/
|
|
78
|
+
export function scanDestructiveSql(sql) {
|
|
79
|
+
const found = [];
|
|
80
|
+
const cleaned = stripCommentsAndStrings(sql);
|
|
81
|
+
for (const rawStmt of cleaned.split(';')) {
|
|
82
|
+
const stmt = rawStmt.trim();
|
|
83
|
+
if (!stmt)
|
|
84
|
+
continue;
|
|
85
|
+
const display = stmt.replace(/\s+/g, ' ');
|
|
86
|
+
let m;
|
|
87
|
+
if ((m = stmt.match(new RegExp(String.raw `^DROP\s+TABLE\s+(IF\s+EXISTS\s+)?${IDENT}`, 'i')))) {
|
|
88
|
+
found.push({
|
|
89
|
+
statement: display,
|
|
90
|
+
kind: 'drop-table',
|
|
91
|
+
target: ident(m[4] ? `${ident(m[2])}.${ident(m[4])}` : m[2]),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if ((m = stmt.match(new RegExp(String.raw `^DROP\s+SCHEMA\s+(IF\s+EXISTS\s+)?${IDENT}`, 'i')))) {
|
|
95
|
+
found.push({ statement: display, kind: 'drop-schema', target: ident(m[2]) });
|
|
96
|
+
}
|
|
97
|
+
else if ((m = stmt.match(new RegExp(String.raw `^TRUNCATE\s+(TABLE\s+)?(ONLY\s+)?${IDENT}`, 'i')))) {
|
|
98
|
+
found.push({
|
|
99
|
+
statement: display,
|
|
100
|
+
kind: 'truncate',
|
|
101
|
+
target: ident(m[5] ? `${ident(m[3])}.${ident(m[5])}` : m[3]),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else if ((m = stmt.match(new RegExp(String.raw `^ALTER\s+TABLE\s+(IF\s+EXISTS\s+)?(ONLY\s+)?${IDENT}[\s\S]*?\bDROP\s+COLUMN\s+(IF\s+EXISTS\s+)?${IDENT}`, 'i')))) {
|
|
105
|
+
found.push({ statement: display, kind: 'drop-column', target: `${ident(m[3])}.${ident(m[7])}` });
|
|
106
|
+
}
|
|
107
|
+
else if ((m = stmt.match(new RegExp(String.raw `^ALTER\s+TABLE\s+(IF\s+EXISTS\s+)?(ONLY\s+)?${IDENT}[\s\S]*?\bALTER\s+(COLUMN\s+)?${IDENT}\s+(SET\s+DATA\s+)?TYPE\b`, 'i')))) {
|
|
108
|
+
found.push({ statement: display, kind: 'alter-column-type', target: `${ident(m[3])}.${ident(m[7])}` });
|
|
109
|
+
}
|
|
110
|
+
else if ((m = stmt.match(new RegExp(String.raw `^DELETE\s+FROM\s+(ONLY\s+)?${IDENT}`, 'i')))) {
|
|
111
|
+
found.push({ statement: display, kind: 'delete', target: ident(m[2]) });
|
|
112
|
+
}
|
|
113
|
+
else if ((m = stmt.match(new RegExp(String.raw `^UPDATE\s+(ONLY\s+)?${IDENT}\b`, 'i'))) &&
|
|
114
|
+
!/\bWHERE\b/i.test(stmt)) {
|
|
115
|
+
found.push({ statement: display, kind: 'update-without-where', target: ident(m[2]) });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return found;
|
|
119
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -93,6 +93,9 @@ function parseArgs() {
|
|
|
93
93
|
case '--fix':
|
|
94
94
|
result.fix = true;
|
|
95
95
|
break;
|
|
96
|
+
case '--allow-destructive':
|
|
97
|
+
result.allowDestructive = true;
|
|
98
|
+
break;
|
|
96
99
|
case '--force':
|
|
97
100
|
case '-f':
|
|
98
101
|
result.force = true;
|
|
@@ -749,11 +752,35 @@ async function cmdMigrateUp(args, config) {
|
|
|
749
752
|
console.log(` ${dim('Proceed only if you are intentionally rewriting migration history.')}`);
|
|
750
753
|
newline();
|
|
751
754
|
}
|
|
755
|
+
if (args.allowDestructive) {
|
|
756
|
+
warn('--allow-destructive is set — data-destroying statements in migrations WILL run.');
|
|
757
|
+
newline();
|
|
758
|
+
}
|
|
752
759
|
const spinner = new Spinner('Applying migrations').start();
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
760
|
+
let result;
|
|
761
|
+
try {
|
|
762
|
+
result = await migrateUp(url, config.migrationsDir, {
|
|
763
|
+
step: args.step,
|
|
764
|
+
allowDrift: args.allowDrift,
|
|
765
|
+
allowDestructive: args.allowDestructive,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
if (!isDestructiveRefusal(err))
|
|
770
|
+
throw err;
|
|
771
|
+
spinner.stop();
|
|
772
|
+
if (!(await confirmDestructive(err.message))) {
|
|
773
|
+
error('Aborted — no migrations were applied and no data was touched.');
|
|
774
|
+
newline();
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
spinner.start();
|
|
778
|
+
result = await migrateUp(url, config.migrationsDir, {
|
|
779
|
+
step: args.step,
|
|
780
|
+
allowDrift: args.allowDrift,
|
|
781
|
+
allowDestructive: true,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
757
784
|
if (result.applied.length === 0 && result.errors.length === 0) {
|
|
758
785
|
spinner.succeed('All migrations are up to date');
|
|
759
786
|
newline();
|
|
@@ -776,6 +803,44 @@ async function cmdMigrateUp(args, config) {
|
|
|
776
803
|
}
|
|
777
804
|
newline();
|
|
778
805
|
}
|
|
806
|
+
/** True when the error is migrate up/down's destructive-statement refusal. */
|
|
807
|
+
function isDestructiveRefusal(err) {
|
|
808
|
+
return err instanceof Error && err.message.includes('DESTRUCTIVE');
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Triple confirmation for destructive migrations:
|
|
812
|
+
* 1. show the full itemized report (statement kinds + targets),
|
|
813
|
+
* 2. require typing the literal phrase `destroy my data`,
|
|
814
|
+
* 3. require a final explicit `yes`.
|
|
815
|
+
* Non-interactive shells (CI, pipes) can never pass this — they must use the
|
|
816
|
+
* explicit `--allow-destructive` flag instead. Anything but exact answers aborts.
|
|
817
|
+
*/
|
|
818
|
+
async function confirmDestructive(report) {
|
|
819
|
+
newline();
|
|
820
|
+
error('DESTRUCTIVE MIGRATION DETECTED');
|
|
821
|
+
newline();
|
|
822
|
+
for (const line of report.split('\n'))
|
|
823
|
+
console.log(` ${line.includes('[turbine]') ? line.replace('[turbine] ', '') : line}`);
|
|
824
|
+
newline();
|
|
825
|
+
if (!process.stdin.isTTY) {
|
|
826
|
+
console.log(` ${dim('Non-interactive shell: rerun with')} ${cyan('--allow-destructive')} ${dim('to proceed.')}`);
|
|
827
|
+
newline();
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
const { createInterface } = await import('node:readline/promises');
|
|
831
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
832
|
+
try {
|
|
833
|
+
console.log(` ${yellow('This will permanently destroy data. There is no undo.')}`);
|
|
834
|
+
const phrase = await rl.question(` Type ${bold('destroy my data')} to continue, anything else to abort: `);
|
|
835
|
+
if (phrase.trim() !== 'destroy my data')
|
|
836
|
+
return false;
|
|
837
|
+
const finalAnswer = await rl.question(` Final confirmation — apply the destructive statements above? Type ${bold('yes')}: `);
|
|
838
|
+
return finalAnswer.trim() === 'yes';
|
|
839
|
+
}
|
|
840
|
+
finally {
|
|
841
|
+
rl.close();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
779
844
|
async function cmdMigrateDown(args, config) {
|
|
780
845
|
banner();
|
|
781
846
|
const url = requireUrl(config);
|
|
@@ -783,9 +848,28 @@ async function cmdMigrateDown(args, config) {
|
|
|
783
848
|
label('Migrations', config.migrationsDir);
|
|
784
849
|
newline();
|
|
785
850
|
const spinner = new Spinner('Rolling back migration(s)').start();
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
851
|
+
let result;
|
|
852
|
+
try {
|
|
853
|
+
result = await migrateDown(url, config.migrationsDir, {
|
|
854
|
+
step: args.step ?? 1,
|
|
855
|
+
allowDestructive: args.allowDestructive,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
catch (err) {
|
|
859
|
+
if (!isDestructiveRefusal(err))
|
|
860
|
+
throw err;
|
|
861
|
+
spinner.stop();
|
|
862
|
+
if (!(await confirmDestructive(err.message))) {
|
|
863
|
+
error('Aborted — nothing was rolled back and no data was touched.');
|
|
864
|
+
newline();
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
spinner.start();
|
|
868
|
+
result = await migrateDown(url, config.migrationsDir, {
|
|
869
|
+
step: args.step ?? 1,
|
|
870
|
+
allowDestructive: true,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
789
873
|
if (result.rolledBack.length === 0 && result.errors.length === 0) {
|
|
790
874
|
spinner.succeed('No migrations to roll back');
|
|
791
875
|
newline();
|
|
@@ -1273,6 +1357,7 @@ function showMigrateHelp() {
|
|
|
1273
1357
|
console.log(` ${cyan('--step, -n')} ${dim('<N>')} Number of migrations to apply/rollback`);
|
|
1274
1358
|
console.log(` ${cyan('--dry-run')} Show SQL without executing`);
|
|
1275
1359
|
console.log(` ${cyan('--allow-drift')} Bypass checksum validation ${dim('(migrate up only — advanced)')}`);
|
|
1360
|
+
console.log(` ${cyan('--allow-destructive')} Run data-destroying migration statements without the interactive confirm`);
|
|
1276
1361
|
console.log(` ${cyan('--verbose, -v')} Show detailed output`);
|
|
1277
1362
|
newline();
|
|
1278
1363
|
console.log(` ${bold('Examples:')}`);
|
package/dist/cli/migrate.d.ts
CHANGED
|
@@ -115,6 +115,8 @@ export declare function migrateUp(connectionString: string, migrationsDir: strin
|
|
|
115
115
|
step?: number;
|
|
116
116
|
allowDrift?: boolean;
|
|
117
117
|
force?: boolean /** @deprecated use allowDrift */;
|
|
118
|
+
/** Run migrations even when they contain data-destroying statements. Default false. */
|
|
119
|
+
allowDestructive?: boolean;
|
|
118
120
|
adapter?: DatabaseAdapter;
|
|
119
121
|
dialect?: Dialect;
|
|
120
122
|
}): Promise<{
|
|
@@ -134,6 +136,7 @@ export declare function migrateUp(connectionString: string, migrationsDir: strin
|
|
|
134
136
|
*/
|
|
135
137
|
export declare function migrateDown(connectionString: string, migrationsDir: string, options?: {
|
|
136
138
|
step?: number;
|
|
139
|
+
allowDestructive?: boolean;
|
|
137
140
|
adapter?: DatabaseAdapter;
|
|
138
141
|
dialect?: Dialect;
|
|
139
142
|
}): Promise<{
|
package/dist/cli/migrate.js
CHANGED
|
@@ -18,6 +18,7 @@ import pg from 'pg';
|
|
|
18
18
|
import { postgresql } from '../adapters/index.js';
|
|
19
19
|
import { postgresDialect } from '../dialect.js';
|
|
20
20
|
import { MigrationError } from '../errors.js';
|
|
21
|
+
import { DESTRUCTIVE_KIND_LABEL, scanDestructiveSql } from './destructive.js';
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// Tracking table management
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
@@ -351,6 +352,35 @@ export async function migrateUp(connectionString, migrationsDir, options) {
|
|
|
351
352
|
if (options?.step != null && options.step > 0) {
|
|
352
353
|
pending = pending.slice(0, options.step);
|
|
353
354
|
}
|
|
355
|
+
// Data-loss gate: refuse to run pending migrations containing destructive
|
|
356
|
+
// statements unless the caller has EXPLICITLY opted in. The CLI layers an
|
|
357
|
+
// interactive typed confirmation on top of this; programmatic callers must
|
|
358
|
+
// pass `allowDestructive: true`. Safe-by-default is the whole point — a
|
|
359
|
+
// DROP TABLE should never run just because a file exists.
|
|
360
|
+
if (!options?.allowDestructive) {
|
|
361
|
+
const offenders = [];
|
|
362
|
+
for (const file of pending) {
|
|
363
|
+
const { up } = parseMigrationSQL(file.path);
|
|
364
|
+
if (!up)
|
|
365
|
+
continue;
|
|
366
|
+
const hits = scanDestructiveSql(up);
|
|
367
|
+
if (hits.length > 0)
|
|
368
|
+
offenders.push({ file: file.filename, hits });
|
|
369
|
+
}
|
|
370
|
+
if (offenders.length > 0) {
|
|
371
|
+
const lines = ['[turbine] Refusing to apply migrations containing DESTRUCTIVE statements:', ''];
|
|
372
|
+
for (const o of offenders) {
|
|
373
|
+
lines.push(` ${o.file}`);
|
|
374
|
+
for (const h of o.hits) {
|
|
375
|
+
lines.push(` - [${h.kind}] ${h.target} — ${DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
lines.push('');
|
|
379
|
+
lines.push('Review the statements above. To proceed: run `npx turbine migrate up` interactively');
|
|
380
|
+
lines.push('and confirm, pass --allow-destructive, or set allowDestructive: true programmatically.');
|
|
381
|
+
throw new MigrationError(lines.join('\n'));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
354
384
|
const results = [];
|
|
355
385
|
const errors = [];
|
|
356
386
|
for (const file of pending) {
|
|
@@ -418,6 +448,36 @@ export async function migrateDown(connectionString, migrationsDir, options) {
|
|
|
418
448
|
const fileMap = new Map(allFiles.map((f) => [f.name, f]));
|
|
419
449
|
// Reverse order — rollback most recent first
|
|
420
450
|
const toRollback = applied.reverse().slice(0, options?.step ?? 1);
|
|
451
|
+
// Same data-loss gate as migrateUp — DOWN sections routinely contain
|
|
452
|
+
// DROP TABLE (the legitimate reverse of a CREATE), which still destroys
|
|
453
|
+
// every row written since the migration ran. Explicit opt-in required.
|
|
454
|
+
if (!options?.allowDestructive) {
|
|
455
|
+
const offenders = [];
|
|
456
|
+
for (const migration of toRollback) {
|
|
457
|
+
const file = fileMap.get(migration.name);
|
|
458
|
+
if (!file)
|
|
459
|
+
continue;
|
|
460
|
+
const { down } = parseMigrationSQL(file.path);
|
|
461
|
+
if (!down)
|
|
462
|
+
continue;
|
|
463
|
+
const hits = scanDestructiveSql(down);
|
|
464
|
+
if (hits.length > 0)
|
|
465
|
+
offenders.push({ file: file.filename, hits });
|
|
466
|
+
}
|
|
467
|
+
if (offenders.length > 0) {
|
|
468
|
+
const lines = ['[turbine] Refusing to roll back migrations whose DOWN sections are DESTRUCTIVE:', ''];
|
|
469
|
+
for (const o of offenders) {
|
|
470
|
+
lines.push(` ${o.file}`);
|
|
471
|
+
for (const h of o.hits) {
|
|
472
|
+
lines.push(` - [${h.kind}] ${h.target} — ${DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
lines.push('');
|
|
476
|
+
lines.push('To proceed: run `npx turbine migrate down` interactively and confirm, pass');
|
|
477
|
+
lines.push('--allow-destructive, or set allowDestructive: true programmatically.');
|
|
478
|
+
throw new MigrationError(lines.join('\n'));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
421
481
|
const results = [];
|
|
422
482
|
const errors = [];
|
|
423
483
|
for (const migration of toRollback) {
|
package/dist/cli/ui.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export declare const symbols: {
|
|
|
30
30
|
readonly bullet: "*" | "•";
|
|
31
31
|
readonly arrow: "→" | "->";
|
|
32
32
|
readonly arrowRight: ">" | "▸";
|
|
33
|
-
readonly info: "
|
|
33
|
+
readonly info: "i" | "ℹ";
|
|
34
34
|
readonly warning: "⚠" | "!";
|
|
35
35
|
readonly dot: "." | "∙";
|
|
36
36
|
readonly line: "─" | "-";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|