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/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;