knex-migrator 4.0.5
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/CHANGELOG.md +171 -0
- package/LICENSE +19 -0
- package/README.md +347 -0
- package/bin/knex-migrator +13 -0
- package/bin/knex-migrator-health +35 -0
- package/bin/knex-migrator-init +34 -0
- package/bin/knex-migrator-migrate +38 -0
- package/bin/knex-migrator-reset +31 -0
- package/bin/knex-migrator-rollback +37 -0
- package/lib/database.js +204 -0
- package/lib/errors.js +84 -0
- package/lib/index.js +1243 -0
- package/lib/locking.js +113 -0
- package/lib/utils.js +213 -0
- package/logging.js +8 -0
- package/migrations/add-primary-key-to-lock-table.js +58 -0
- package/migrations/field-length.js +22 -0
- package/migrations/index.js +17 -0
- package/migrations/lock-table.js +41 -0
- package/migrations/use-index.js +24 -0
- package/package.json +80 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1243 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const Promise = require('bluebird');
|
|
4
|
+
const debug = require('debug')('knex-migrator:index');
|
|
5
|
+
const database = require('./database');
|
|
6
|
+
const utils = require('./utils');
|
|
7
|
+
const errors = require('./errors');
|
|
8
|
+
const logging = require('../logging');
|
|
9
|
+
const migrations = require('../migrations');
|
|
10
|
+
const locking = require('./locking');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @description Prototype of Knex-migrator.
|
|
14
|
+
* @param {Object} options
|
|
15
|
+
* @constructor
|
|
16
|
+
*/
|
|
17
|
+
function KnexMigrator(options = {}) {
|
|
18
|
+
let config = utils.loadConfig(options);
|
|
19
|
+
|
|
20
|
+
if (!config.database) {
|
|
21
|
+
throw new Error('MigratorConfig.js needs to export a database config.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!config.migrationPath) {
|
|
25
|
+
throw new Error('MigratorConfig.js needs to export the location of your migration files.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!config.currentVersion) {
|
|
29
|
+
throw new Error('MigratorConfig.js needs to export the a current version.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.executedFromShell = options.executedFromShell;
|
|
33
|
+
this.currentVersion = config.currentVersion;
|
|
34
|
+
this.migrationPath = config.migrationPath;
|
|
35
|
+
this.subfolder = config.subfolder || 'versions';
|
|
36
|
+
|
|
37
|
+
this.dbConfig = config.database;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `knex-migrator init`
|
|
42
|
+
*
|
|
43
|
+
* @description This task will run init scripts.
|
|
44
|
+
*
|
|
45
|
+
* The `init` command goes through the following steps:
|
|
46
|
+
*
|
|
47
|
+
* 1. Create a database knex connection.
|
|
48
|
+
* 2. Create database if it does not exist.
|
|
49
|
+
* 3. Create tables for knex-migrator (migrations, migrations-lock)
|
|
50
|
+
* 4. Run table upgrades/migrations if available.
|
|
51
|
+
* 5. Lock the tables to avoid running migrations in parallel.
|
|
52
|
+
* 6. Execute hooks and init scripts.
|
|
53
|
+
* 7. Init completion: add all existing migration scripts to the database to make it possible to detect a state of the database correctly.
|
|
54
|
+
* 8. Unlock tables.
|
|
55
|
+
* 9. Disconnect from database otherwise the CLI won't close properly.
|
|
56
|
+
*
|
|
57
|
+
* The `init` command can be triggered via `knex-migrator migrate --init`.
|
|
58
|
+
* This is a feature which makes it easier to not having to differentiate if a database needs a migration or an
|
|
59
|
+
* initilisation. The special characteristic of this combo is that the init completation shouldn't run, because
|
|
60
|
+
* otherwise we would overjump migration files to execute.
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} options - Custom options you can pass in (disableHooks, noScripts, skipInitCompletion, only, skip)
|
|
63
|
+
* @returns {Bluebird<R>}
|
|
64
|
+
*/
|
|
65
|
+
KnexMigrator.prototype.init = function init(options) {
|
|
66
|
+
options = options || {};
|
|
67
|
+
|
|
68
|
+
let self = this,
|
|
69
|
+
disableHooks = options.disableHooks,
|
|
70
|
+
noScripts = options.noScripts,
|
|
71
|
+
skipInitCompletion = options.skipInitCompletion,
|
|
72
|
+
skippedTasks = [],
|
|
73
|
+
hooks = {};
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
if (!disableHooks) {
|
|
77
|
+
hooks = require(path.join(self.migrationPath, '/hooks/init'));
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
debug('Hook Error: ' + err.message);
|
|
81
|
+
debug('No hooks found, no problem.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.connection = database.connect(this.dbConfig);
|
|
85
|
+
|
|
86
|
+
return database.createDatabaseIfNotExist(self.dbConfig)
|
|
87
|
+
.then(function () {
|
|
88
|
+
if (noScripts) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// @NOTE: create table outside of the transaction! (implicit)
|
|
93
|
+
return database.createMigrationsTable(self.connection).then(function () {
|
|
94
|
+
return migrations.run(self.connection).then(function () {
|
|
95
|
+
return locking.lock(self.connection)
|
|
96
|
+
.then(function () {
|
|
97
|
+
if (hooks.before) {
|
|
98
|
+
debug('Before hook');
|
|
99
|
+
return hooks.before({
|
|
100
|
+
connection: self.connection
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
.then(function executeMigrate() {
|
|
105
|
+
return self._migrateTo({
|
|
106
|
+
version: 'init',
|
|
107
|
+
only: options.only,
|
|
108
|
+
skip: options.skip
|
|
109
|
+
});
|
|
110
|
+
})
|
|
111
|
+
.then(function (response) {
|
|
112
|
+
skippedTasks = response.skippedTasks;
|
|
113
|
+
|
|
114
|
+
if (hooks.after) {
|
|
115
|
+
debug('After hook');
|
|
116
|
+
return hooks.after({
|
|
117
|
+
connection: self.connection
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
.then(function () {
|
|
122
|
+
const initTasks = utils.listFiles(path.join(self.migrationPath, 'init'));
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* CASE 1: You can disable init completion manually
|
|
126
|
+
* CASE 2: Only skip init completion if you have all init scripts in place already!!
|
|
127
|
+
*
|
|
128
|
+
* Example:
|
|
129
|
+
* - knex-migrator migrate --init should not execute init completion
|
|
130
|
+
* -> does not run init completion
|
|
131
|
+
*
|
|
132
|
+
* Example:
|
|
133
|
+
* - knex-migrator init (process was destroyed, you only have 1 init script in your db)
|
|
134
|
+
* - knex-migrator init (the other init script get's executed)
|
|
135
|
+
* -> run init completion
|
|
136
|
+
*
|
|
137
|
+
*/
|
|
138
|
+
if (skippedTasks.length === initTasks.length || skipInitCompletion) {
|
|
139
|
+
return Promise.resolve();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let versionsToMigrateTo;
|
|
143
|
+
|
|
144
|
+
// CASE: insert all migration files, otherwise you will run into problems
|
|
145
|
+
// e.g. you are on 1.2, you initialise the database, but there is 1.3 migration script
|
|
146
|
+
try {
|
|
147
|
+
versionsToMigrateTo = utils.readVersionFolders(
|
|
148
|
+
path.join(self.migrationPath, self.subfolder)) || [];
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// CASE: versions folder does not exists
|
|
151
|
+
if (err.code === 'READ_FOLDERS') {
|
|
152
|
+
return Promise.resolve();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return database.createTransaction(self.connection, function (transacting) {
|
|
159
|
+
// CASE: Run over all migration scripts and add the file name to the database.
|
|
160
|
+
return Promise.each(versionsToMigrateTo, function (versionToMigrateTo) {
|
|
161
|
+
let versionPath = path.join(self.migrationPath, self.subfolder, versionToMigrateTo);
|
|
162
|
+
let filesToMigrateTo = utils.listFiles(versionPath) || [];
|
|
163
|
+
|
|
164
|
+
return Promise.each(filesToMigrateTo, function (fileToMigrateTo) {
|
|
165
|
+
// CASE: check if migration exists, do not insert twice
|
|
166
|
+
return transacting('migrations')
|
|
167
|
+
.where('name', fileToMigrateTo)
|
|
168
|
+
.then(function (migrationExists) {
|
|
169
|
+
if (migrationExists.length) {
|
|
170
|
+
return Promise.resolve();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return transacting('migrations')
|
|
174
|
+
.insert({
|
|
175
|
+
name: fileToMigrateTo,
|
|
176
|
+
version: versionToMigrateTo,
|
|
177
|
+
currentVersion: self.currentVersion
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
})
|
|
184
|
+
.then(function () {
|
|
185
|
+
return locking.unlock(self.connection);
|
|
186
|
+
})
|
|
187
|
+
.catch(function (err) {
|
|
188
|
+
if (err instanceof errors.MigrationsAreLockedError) {
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (err instanceof errors.LockError) {
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return locking.unlock(self.connection)
|
|
197
|
+
.then(function () {
|
|
198
|
+
throw err;
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
})
|
|
204
|
+
.then(function onInitSuccess() {
|
|
205
|
+
debug('Init Success');
|
|
206
|
+
})
|
|
207
|
+
.catch(function onInitError(err) {
|
|
208
|
+
// CASE: Do not rollback if migrations are locked
|
|
209
|
+
if (err instanceof errors.MigrationsAreLockedError) {
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// CASE: Do not rollback migration scripts, if lock error
|
|
214
|
+
if (err instanceof errors.LockError) {
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// CASE: ETIMEDOUT, ENOTFOUND
|
|
219
|
+
if (err instanceof errors.DatabaseError) {
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
debug('Rolling back: ' + err.message);
|
|
224
|
+
|
|
225
|
+
return self._rollback({
|
|
226
|
+
version: 'init',
|
|
227
|
+
skippedTasks: {
|
|
228
|
+
init: skippedTasks
|
|
229
|
+
}
|
|
230
|
+
}).then(function () {
|
|
231
|
+
throw err;
|
|
232
|
+
}).catch(function (innerErr) {
|
|
233
|
+
if (errors.utils.isIgnitionError(innerErr)) {
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
throw new errors.RollbackError({
|
|
238
|
+
message: innerErr.message,
|
|
239
|
+
err: innerErr,
|
|
240
|
+
context: `OuterError: ${err.message}`
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
})
|
|
244
|
+
.finally(function () {
|
|
245
|
+
let ops = [];
|
|
246
|
+
|
|
247
|
+
if (hooks.shutdown) {
|
|
248
|
+
ops.push(function shutdownHook() {
|
|
249
|
+
debug('Shutdown hook');
|
|
250
|
+
return hooks.shutdown({
|
|
251
|
+
executedFromShell: self.executedFromShell
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
ops.push(function destroyConnection() {
|
|
257
|
+
debug('Destroy connection');
|
|
258
|
+
return self.connection.destroy()
|
|
259
|
+
.then(function () {
|
|
260
|
+
debug('Destroyed connection');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return Promise.each(ops, function (op) {
|
|
265
|
+
return op.bind(self)();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* `knex-migrator migrate`
|
|
272
|
+
*
|
|
273
|
+
* @description This task will run migration scripts.
|
|
274
|
+
*
|
|
275
|
+
* The `migrate` task runs through the following steps:
|
|
276
|
+
*
|
|
277
|
+
* 1. Create a database knex connection.
|
|
278
|
+
* 2. Ensure connection works as expected.
|
|
279
|
+
* 3. Run table upgrades/migrations if available.
|
|
280
|
+
* 4. Lock the target tables to avoid running migrations in parallel.
|
|
281
|
+
* 5. Perform an integrity check to figure out which migrations need to run (compare files against database)
|
|
282
|
+
* 6. Execute migrations.
|
|
283
|
+
* 7. Unlock table.
|
|
284
|
+
* 8. Disconnect from database otherwise the CLI won't close properly.
|
|
285
|
+
*
|
|
286
|
+
* @param {Object} options - Custom options you can pass in (version, force, init, only, skip)
|
|
287
|
+
* @returns {Bluebird<any>}
|
|
288
|
+
*/
|
|
289
|
+
KnexMigrator.prototype.migrate = function migrate(options) {
|
|
290
|
+
options = options || {};
|
|
291
|
+
|
|
292
|
+
let self = this,
|
|
293
|
+
onlyVersion = options.version,
|
|
294
|
+
force = options.force,
|
|
295
|
+
init = options.init,
|
|
296
|
+
onlyFile = options.only,
|
|
297
|
+
versionsToMigrate = [],
|
|
298
|
+
hooks = {};
|
|
299
|
+
|
|
300
|
+
// CASE: you can only use only in combination with the version flag
|
|
301
|
+
if (onlyFile && !onlyVersion) {
|
|
302
|
+
onlyFile = null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (onlyVersion) {
|
|
306
|
+
debug('onlyVersion: ' + onlyVersion);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// CASE: `--init` flag is passed. Combo feature.
|
|
310
|
+
if (init) {
|
|
311
|
+
return this.init()
|
|
312
|
+
.then(function () {
|
|
313
|
+
return self.migrate(_.omit(options, 'init'));
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
hooks = require(path.join(self.migrationPath, '/hooks/migrate'));
|
|
319
|
+
} catch (err) {
|
|
320
|
+
debug('Hook Error: ' + err.message);
|
|
321
|
+
debug('No hooks found, no problem.');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this.connection = database.connect(this.dbConfig);
|
|
325
|
+
|
|
326
|
+
return database.ensureConnectionWorks(this.connection)
|
|
327
|
+
.then(function () {
|
|
328
|
+
return migrations.run(self.connection);
|
|
329
|
+
})
|
|
330
|
+
.then(function () {
|
|
331
|
+
return locking.lock(self.connection);
|
|
332
|
+
})
|
|
333
|
+
.then(function () {
|
|
334
|
+
return self._integrityCheck({
|
|
335
|
+
force: force
|
|
336
|
+
});
|
|
337
|
+
})
|
|
338
|
+
.then(function (result) {
|
|
339
|
+
_.each(result, function (value, version) {
|
|
340
|
+
// CASE: Log which versions won't be executed based on the "only" flag
|
|
341
|
+
if (onlyVersion && version !== onlyVersion) {
|
|
342
|
+
debug('Do not execute: ' + version);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if (onlyVersion) {
|
|
348
|
+
// CASE: filter out versions which should not run
|
|
349
|
+
let containsVersion = _.find(result, function (obj, key) {
|
|
350
|
+
return key === onlyVersion;
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
if (!containsVersion) {
|
|
354
|
+
logging.warn('Cannot find requested version: ' + onlyVersion);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_.each(result, function (value, version) {
|
|
359
|
+
// CASE: compare files on disk with files in database
|
|
360
|
+
if (value.expected !== value.actual) {
|
|
361
|
+
debug('Need to execute migrations for: ' + version);
|
|
362
|
+
versionsToMigrate.push(version);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
})
|
|
366
|
+
.then(function executeBeforeHook() {
|
|
367
|
+
if (!versionsToMigrate.length) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (hooks.before) {
|
|
372
|
+
debug('Before hook');
|
|
373
|
+
return hooks.before({
|
|
374
|
+
connection: self.connection
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
.then(function executeMigrations() {
|
|
379
|
+
if (!versionsToMigrate.length) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return Promise.each(versionsToMigrate, function (versionToMigrate) {
|
|
384
|
+
return self._migrateTo({
|
|
385
|
+
version: versionToMigrate,
|
|
386
|
+
only: onlyFile,
|
|
387
|
+
hooks: hooks
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
})
|
|
391
|
+
.then(function executeAfterHook() {
|
|
392
|
+
if (!versionsToMigrate.length) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (hooks.after) {
|
|
397
|
+
debug('After hook');
|
|
398
|
+
return hooks.after({
|
|
399
|
+
connection: self.connection
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
.then(function () {
|
|
404
|
+
return locking.unlock(self.connection);
|
|
405
|
+
})
|
|
406
|
+
.catch(function (err) {
|
|
407
|
+
// CASE: Do not rollback if migrations are locked
|
|
408
|
+
if (err instanceof errors.MigrationsAreLockedError) {
|
|
409
|
+
throw err;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// CASE: Do not rollback migration scripts, if lock error
|
|
413
|
+
if (err instanceof errors.LockError) {
|
|
414
|
+
throw err;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// CASE: ETIMEDOUT, ENOTFOUND
|
|
418
|
+
if (err instanceof errors.DatabaseError) {
|
|
419
|
+
throw err;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (err.context && err.context.name) {
|
|
423
|
+
debug(`Task failed: ${err.context.name}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
debug(`Rolling back: ${err.message}`);
|
|
427
|
+
|
|
428
|
+
versionsToMigrate.reverse();
|
|
429
|
+
|
|
430
|
+
// CASE: rollback in reversed order
|
|
431
|
+
return Promise.each(versionsToMigrate, function (version) {
|
|
432
|
+
return self._rollback({version: version, task: err.context});
|
|
433
|
+
}).then(function () {
|
|
434
|
+
throw err;
|
|
435
|
+
}).catch(function (innerErr) {
|
|
436
|
+
if (errors.utils.isIgnitionError(innerErr)) {
|
|
437
|
+
throw err;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
throw new errors.RollbackError({
|
|
441
|
+
message: innerErr.message,
|
|
442
|
+
err: innerErr,
|
|
443
|
+
context: `OuterError: ${err.message}`
|
|
444
|
+
});
|
|
445
|
+
}).finally(function () {
|
|
446
|
+
return locking.unlock(self.connection);
|
|
447
|
+
});
|
|
448
|
+
}).finally(function () {
|
|
449
|
+
let ops = [];
|
|
450
|
+
|
|
451
|
+
if (hooks.shutdown) {
|
|
452
|
+
ops.push(function shutdownHook() {
|
|
453
|
+
debug('Shutdown hook');
|
|
454
|
+
return hooks.shutdown({
|
|
455
|
+
executedFromShell: self.executedFromShell
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
ops.push(function destroyConnection() {
|
|
461
|
+
debug('Destroy connection');
|
|
462
|
+
return self.connection.destroy()
|
|
463
|
+
.then(function () {
|
|
464
|
+
debug('Destroyed connection');
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
return Promise.each(ops, function (op) {
|
|
469
|
+
return op.bind(self)();
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* `knex-migrator reset`
|
|
476
|
+
*
|
|
477
|
+
* @description The rest command will do a full hard reset.
|
|
478
|
+
*
|
|
479
|
+
* It will:
|
|
480
|
+
*
|
|
481
|
+
* 1. Create a connection to the database.
|
|
482
|
+
* 2. Ensure connection works as expected.
|
|
483
|
+
* 3. Lock the table to avoid running reset in parallel.
|
|
484
|
+
* 4. Run table upgrades/migrations if available.
|
|
485
|
+
* 5. Drop the database.
|
|
486
|
+
*
|
|
487
|
+
* If you pass the "force" flag, you will skip step 2-4.
|
|
488
|
+
*
|
|
489
|
+
* @param {Object} options - Custom options the user can pass in (force)
|
|
490
|
+
* @returns {*}
|
|
491
|
+
*/
|
|
492
|
+
KnexMigrator.prototype.reset = function reset(options) {
|
|
493
|
+
options = options || {};
|
|
494
|
+
|
|
495
|
+
let self = this;
|
|
496
|
+
let force = options.force;
|
|
497
|
+
|
|
498
|
+
this.connection = database.connect(this.dbConfig);
|
|
499
|
+
|
|
500
|
+
// CASE: ignore lock completely and drop the db
|
|
501
|
+
if (force) {
|
|
502
|
+
return database.drop({
|
|
503
|
+
connection: self.connection,
|
|
504
|
+
dbConfig: self.dbConfig
|
|
505
|
+
}).catch(function onRestError(err) {
|
|
506
|
+
// Database does not exist. MySql.
|
|
507
|
+
if (err.errno === 1049) {
|
|
508
|
+
return Promise.resolve();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
throw err;
|
|
512
|
+
}).finally(function () {
|
|
513
|
+
debug('Destroy connection');
|
|
514
|
+
return self.connection.destroy()
|
|
515
|
+
.then(function () {
|
|
516
|
+
debug('Destroyed connection');
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return database.ensureConnectionWorks(this.connection)
|
|
522
|
+
.then(function () {
|
|
523
|
+
return migrations.run(self.connection);
|
|
524
|
+
})
|
|
525
|
+
.then(function () {
|
|
526
|
+
return locking.lock(self.connection);
|
|
527
|
+
})
|
|
528
|
+
.then(function () {
|
|
529
|
+
return database.drop({
|
|
530
|
+
connection: self.connection,
|
|
531
|
+
dbConfig: self.dbConfig
|
|
532
|
+
});
|
|
533
|
+
})
|
|
534
|
+
.catch(function onRestError(err) {
|
|
535
|
+
if (err instanceof errors.MigrationsAreLockedError) {
|
|
536
|
+
throw err;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// CASE: ETIMEDOUT, ENOTFOUND
|
|
540
|
+
if (err instanceof errors.DatabaseError) {
|
|
541
|
+
throw err;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
debug('Reset error: ' + err.message);
|
|
545
|
+
|
|
546
|
+
// CASE: Database does not exist. For MySql.
|
|
547
|
+
if (err.errno === 1049) {
|
|
548
|
+
return Promise.resolve();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return locking.unlock(self.connection)
|
|
552
|
+
.then(function () {
|
|
553
|
+
throw err;
|
|
554
|
+
});
|
|
555
|
+
})
|
|
556
|
+
.finally(function () {
|
|
557
|
+
debug('Destroy connection');
|
|
558
|
+
return self.connection.destroy()
|
|
559
|
+
.then(function () {
|
|
560
|
+
debug('Destroyed connection');
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* `knex-migrator health`
|
|
567
|
+
*
|
|
568
|
+
* @description This task detects the (migration) state of your database.
|
|
569
|
+
*
|
|
570
|
+
* It asks the database if....
|
|
571
|
+
*
|
|
572
|
+
* - the database was initialised?
|
|
573
|
+
* - migration files need to be executed?
|
|
574
|
+
*
|
|
575
|
+
* The task runs through the following steps:
|
|
576
|
+
*
|
|
577
|
+
* 1. Create a database connection.
|
|
578
|
+
* 2. Ensure the connection works (credentials are correct)
|
|
579
|
+
* 3. Run table upgrades/migrations if available.
|
|
580
|
+
* 4. Asks if the database is locked and aborts if so.
|
|
581
|
+
* 5. Perform an integrity check to figure out which migrations need to run (compare files against database)
|
|
582
|
+
* 6. Returns result.
|
|
583
|
+
* 7. Destroy connection.
|
|
584
|
+
*
|
|
585
|
+
* @returns {Bluebird<any>}
|
|
586
|
+
*/
|
|
587
|
+
KnexMigrator.prototype.isDatabaseOK = function isDatabaseOK() {
|
|
588
|
+
let self = this;
|
|
589
|
+
this.connection = database.connect(this.dbConfig);
|
|
590
|
+
|
|
591
|
+
return database.ensureConnectionWorks(this.connection)
|
|
592
|
+
.then(function () {
|
|
593
|
+
return migrations.run(self.connection);
|
|
594
|
+
})
|
|
595
|
+
.then(function () {
|
|
596
|
+
return locking.isLocked(self.connection);
|
|
597
|
+
})
|
|
598
|
+
.then(function () {
|
|
599
|
+
return self._integrityCheck();
|
|
600
|
+
})
|
|
601
|
+
.then(function (result) {
|
|
602
|
+
// CASE: if an init script was removed, the health check will be positive (see #48)
|
|
603
|
+
if (result.init && result.init.expected > result.init.actual) {
|
|
604
|
+
throw new errors.DatabaseIsNotOkError({
|
|
605
|
+
message: 'Please run knex-migrator init',
|
|
606
|
+
code: 'DB_NOT_INITIALISED'
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
_.each(_.omit(result, 'init'), function (value) {
|
|
611
|
+
// CASE: there are more migrations expected than have been run, database needs to be migrated
|
|
612
|
+
if (value.expected > value.actual) {
|
|
613
|
+
throw new errors.DatabaseIsNotOkError({
|
|
614
|
+
message: 'Migrations are missing. Please run `knex-migrator migrate`.',
|
|
615
|
+
code: 'DB_NEEDS_MIGRATION',
|
|
616
|
+
help: `Expected: ${value.expected} items in migrations table, found: ${value.actual}`
|
|
617
|
+
});
|
|
618
|
+
// CASE: there are more actual migrations than expected, something has gone wrong :(
|
|
619
|
+
} else if (value.expected < value.actual) {
|
|
620
|
+
throw new errors.DatabaseIsNotOkError({
|
|
621
|
+
message: 'Detected more items in the migrations table than expected. Please manually inspect the migrations table.',
|
|
622
|
+
code: 'MIGRATION_STATE_ERROR',
|
|
623
|
+
help: `Expected: ${value.expected} items in migrations table, found: ${value.actual}`
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
})
|
|
628
|
+
.catch(function (err) {
|
|
629
|
+
// CASE: database does not exist
|
|
630
|
+
if (err.errno === 1049) {
|
|
631
|
+
throw new errors.DatabaseIsNotOkError({
|
|
632
|
+
message: 'Please run knex-migrator init',
|
|
633
|
+
code: 'DB_NOT_INITIALISED'
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
throw err;
|
|
638
|
+
})
|
|
639
|
+
.finally(function () {
|
|
640
|
+
if (!self.connection) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
debug('Destroy connection');
|
|
645
|
+
return self.connection.destroy()
|
|
646
|
+
.then(function () {
|
|
647
|
+
debug('Destroyed connection');
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* `knex-migrator rollback`
|
|
654
|
+
*
|
|
655
|
+
* @description This task will rollback the database to a version.
|
|
656
|
+
*
|
|
657
|
+
* It will:
|
|
658
|
+
*
|
|
659
|
+
* 1. Create a connection to the database.
|
|
660
|
+
* 2. Ensure the connection works (credentials are correct)
|
|
661
|
+
* 3. Asks the database if the lock is active.
|
|
662
|
+
* 4. If the lock is not active, you cannot rollback. This is the current default behaviour.
|
|
663
|
+
* 5. If you pass the "force" flag, you can rollback if the lock is inactive.
|
|
664
|
+
* 6. Executes rollback helper to rollback to a version.
|
|
665
|
+
* 7. Destroy connection.
|
|
666
|
+
*
|
|
667
|
+
* @param {Object} options - Custom options the user can pass in (force, version, v, disableHooks)
|
|
668
|
+
* @returns {Bluebird<any>}
|
|
669
|
+
*/
|
|
670
|
+
KnexMigrator.prototype.rollback = function rollback(options) {
|
|
671
|
+
options = options || {};
|
|
672
|
+
|
|
673
|
+
let self = this;
|
|
674
|
+
let force = options.force;
|
|
675
|
+
let version = options.version || options.v;
|
|
676
|
+
let disableHooks = options.disableHooks;
|
|
677
|
+
let hooks = {};
|
|
678
|
+
|
|
679
|
+
this.connection = database.connect(this.dbConfig);
|
|
680
|
+
|
|
681
|
+
const helper = function helper() {
|
|
682
|
+
return new Promise(function (resolve, reject) {
|
|
683
|
+
try {
|
|
684
|
+
if (!disableHooks) {
|
|
685
|
+
// @TODO: load init or migrate hooks
|
|
686
|
+
hooks = require(path.join(self.migrationPath, '/hooks/init'));
|
|
687
|
+
}
|
|
688
|
+
} catch (err) {
|
|
689
|
+
debug('Hook Error: ' + err.message);
|
|
690
|
+
debug('No hooks found, no problem.');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (hooks.before) {
|
|
694
|
+
return hooks.before({
|
|
695
|
+
connection: self.connection
|
|
696
|
+
}).then(resolve).catch(reject);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
resolve();
|
|
700
|
+
}).then(function () {
|
|
701
|
+
let whereQuery = {};
|
|
702
|
+
|
|
703
|
+
// CASE 1: rollback to specific version (query all and filter out)
|
|
704
|
+
// CASE 2: rollback current version you are on
|
|
705
|
+
if (version) {
|
|
706
|
+
debug(`Rollback to specific version: ${version}`);
|
|
707
|
+
whereQuery = {};
|
|
708
|
+
} else {
|
|
709
|
+
whereQuery = {
|
|
710
|
+
currentVersion: self.currentVersion
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return self.connection('migrations')
|
|
715
|
+
.where(whereQuery)
|
|
716
|
+
.then(function (values) {
|
|
717
|
+
if (!values.length) {
|
|
718
|
+
throw new errors.IncorrectUsageError({
|
|
719
|
+
message: 'No migrations available to rollback.'
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// CASE: filter out all versions which are smaller than the version we want to rollback to
|
|
724
|
+
if (version) {
|
|
725
|
+
values = _.filter(values, function (value) {
|
|
726
|
+
return utils.isGreaterThanVersion({
|
|
727
|
+
greaterVersion: value.version,
|
|
728
|
+
smallerVersion: version
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// @NOTE: we never ever rollback init scripts for now.
|
|
734
|
+
// this can be very dangerous, because it removes tables
|
|
735
|
+
// @EXCEPTION: you run init scripts and they fail
|
|
736
|
+
values = _.filter(values, function (value) {
|
|
737
|
+
return value.version !== 'init';
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
values.reverse();
|
|
741
|
+
return Promise.each(values, function (value) {
|
|
742
|
+
return self._rollback({
|
|
743
|
+
version: value.version,
|
|
744
|
+
onlyTasks: [value.name]
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
}).then(function () {
|
|
749
|
+
if (hooks.shutdown) {
|
|
750
|
+
return hooks.shutdown({
|
|
751
|
+
executedFromShell: self.executedFromShell
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
return database.ensureConnectionWorks(this.connection)
|
|
758
|
+
.then(function () {
|
|
759
|
+
return migrations.run(self.connection);
|
|
760
|
+
})
|
|
761
|
+
.then(function () {
|
|
762
|
+
return locking.isLocked(self.connection);
|
|
763
|
+
})
|
|
764
|
+
.then(function () {
|
|
765
|
+
// CASE: db is not locked, force
|
|
766
|
+
if (force) {
|
|
767
|
+
return helper();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
throw new errors.IncorrectUsageError({
|
|
771
|
+
message: 'Rollback did not happen.',
|
|
772
|
+
help: 'Use --force if you want to force a rollback. By default, rollbacks are only allowed if your database is locked.'
|
|
773
|
+
});
|
|
774
|
+
})
|
|
775
|
+
.catch(function (err) {
|
|
776
|
+
if (err instanceof errors.MigrationsAreLockedError) {
|
|
777
|
+
return helper()
|
|
778
|
+
.then(function () {
|
|
779
|
+
return locking.unlock(self.connection);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
throw err;
|
|
784
|
+
})
|
|
785
|
+
.finally(function () {
|
|
786
|
+
if (!self.connection) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
debug('Destroy connection');
|
|
791
|
+
return self.connection.destroy()
|
|
792
|
+
.then(function () {
|
|
793
|
+
debug('Destroyed connection');
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// @TODO: All of these functions below are helper functions. Source them out as part of https://github.com/TryGhost/knex-migrator/issues/95.
|
|
799
|
+
/**
|
|
800
|
+
* @description Private helper function for rolling back. It is called in various places to rollback to a state.
|
|
801
|
+
*
|
|
802
|
+
* Cases:
|
|
803
|
+
*
|
|
804
|
+
* 1. Init or migrate task failed, rollback the previous tasks too.
|
|
805
|
+
* 2. Rollback task is executed.
|
|
806
|
+
*
|
|
807
|
+
* It will:
|
|
808
|
+
*
|
|
809
|
+
* 1. Read the migration tasks from disk.
|
|
810
|
+
* 2. Call "down" fn of target migration script.
|
|
811
|
+
* 3. Delete migration entry from database.
|
|
812
|
+
*
|
|
813
|
+
* @param {Object} options - Custom options the user can pass in (version, skippedTasks, onlyTasks, task)
|
|
814
|
+
* @returns {Bluebird<IterableOrNever<R>>}
|
|
815
|
+
* @private
|
|
816
|
+
*/
|
|
817
|
+
KnexMigrator.prototype._rollback = function _rollback(options) {
|
|
818
|
+
let version = options.version;
|
|
819
|
+
let skippedTasks = options.skippedTasks || [];
|
|
820
|
+
let onlyTasks = options.onlyTasks || [];
|
|
821
|
+
const failedTask = options.task;
|
|
822
|
+
let tasks = [];
|
|
823
|
+
let self = this;
|
|
824
|
+
|
|
825
|
+
if (version !== 'init') {
|
|
826
|
+
tasks = utils.readTasks(path.join(this.migrationPath, this.subfolder, version));
|
|
827
|
+
} else {
|
|
828
|
+
try {
|
|
829
|
+
tasks = utils.readTasks(path.join(this.migrationPath, version));
|
|
830
|
+
} catch (err) {
|
|
831
|
+
if (err.code === 'MIGRATION_PATH') {
|
|
832
|
+
tasks = [];
|
|
833
|
+
} else {
|
|
834
|
+
throw err;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// CASE: rollback failed in one of the tasks in init or migrate
|
|
840
|
+
// CASE: if no task available, you are about to rollback manually `knex-migrator rollback`
|
|
841
|
+
if (failedTask) {
|
|
842
|
+
const newTasks = [];
|
|
843
|
+
|
|
844
|
+
for (let i = 0; i < tasks.length; i = i + 1) {
|
|
845
|
+
const task = tasks[i];
|
|
846
|
+
|
|
847
|
+
if (task.name !== failedTask.name) {
|
|
848
|
+
newTasks.push(task);
|
|
849
|
+
} else if (task.name === failedTask.name) {
|
|
850
|
+
/**
|
|
851
|
+
* @NOTE
|
|
852
|
+
*
|
|
853
|
+
* The task, which has failed, is never written to the database.
|
|
854
|
+
* But we have to double check if the target task was running in a transaction.
|
|
855
|
+
*
|
|
856
|
+
* Transaction: no need to rollback this task
|
|
857
|
+
* No Transaction: we have to rollback this task, because of implicit commits
|
|
858
|
+
*/
|
|
859
|
+
if (!failedTask.config || !failedTask.config.transaction) {
|
|
860
|
+
newTasks.push(task);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
tasks = newTasks;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// CASE: one of the migrations that are about to be rolled back is marked as irreversible. Exit early without performing any actions
|
|
871
|
+
const irreversibleMigrations = _.filter(tasks, function (task) {
|
|
872
|
+
return !!_.get(task, 'config.irreversible');
|
|
873
|
+
});
|
|
874
|
+
if (irreversibleMigrations.length) {
|
|
875
|
+
return Promise.reject(new errors.IrreversibleMigrationError({
|
|
876
|
+
message: 'Unable to rollback',
|
|
877
|
+
help: 'There are irreversible migrations when rolling back to the selected version, this typically means data required for earlier versions has been deleted. Please restore from a backup instead.',
|
|
878
|
+
code: 'IRREVERSIBLE_MIGRATION'
|
|
879
|
+
}));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
tasks.reverse();
|
|
883
|
+
|
|
884
|
+
return Promise.each(tasks, function (task) {
|
|
885
|
+
if (skippedTasks[version] && skippedTasks[version].indexOf(task.name) !== -1) {
|
|
886
|
+
return Promise.resolve();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (onlyTasks.length && onlyTasks.indexOf(task.name) === -1) {
|
|
890
|
+
return Promise.resolve();
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (!task.down) {
|
|
894
|
+
debug('No down function provided', task.name);
|
|
895
|
+
return self.connection('migrations')
|
|
896
|
+
.where({
|
|
897
|
+
name: task.name,
|
|
898
|
+
version: version,
|
|
899
|
+
currentVersion: self.currentVersion
|
|
900
|
+
})
|
|
901
|
+
.delete();
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
debug('Rollback', task.name);
|
|
905
|
+
|
|
906
|
+
if (task.config && task.config.transaction) {
|
|
907
|
+
return database.createTransaction(self.connection, function (txn) {
|
|
908
|
+
return task.down({
|
|
909
|
+
transacting: txn
|
|
910
|
+
});
|
|
911
|
+
}).then(function () {
|
|
912
|
+
return self.connection('migrations')
|
|
913
|
+
.where({
|
|
914
|
+
name: task.name,
|
|
915
|
+
version: version
|
|
916
|
+
})
|
|
917
|
+
.delete();
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
return task.down({
|
|
922
|
+
connection: self.connection
|
|
923
|
+
}).then(function () {
|
|
924
|
+
return self.connection('migrations')
|
|
925
|
+
.where({
|
|
926
|
+
name: task.name,
|
|
927
|
+
version: version
|
|
928
|
+
})
|
|
929
|
+
.delete();
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* @description Private migrate helper.
|
|
936
|
+
*
|
|
937
|
+
* Cases:
|
|
938
|
+
* 1. Init task will use this helper to migrate to "init".
|
|
939
|
+
* 2. Migrate task will use this helper to migrate to a version e.g. "1.1"
|
|
940
|
+
*
|
|
941
|
+
* It will:
|
|
942
|
+
* 1. Read the migration tasks from disk.
|
|
943
|
+
* 2. Execute hooks.
|
|
944
|
+
* 3. Create a transaction for the target migration file if configured. Each migration scripts can run in one transaction.
|
|
945
|
+
* If multiple versions/scripts are executed, we cannot run all of them in a single txn, because implicit commands can happen in between.
|
|
946
|
+
* 4. Execute "up" function of migration file.
|
|
947
|
+
* 5. Returns any skipped task. Skipped tasks are tasks which failed. Only one task is returned, the last one which failed.
|
|
948
|
+
*
|
|
949
|
+
* @param {Object} options - Custom options the user can pass in (version, hooks, only, skip)
|
|
950
|
+
* @returns {Bluebird<{skippedTasks: Array}>}
|
|
951
|
+
* @private
|
|
952
|
+
*/
|
|
953
|
+
KnexMigrator.prototype._migrateTo = function _migrateTo(options) {
|
|
954
|
+
options = options || {};
|
|
955
|
+
|
|
956
|
+
let self = this,
|
|
957
|
+
version = options.version,
|
|
958
|
+
hooks = options.hooks || {},
|
|
959
|
+
only = options.only || null,
|
|
960
|
+
skip = options.skip || null,
|
|
961
|
+
subfolder = this.subfolder,
|
|
962
|
+
skippedTasks = [],
|
|
963
|
+
tasks = [];
|
|
964
|
+
|
|
965
|
+
if (version !== 'init') {
|
|
966
|
+
tasks = utils.readTasks(path.join(self.migrationPath, subfolder, version));
|
|
967
|
+
} else {
|
|
968
|
+
try {
|
|
969
|
+
tasks = utils.readTasks(path.join(self.migrationPath, version));
|
|
970
|
+
} catch (err) {
|
|
971
|
+
if (err.code === 'MIGRATION_PATH') {
|
|
972
|
+
tasks = [];
|
|
973
|
+
} else {
|
|
974
|
+
throw err;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (only !== null) {
|
|
980
|
+
debug('only: ' + only);
|
|
981
|
+
tasks = [tasks[only - 1]];
|
|
982
|
+
} else if (skip !== null) {
|
|
983
|
+
debug('skip: ' + skip);
|
|
984
|
+
tasks.splice(skip - 1, 1);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
debug('Migrate: ' + version + ' with ' + tasks.length + ' tasks.');
|
|
988
|
+
debug('Tasks: ' + JSON.stringify(tasks));
|
|
989
|
+
|
|
990
|
+
return Promise.each(tasks, function executeTask(task) {
|
|
991
|
+
return self._beforeEach({
|
|
992
|
+
task: task.name,
|
|
993
|
+
version: version
|
|
994
|
+
}).then(function () {
|
|
995
|
+
if (hooks.beforeEach) {
|
|
996
|
+
return hooks.beforeEach({
|
|
997
|
+
connection: self.connection
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
}).then(function () {
|
|
1001
|
+
debug('Running up: ' + task.name);
|
|
1002
|
+
|
|
1003
|
+
if (task.config && task.config.transaction) {
|
|
1004
|
+
return database.createTransaction(self.connection, function (txn) {
|
|
1005
|
+
return task.up({
|
|
1006
|
+
transacting: txn
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return task.up({
|
|
1012
|
+
connection: self.connection
|
|
1013
|
+
});
|
|
1014
|
+
}).then(function () {
|
|
1015
|
+
if (hooks.afterEach) {
|
|
1016
|
+
return hooks.afterEach({
|
|
1017
|
+
connection: self.connection
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}).then(function () {
|
|
1021
|
+
return self._afterEach({
|
|
1022
|
+
task: task,
|
|
1023
|
+
version: version
|
|
1024
|
+
});
|
|
1025
|
+
}).catch(function (err) {
|
|
1026
|
+
if (err instanceof errors.MigrationExistsError) {
|
|
1027
|
+
debug('Skipping:' + task.name);
|
|
1028
|
+
skippedTasks.push(task.name);
|
|
1029
|
+
return Promise.resolve();
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* @NOTE: When your database encoding is set to utf8mb4 and you set a field length > 191 characters,
|
|
1034
|
+
* MySQL will throw an error, BUT it won't roll back the changes, because ALTER/CREATE table commands are
|
|
1035
|
+
* implicit commands.
|
|
1036
|
+
*
|
|
1037
|
+
* https://bugs.mysql.com/bug.php?id=28727
|
|
1038
|
+
* https://github.com/TryGhost/knex-migrator/issues/51
|
|
1039
|
+
*/
|
|
1040
|
+
if (err.code === 'ER_TOO_LONG_KEY') {
|
|
1041
|
+
let match = err.message.match(/`\w+`/g);
|
|
1042
|
+
let table = match[0];
|
|
1043
|
+
let field = match[2];
|
|
1044
|
+
|
|
1045
|
+
throw new errors.MigrationScriptError({
|
|
1046
|
+
message: 'Field length of %field% in %table% is too long!'.replace('%field%', field).replace('%table%', table),
|
|
1047
|
+
context: 'This usually happens if your database encoding is utf8mb4.\n' +
|
|
1048
|
+
'All unique fields and indexes must be lower than 191 characters.\n' +
|
|
1049
|
+
'Please correct your field length and reset your database with knex-migrator reset.\n',
|
|
1050
|
+
help: 'Read more here: https://github.com/TryGhost/knex-migrator/issues/51\n',
|
|
1051
|
+
err: err
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
throw new errors.MigrationScriptError({
|
|
1056
|
+
message: err.message,
|
|
1057
|
+
help: 'Error occurred while executing the following migration: ' + task.name,
|
|
1058
|
+
context: task,
|
|
1059
|
+
err: err
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
}).then(function () {
|
|
1063
|
+
return {
|
|
1064
|
+
skippedTasks: skippedTasks
|
|
1065
|
+
};
|
|
1066
|
+
});
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* @description Private helper to execute logic before each migration script is executed.
|
|
1071
|
+
*
|
|
1072
|
+
* It ensures that migration scripts are not executed twice.
|
|
1073
|
+
*
|
|
1074
|
+
* @param {Object} options
|
|
1075
|
+
* @returns {*}
|
|
1076
|
+
* @private
|
|
1077
|
+
*/
|
|
1078
|
+
KnexMigrator.prototype._beforeEach = function _beforeEach(options) {
|
|
1079
|
+
options = options || {};
|
|
1080
|
+
|
|
1081
|
+
let task = options.task,
|
|
1082
|
+
version = options.version;
|
|
1083
|
+
|
|
1084
|
+
return this.connection('migrations')
|
|
1085
|
+
.then(function (migrations) {
|
|
1086
|
+
if (!migrations.length) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (_.find(migrations, {name: task, version: version})) {
|
|
1091
|
+
throw new errors.MigrationExistsError();
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* @description Private helper to execute logic after each migration script is executed.
|
|
1098
|
+
*
|
|
1099
|
+
* It ensures that migration files are inserted into the database.
|
|
1100
|
+
*
|
|
1101
|
+
* @param {Object} options
|
|
1102
|
+
* @returns {*}
|
|
1103
|
+
* @private
|
|
1104
|
+
*/
|
|
1105
|
+
KnexMigrator.prototype._afterEach = function _afterEach(options) {
|
|
1106
|
+
options = options || {};
|
|
1107
|
+
|
|
1108
|
+
let self = this;
|
|
1109
|
+
let task = options.task;
|
|
1110
|
+
let version = options.version;
|
|
1111
|
+
|
|
1112
|
+
return this.connection('migrations')
|
|
1113
|
+
.insert({
|
|
1114
|
+
name: task.name,
|
|
1115
|
+
version: version,
|
|
1116
|
+
currentVersion: self.currentVersion
|
|
1117
|
+
});
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* @description Private helper to execute an integrity check. The integrity check compares files against entries in the
|
|
1122
|
+
* database. It returns expected and actual database state.
|
|
1123
|
+
*
|
|
1124
|
+
* @param {Object} options - Custom user options (force)
|
|
1125
|
+
* @returns {Bluebird<any>}
|
|
1126
|
+
* @private
|
|
1127
|
+
*/
|
|
1128
|
+
KnexMigrator.prototype._integrityCheck = function _integrityCheck(options) {
|
|
1129
|
+
options = options || {};
|
|
1130
|
+
|
|
1131
|
+
let self = this,
|
|
1132
|
+
subfolder = this.subfolder,
|
|
1133
|
+
force = options.force,
|
|
1134
|
+
folders = [],
|
|
1135
|
+
toReturn = {},
|
|
1136
|
+
futureVersions = [];
|
|
1137
|
+
|
|
1138
|
+
// CASE: we always fetch the init scripts and check them
|
|
1139
|
+
// 1. to be able to add more init scripts
|
|
1140
|
+
// 2. to check if migration scripts need's to be executed or not, see https://github.com/TryGhost/knex-migrator/issues/39
|
|
1141
|
+
folders.push('init');
|
|
1142
|
+
|
|
1143
|
+
// CASE: no subfolder yet. You can tell knex-migrator if scripts live on a sub folder.
|
|
1144
|
+
try {
|
|
1145
|
+
folders = folders.concat(utils.readVersionFolders(path.join(self.migrationPath, subfolder)));
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
// ignore
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
return this
|
|
1151
|
+
.connection('migrations')
|
|
1152
|
+
.select('version')
|
|
1153
|
+
.count('version', {as: 'c'})
|
|
1154
|
+
.groupBy('version')
|
|
1155
|
+
.then((dbMigrations) => {
|
|
1156
|
+
_.each(folders, function (folder) {
|
|
1157
|
+
// CASE: versions/1.1-members or versions/2.0-payments
|
|
1158
|
+
if (folder !== 'init') {
|
|
1159
|
+
try {
|
|
1160
|
+
folder = folder.match(/([\d._]+)/)[0];
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
logging.warn('Cannot parse folder name.');
|
|
1163
|
+
logging.warn('Ignore Folder: ' + folder);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// CASE:
|
|
1169
|
+
// if your current version is 1.0 and you add migration scripts for the next version 1.1
|
|
1170
|
+
// we won't execute them until your current version changes to 1.1 or until you force KM to migrate to it
|
|
1171
|
+
if (self.currentVersion && !force) {
|
|
1172
|
+
if (utils.isGreaterThanVersion({smallerVersion: self.currentVersion, greaterVersion: folder})) {
|
|
1173
|
+
futureVersions.push(folder);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
let actual = 0;
|
|
1178
|
+
let expected;
|
|
1179
|
+
|
|
1180
|
+
const migrationCount = dbMigrations.find(m => m.version === folder);
|
|
1181
|
+
if (migrationCount) {
|
|
1182
|
+
actual = migrationCount.c;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
if (folder !== 'init') {
|
|
1186
|
+
expected = utils.listFiles(path.join(self.migrationPath, subfolder, folder)).length;
|
|
1187
|
+
} else {
|
|
1188
|
+
expected = utils.listFiles(path.join(self.migrationPath, folder)).length;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
debug('Version ' + folder + ' expected: ' + expected);
|
|
1192
|
+
debug('Version ' + folder + ' actual: ' + actual);
|
|
1193
|
+
|
|
1194
|
+
toReturn[folder] = {
|
|
1195
|
+
expected: expected,
|
|
1196
|
+
actual: actual
|
|
1197
|
+
};
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// CASE: ensure that either you have to run `migrate --force` or they ran already
|
|
1201
|
+
if (futureVersions.length) {
|
|
1202
|
+
_.each(futureVersions, function (futureVersion) {
|
|
1203
|
+
if (toReturn[futureVersion].actual !== toReturn[futureVersion].expected) {
|
|
1204
|
+
logging.warn('knex-migrator is skipping ' + futureVersion);
|
|
1205
|
+
logging.warn('Current version in MigratorConfig.js is smaller then requested version, use --force to proceed!');
|
|
1206
|
+
logging.warn('Please run `knex-migrator migrate --v ' + futureVersion + ' --force` to proceed!');
|
|
1207
|
+
delete toReturn[futureVersion];
|
|
1208
|
+
}
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
return toReturn;
|
|
1213
|
+
}).catch(function onMigrationsLookupError(err) {
|
|
1214
|
+
// CASE: no database selected (database.connection.database="")
|
|
1215
|
+
if (err.errno === 1046) {
|
|
1216
|
+
throw new errors.DatabaseIsNotOkError({
|
|
1217
|
+
message: 'Please define a target database in your configuration.',
|
|
1218
|
+
help: 'database: {\n\tconnection:\n\t\tdatabase:"database_name"\n\t}\n}\n',
|
|
1219
|
+
code: 'DB_NOT_INITIALISED'
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// CASE: database does not exist
|
|
1224
|
+
if (err.errno === 1049) {
|
|
1225
|
+
throw new errors.DatabaseIsNotOkError({
|
|
1226
|
+
message: 'Please run knex-migrator init',
|
|
1227
|
+
code: 'DB_NOT_INITIALISED'
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// CASE: migration table does not exist
|
|
1232
|
+
if (err.errno === 1 || err.errno === 1146) {
|
|
1233
|
+
throw new errors.DatabaseIsNotOkError({
|
|
1234
|
+
message: 'Please run knex-migrator init',
|
|
1235
|
+
code: 'MIGRATION_TABLE_IS_MISSING'
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
throw err;
|
|
1240
|
+
});
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
module.exports = KnexMigrator;
|