knex-migrator 4.0.4 → 4.1.2

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.
Files changed (3) hide show
  1. package/README.md +12 -12
  2. package/lib/index.js +182 -208
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -6,14 +6,14 @@ A database migration tool for [knex.js](https://github.com/tgriesser/knex), whic
6
6
 
7
7
  - [x] JS API
8
8
  - [x] CLI Tool
9
- - [x] Differentiation between database initialisation and migration (Support for a database schema, [like we use in Ghost](https://github.com/TryGhost/Ghost/blob/1.16.2/core/server/data/schema/schema.js))
9
+ - [x] Differentiation between database initialization and migration (Support for a database schema, [like we use in Ghost](https://github.com/TryGhost/Ghost/blob/1.16.2/core/server/data/schema/schema.js))
10
10
  - [x] Support for database creation
11
11
  - [x] Hooks
12
12
  - [x] Rollback to latest version
13
13
  - [x] Auto-Rollback on error
14
14
  - [x] Database health check
15
15
  - [x] Supports transactions
16
- - [x] Full atomic, support for separate DML/DDL scripts (no autocommit)
16
+ - [x] Full atomic, support for separate DML/DDL scripts (no autocommit)
17
17
  - [x] Migration lock
18
18
  - [x] Full debug & pretty log support
19
19
  - [x] Custom migration folder structure
@@ -36,8 +36,8 @@ Add me to your globals:
36
36
 
37
37
  - Replicas are unsupported, because Knex.js [doesn't support them](https://github.com/tgriesser/knex/issues/2253).
38
38
  - Sqlite does **not** support read locks by default. Read [here](https://github.com/TryGhost/knex-migrator/issues/87) why.
39
- - [Comparision](https://github.com/TryGhost/knex-migrator/issues/119) with other available migration tools.
40
- - Don't mix DDL/DML statements in a migration script. In MySQL DDL statements use implicit commits.
39
+ - [Comparison](https://github.com/TryGhost/knex-migrator/issues/119) with other available migration tools.
40
+ - Don't mix DDL/DML statements in a migration script. In MySQL DDL statements use implicit commits.
41
41
  - It's highly recommended to write both the `up` and the `down` function to ensure a full rollback.
42
42
  - If your process dies while migrations are running, knex-migrator won't be able to release the migration lock.
43
43
  To release to lock you can run `knex-migrator rollback`. **But** it's recommended to check your database first to see in which state it is.
@@ -73,7 +73,7 @@ Please take a look at [this real example](https://github.com/TryGhost/Ghost/blob
73
73
 
74
74
  ```
75
75
  project/
76
- migrations/
76
+ migrations/
77
77
  hooks/
78
78
  init/
79
79
  index.js
@@ -88,9 +88,9 @@ project/
88
88
  versions/
89
89
  1.0/
90
90
  1-add-events-table.js
91
- 2-normalise-settings.js
91
+ 2-normalise-settings.js
92
92
  2.0/
93
- 1-add-timestamps-columns.js
93
+ 1-add-timestamps-columns.js
94
94
  2.1/
95
95
  1-remove-empty-strings.js
96
96
  2-add-webhooks-table.js
@@ -199,7 +199,7 @@ Commands:
199
199
 
200
200
  #### knex-migrator init
201
201
 
202
- - Initialises your database based on your init scripts
202
+ - Initializes your database based on your init scripts
203
203
  - Creates the database if it was not created yet
204
204
 
205
205
  ##### Options
@@ -223,10 +223,10 @@ Commands:
223
223
  ##### Options
224
224
 
225
225
  ```bash
226
- # The bersion you would like to migrate to
226
+ # The version you would like to migrate to
227
227
  --v
228
228
 
229
- # Combo Feature to check whether the database was already initialised
229
+ # Combo Feature to check whether the database was already initialized
230
230
  --init
231
231
 
232
232
  # Force the execution no matter which current version you are on
@@ -312,13 +312,13 @@ knexMigrator.reset
312
312
  knexMigrator.isDatabaseOK()
313
313
  .then(function() {
314
314
  // database is OK
315
- // initialisation & migrations are not missing
315
+ // initialization & migrations are not missing
316
316
  })
317
317
  .catch(function(err) {
318
318
  if (err.code === 'DB_NOT_INITIALISED') {
319
319
  return knexMigrator.init();
320
320
  }
321
-
321
+
322
322
  if (err.code === 'DB_NEEDS_MIGRATION') {
323
323
  return knexMigrator.migrate();
324
324
  }
package/lib/index.js CHANGED
@@ -155,30 +155,28 @@ KnexMigrator.prototype.init = function init(options) {
155
155
  throw err;
156
156
  }
157
157
 
158
- return database.createTransaction(self.connection, function (transacting) {
158
+ return database.createTransaction(self.connection, async function (transacting) {
159
+ const existingMigrations = await transacting('migrations').select('name');
160
+ const existingMigrationsNames = existingMigrations.map(m => m.name);
161
+
159
162
  // CASE: Run over all migration scripts and add the file name to the database.
160
- return Promise.each(versionsToMigrateTo, function (versionToMigrateTo) {
163
+ for (const versionToMigrateTo of versionsToMigrateTo) {
161
164
  let versionPath = path.join(self.migrationPath, self.subfolder, versionToMigrateTo);
162
165
  let filesToMigrateTo = utils.listFiles(versionPath) || [];
163
166
 
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
- });
167
+ // CASE: check if migration exists, do not insert twice
168
+ for (const name of filesToMigrateTo) {
169
+ if (existingMigrationsNames.includes(name)) {
170
+ continue;
171
+ }
172
+
173
+ await transacting('migrations').insert({
174
+ name,
175
+ version: versionToMigrateTo,
176
+ currentVersion: self.currentVersion
177
+ });
178
+ }
179
+ }
182
180
  });
183
181
  })
184
182
  .then(function () {
@@ -286,11 +284,10 @@ KnexMigrator.prototype.init = function init(options) {
286
284
  * @param {Object} options - Custom options you can pass in (version, force, init, only, skip)
287
285
  * @returns {Bluebird<any>}
288
286
  */
289
- KnexMigrator.prototype.migrate = function migrate(options) {
287
+ KnexMigrator.prototype.migrate = async function migrate(options) {
290
288
  options = options || {};
291
289
 
292
- let self = this,
293
- onlyVersion = options.version,
290
+ let onlyVersion = options.version,
294
291
  force = options.force,
295
292
  init = options.init,
296
293
  onlyFile = options.only,
@@ -308,14 +305,11 @@ KnexMigrator.prototype.migrate = function migrate(options) {
308
305
 
309
306
  // CASE: `--init` flag is passed. Combo feature.
310
307
  if (init) {
311
- return this.init()
312
- .then(function () {
313
- return self.migrate(_.omit(options, 'init'));
314
- });
308
+ await this.init();
315
309
  }
316
310
 
317
311
  try {
318
- hooks = require(path.join(self.migrationPath, '/hooks/migrate'));
312
+ hooks = require(path.join(this.migrationPath, '/hooks/migrate'));
319
313
  } catch (err) {
320
314
  debug('Hook Error: ' + err.message);
321
315
  debug('No hooks found, no problem.');
@@ -323,152 +317,130 @@ KnexMigrator.prototype.migrate = function migrate(options) {
323
317
 
324
318
  this.connection = database.connect(this.dbConfig);
325
319
 
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
- });
320
+ try {
321
+ await database.ensureConnectionWorks(this.connection);
346
322
 
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
- });
323
+ await migrations.run(this.connection);
352
324
 
353
- if (!containsVersion) {
354
- logging.warn('Cannot find requested version: ' + onlyVersion);
355
- }
356
- }
325
+ await locking.lock(this.connection);
357
326
 
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
- }
327
+ const result = await this._integrityCheck({force});
370
328
 
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) {
329
+ _.each(result, function (_value, version) {
330
+ // CASE: Log which versions won't be executed based on the "only" flag
331
+ if (onlyVersion && version !== onlyVersion) {
332
+ debug('Do not execute: ' + version);
380
333
  return;
381
334
  }
335
+ });
382
336
 
383
- return Promise.each(versionsToMigrate, function (versionToMigrate) {
384
- return self._migrateTo({
385
- version: versionToMigrate,
386
- only: onlyFile,
387
- hooks: hooks
388
- });
337
+ if (onlyVersion) {
338
+ // CASE: filter out versions which should not run
339
+ let containsVersion = _.find(result, function (_obj, key) {
340
+ return key === onlyVersion;
389
341
  });
390
- })
391
- .then(function executeAfterHook() {
392
- if (!versionsToMigrate.length) {
393
- return;
394
- }
395
342
 
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;
343
+ if (!containsVersion) {
344
+ logging.warn('Cannot find requested version: ' + onlyVersion);
410
345
  }
346
+ }
411
347
 
412
- // CASE: Do not rollback migration scripts, if lock error
413
- if (err instanceof errors.LockError) {
414
- throw err;
348
+ _.each(result, function (value, version) {
349
+ // CASE: compare files on disk with files in database
350
+ if (value.expected !== value.actual) {
351
+ debug('Need to execute migrations for: ' + version);
352
+ versionsToMigrate.push(version);
415
353
  }
354
+ });
416
355
 
417
- // CASE: ETIMEDOUT, ENOTFOUND
418
- if (err instanceof errors.DatabaseError) {
419
- throw err;
356
+ if (versionsToMigrate.length) {
357
+ if (hooks.before) {
358
+ debug('Before hook');
359
+ await hooks.before({
360
+ connection: this.connection
361
+ });
420
362
  }
421
363
 
422
- if (err.context && err.context.name) {
423
- debug(`Task failed: ${err.context.name}`);
424
- }
364
+ logging.info('Running migrations.');
425
365
 
426
- debug(`Rolling back: ${err.message}`);
366
+ for (const versionToMigrate of versionsToMigrate) {
367
+ try {
368
+ await this._migrateTo({
369
+ version: versionToMigrate,
370
+ only: onlyFile,
371
+ hooks: hooks
372
+ });
373
+ } catch (err) {
374
+ // CASE: Do not rollback if migrations are locked
375
+ if (err instanceof errors.MigrationsAreLockedError) {
376
+ throw err;
377
+ }
427
378
 
428
- versionsToMigrate.reverse();
379
+ // CASE: Do not rollback migration scripts, if lock error
380
+ if (err instanceof errors.LockError) {
381
+ throw err;
382
+ }
429
383
 
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
- }
384
+ // CASE: ETIMEDOUT, ENOTFOUND
385
+ if (err instanceof errors.DatabaseError) {
386
+ throw err;
387
+ }
439
388
 
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 = [];
389
+ if (err.context && err.context.name) {
390
+ debug(`Task failed: ${err.context.name}`);
391
+ }
450
392
 
451
- if (hooks.shutdown) {
452
- ops.push(function shutdownHook() {
453
- debug('Shutdown hook');
454
- return hooks.shutdown({
455
- executedFromShell: self.executedFromShell
456
- });
457
- });
393
+ logging.info(`Rolling back: ${err.message}.`);
394
+
395
+ const versionsMigrated = versionsToMigrate.slice(
396
+ 0,
397
+ versionsToMigrate.indexOf(versionToMigrate) + 1
398
+ );
399
+
400
+ versionsMigrated.reverse();
401
+
402
+ try {
403
+ for (const versionMigrated of versionsMigrated) {
404
+ await this._rollback({version: versionMigrated, task: err.context});
405
+ }
406
+ logging.info(`Rollback was successful.`);
407
+ throw err;
408
+ } catch (innerErr) {
409
+ if (errors.utils.isIgnitionError(innerErr)) {
410
+ throw err;
411
+ }
412
+
413
+ throw new errors.RollbackError({
414
+ message: innerErr.message,
415
+ err: innerErr,
416
+ context: `OuterError: ${err.message}`
417
+ });
418
+ } finally {
419
+ await locking.unlock(this.connection);
420
+ }
421
+ }
458
422
  }
459
423
 
460
- ops.push(function destroyConnection() {
461
- debug('Destroy connection');
462
- return self.connection.destroy()
463
- .then(function () {
464
- debug('Destroyed connection');
465
- });
424
+ if (hooks.after) {
425
+ debug('After hook');
426
+ await hooks.after({
427
+ connection: this.connection
428
+ });
429
+ }
430
+ }
431
+ await locking.unlock(this.connection);
432
+ } finally {
433
+ if (hooks.shutdown) {
434
+ debug('Shutdown hook');
435
+ await hooks.shutdown({
436
+ executedFromShell: this.executedFromShell
466
437
  });
438
+ }
467
439
 
468
- return Promise.each(ops, function (op) {
469
- return op.bind(self)();
470
- });
471
- });
440
+ debug('Destroy connection');
441
+ await this.connection.destroy();
442
+ debug('Destroyed connection');
443
+ }
472
444
  };
473
445
 
474
446
  /**
@@ -1132,7 +1104,6 @@ KnexMigrator.prototype._integrityCheck = function _integrityCheck(options) {
1132
1104
  subfolder = this.subfolder,
1133
1105
  force = options.force,
1134
1106
  folders = [],
1135
- operations = {},
1136
1107
  toReturn = {},
1137
1108
  futureVersions = [];
1138
1109
 
@@ -1148,29 +1119,69 @@ KnexMigrator.prototype._integrityCheck = function _integrityCheck(options) {
1148
1119
  // ignore
1149
1120
  }
1150
1121
 
1151
- _.each(folders, function (folder) {
1152
- // CASE: versions/1.1-members or versions/2.0-payments
1153
- if (folder !== 'init') {
1154
- try {
1155
- folder = folder.match(/([\d._]+)/)[0];
1156
- } catch (err) {
1157
- logging.warn('Cannot parse folder name.');
1158
- logging.warn('Ignore Folder: ' + folder);
1159
- return;
1160
- }
1161
- }
1122
+ return this
1123
+ .connection('migrations')
1124
+ .select('version')
1125
+ .count('version', {as: 'c'})
1126
+ .groupBy('version')
1127
+ .then((dbMigrations) => {
1128
+ _.each(folders, function (folder) {
1129
+ // CASE: versions/1.1-members or versions/2.0-payments
1130
+ if (folder !== 'init') {
1131
+ try {
1132
+ folder = folder.match(/([\d._]+)/)[0];
1133
+ } catch (err) {
1134
+ logging.warn('Cannot parse folder name.');
1135
+ logging.warn('Ignore Folder: ' + folder);
1136
+ return;
1137
+ }
1138
+ }
1162
1139
 
1163
- // CASE:
1164
- // if your current version is 1.0 and you add migration scripts for the next version 1.1
1165
- // we won't execute them until your current version changes to 1.1 or until you force KM to migrate to it
1166
- if (self.currentVersion && !force) {
1167
- if (utils.isGreaterThanVersion({smallerVersion: self.currentVersion, greaterVersion: folder})) {
1168
- futureVersions.push(folder);
1140
+ // CASE:
1141
+ // if your current version is 1.0 and you add migration scripts for the next version 1.1
1142
+ // we won't execute them until your current version changes to 1.1 or until you force KM to migrate to it
1143
+ if (self.currentVersion && !force) {
1144
+ if (utils.isGreaterThanVersion({smallerVersion: self.currentVersion, greaterVersion: folder})) {
1145
+ futureVersions.push(folder);
1146
+ }
1147
+ }
1148
+
1149
+ let actual = 0;
1150
+ let expected;
1151
+
1152
+ const migrationCount = dbMigrations.find(m => m.version === folder);
1153
+ if (migrationCount) {
1154
+ actual = migrationCount.c;
1155
+ }
1156
+
1157
+ if (folder !== 'init') {
1158
+ expected = utils.listFiles(path.join(self.migrationPath, subfolder, folder)).length;
1159
+ } else {
1160
+ expected = utils.listFiles(path.join(self.migrationPath, folder)).length;
1161
+ }
1162
+
1163
+ debug('Version ' + folder + ' expected: ' + expected);
1164
+ debug('Version ' + folder + ' actual: ' + actual);
1165
+
1166
+ toReturn[folder] = {
1167
+ expected: expected,
1168
+ actual: actual
1169
+ };
1170
+ });
1171
+
1172
+ // CASE: ensure that either you have to run `migrate --force` or they ran already
1173
+ if (futureVersions.length) {
1174
+ _.each(futureVersions, function (futureVersion) {
1175
+ if (toReturn[futureVersion].actual !== toReturn[futureVersion].expected) {
1176
+ logging.warn('knex-migrator is skipping ' + futureVersion);
1177
+ logging.warn('Current version in MigratorConfig.js is smaller then requested version, use --force to proceed!');
1178
+ logging.warn('Please run `knex-migrator migrate --v ' + futureVersion + ' --force` to proceed!');
1179
+ delete toReturn[futureVersion];
1180
+ }
1181
+ });
1169
1182
  }
1170
- }
1171
1183
 
1172
- operations[folder] = self.connection('migrations').where({
1173
- version: folder
1184
+ return toReturn;
1174
1185
  }).catch(function onMigrationsLookupError(err) {
1175
1186
  // CASE: no database selected (database.connection.database="")
1176
1187
  if (err.errno === 1046) {
@@ -1199,43 +1210,6 @@ KnexMigrator.prototype._integrityCheck = function _integrityCheck(options) {
1199
1210
 
1200
1211
  throw err;
1201
1212
  });
1202
- });
1203
-
1204
- return Promise.props(operations)
1205
- .then(function (result) {
1206
- _.each(result, function (value, version) {
1207
- let actual = value.length,
1208
- expected = actual;
1209
-
1210
- if (version !== 'init') {
1211
- expected = utils.listFiles(path.join(self.migrationPath, subfolder, version)).length;
1212
- } else {
1213
- expected = utils.listFiles(path.join(self.migrationPath, version)).length;
1214
- }
1215
-
1216
- debug('Version ' + version + ' expected: ' + expected);
1217
- debug('Version ' + version + ' actual: ' + actual);
1218
-
1219
- toReturn[version] = {
1220
- expected: expected,
1221
- actual: actual
1222
- };
1223
- });
1224
-
1225
- // CASE: ensure that either you have to run `migrate --force` or they ran already
1226
- if (futureVersions.length) {
1227
- _.each(futureVersions, function (futureVersion) {
1228
- if (toReturn[futureVersion].actual !== toReturn[futureVersion].expected) {
1229
- logging.warn('knex-migrator is skipping ' + futureVersion);
1230
- logging.warn('Current version in MigratorConfig.js is smaller then requested version, use --force to proceed!');
1231
- logging.warn('Please run `knex-migrator migrate --v ' + futureVersion + ' --force` to proceed!');
1232
- delete toReturn[futureVersion];
1233
- }
1234
- });
1235
- }
1236
-
1237
- return toReturn;
1238
- });
1239
1213
  };
1240
1214
 
1241
1215
  module.exports = KnexMigrator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knex-migrator",
3
- "version": "4.0.4",
3
+ "version": "4.1.2",
4
4
  "description": "Database migrations with knex.",
5
5
  "keywords": [
6
6
  "ghost",
@@ -44,18 +44,18 @@
44
44
  "knex-migrator-rollback": "./bin/knex-migrator-rollback"
45
45
  },
46
46
  "engines": {
47
- "node": "^10.13.0 || ^12.10.0 || ^14.14.0"
47
+ "node": "^12.22.1 || ^14.17.0 || ^16.13.0"
48
48
  },
49
49
  "dependencies": {
50
50
  "bluebird": "3.7.2",
51
51
  "commander": "5.1.0",
52
52
  "compare-ver": "2.0.2",
53
- "debug": "4.3.1",
54
- "ghost-ignition": "4.6.2",
53
+ "debug": "4.3.2",
54
+ "ghost-ignition": "4.6.3",
55
55
  "knex": "0.21.19",
56
56
  "lodash": "4.17.21",
57
57
  "moment": "2.24.0",
58
- "nconf": "0.11.2",
58
+ "nconf": "0.11.3",
59
59
  "resolve": "1.20.0"
60
60
  },
61
61
  "files": [