forked from SmartDropLabs/smartdrop-backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmigrate.js
More file actions
181 lines (154 loc) · 5.78 KB
/
Copy pathmigrate.js
File metadata and controls
181 lines (154 loc) · 5.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/env node
'use strict';
/**
* Migration CLI with safety rails (issue #252).
*
* node src/db/migrate.js up apply pending migrations
* node src/db/migrate.js up --dry-run preview without applying
* node src/db/migrate.js up --allow-destructive permit DROP/ALTER in production
* node src/db/migrate.js down roll back the last batch
* node src/db/migrate.js status show applied/pending state
*
* Every path runs the same sequence: inspect the migration SQL, enforce the
* production gate, verify the database is reachable, then apply — recording
* an audit row for the attempt either way.
*/
const path = require('path');
const knexFactory = require('knex');
const knexConfig = require('../../knexfile');
const safety = require('./migrationSafety');
const MIGRATIONS_DIR = path.join(__dirname, 'migrations');
function parseArgs(argv) {
const args = argv.slice(2);
const command = args.find((arg) => !arg.startsWith('--')) || 'up';
return {
command,
dryRun: args.includes('--dry-run'),
allowDestructive: args.includes('--allow-destructive'),
};
}
function environment() {
return process.env.NODE_ENV || 'development';
}
function print(message) {
process.stdout.write(`${message}\n`);
}
/**
* Prints what a migration run would do without touching the database.
*
* Knex has no native "explain this migration" mode — migrations are
* arbitrary JS, so the only fully accurate preview would be running them.
* This reports which migrations are pending and which destructive
* operations their SQL contains, which is what the decision to proceed
* actually turns on.
*/
async function dryRun(knex, inspected) {
print('DRY RUN — no changes will be applied\n');
print(`Environment: ${environment()}`);
// The destructive scan is static analysis over the migration files, so it
// still works with no database in reach. Only the applied/pending split
// needs a connection — losing it degrades the preview rather than failing
// it, since "what would this drop?" is exactly the question an operator
// wants answered before pointing the CLI at a live database.
let pending = null;
try {
const [completed, todo] = await knex.migrate.list();
pending = todo.map((m) => m.file ?? m);
print(`Already applied: ${completed.length}`);
print(`Pending: ${pending.length}`);
} catch (err) {
print(`Database unreachable (${err.message})`);
print('Reporting static analysis of all migration files instead.');
}
const listed = pending ?? inspected.map((entry) => entry.file);
if (listed.length === 0) {
print('\nNothing to apply.');
return;
}
print('');
for (const name of listed) {
const entry = inspected.find((item) => item.file === name);
const operations = entry && entry.destructive.length > 0
? entry.destructive.map((d) => d.operation).join(', ')
: null;
print(` - ${name}${operations ? ` [DESTRUCTIVE: ${operations}]` : ''}`);
}
const destructive = inspected.filter((entry) => entry.destructive.length > 0);
if (destructive.length > 0) {
print(
`\n${destructive.length} migration(s) contain destructive operations. `
+ 'Applying these against production requires --allow-destructive.',
);
}
}
async function run() {
const { command, dryRun: isDryRun, allowDestructive } = parseArgs(process.argv);
const env = environment();
const config = knexConfig[env] || knexConfig.development;
const knex = knexFactory(config);
let exitCode = 0;
try {
const inspected = safety.inspectMigrationDirectory(MIGRATIONS_DIR);
if (command === 'status') {
const [completed, pending] = await knex.migrate.list();
print(`Environment: ${env}`);
print(`Applied (${completed.length}):`);
completed.forEach((m) => print(` - ${m.name ?? m}`));
print(`Pending (${pending.length}):`);
pending.forEach((m) => print(` - ${m.file ?? m}`));
return;
}
// The gate runs before the dry-run branch too, so a blocked migration
// is reported as blocked rather than quietly previewing as applicable.
safety.assertMigrationAllowed(inspected, { env, allowDestructive });
if (isDryRun) {
await dryRun(knex, inspected);
return;
}
const preflight = await safety.preflightCheck(knex);
preflight.warnings.forEach((warning) => print(`WARNING: ${warning}`));
const direction = command === 'down' ? 'down' : 'up';
const destructiveOps = inspected
.flatMap((entry) => entry.destructive.map((d) => d.operation));
try {
const [batch, applied] = direction === 'down'
? await knex.migrate.down()
: await knex.migrate.latest();
const names = Array.isArray(applied) ? applied : [applied].filter(Boolean);
if (names.length === 0) {
print(direction === 'down' ? 'No migration to roll back.' : 'Already up to date.');
} else {
print(`Batch ${batch}: ${direction} — ${names.join(', ')}`);
}
for (const name of names) {
await safety.recordMigrationRun(knex, {
migrationName: String(name),
direction,
status: 'applied',
destructive: destructiveOps.length > 0,
detectedOperations: destructiveOps,
});
}
} catch (err) {
await safety.recordMigrationRun(knex, {
migrationName: `${direction}:failed`,
direction,
status: 'failed',
destructive: destructiveOps.length > 0,
detectedOperations: destructiveOps,
error: err.message,
});
throw err;
}
} catch (err) {
process.stderr.write(`${err.message}\n`);
exitCode = 1;
} finally {
await knex.destroy();
}
process.exitCode = exitCode;
}
if (require.main === module) {
run();
}
module.exports = { parseArgs, run };