rittenhop-dev/versions/5.94.2/node_modules/knex-migrator/lib/index.js

1222 lines
43 KiB
JavaScript
Raw Normal View History

2024-09-23 19:40:12 -04:00
const _ = require('lodash');
const path = require('path');
const {sequence} = require('@tryghost/promise');
const debug = require('debug')('knex-migrator:index');
const database = require('./database');
const utils = require('./utils');
const errors = require('./errors');
const logging = require('@tryghost/logging');
const migrations = require('../migrations');
const locking = require('./locking');
/**
* @description Prototype of Knex-migrator.
* @param {Object} options
* @constructor
*/
function KnexMigrator(options = {}) {
let config = utils.loadConfig(options);
if (!config.database) {
throw new Error('MigratorConfig.js needs to export a database config.');
}
if (!config.migrationPath) {
throw new Error('MigratorConfig.js needs to export the location of your migration files.');
}
if (!config.currentVersion) {
throw new Error('MigratorConfig.js needs to export the a current version.');
}
this.executedFromShell = options.executedFromShell;
this.currentVersion = config.currentVersion;
this.migrationPath = config.migrationPath;
this.subfolder = config.subfolder || 'versions';
this.dbConfig = config.database;
}
/**
* `knex-migrator init`
*
* @description This task will run init scripts.
*
* The `init` command goes through the following steps:
*
* 1. Create a database knex connection.
* 2. Create database if it does not exist.
* 3. Create tables for knex-migrator (migrations, migrations-lock)
* 4. Run table upgrades/migrations if available.
* 5. Lock the tables to avoid running migrations in parallel.
* 6. Execute hooks and init scripts.
* 7. Init completion: add all existing migration scripts to the database to make it possible to detect a state of the database correctly.
* 8. Unlock tables.
* 9. Disconnect from database otherwise the CLI won't close properly.
*
* The `init` command can be triggered via `knex-migrator migrate --init`.
* This is a feature which makes it easier to not having to differentiate if a database needs a migration or an
* initilisation. The special characteristic of this combo is that the init completation shouldn't run, because
* otherwise we would overjump migration files to execute.
*
* @param {Object} options - Custom options you can pass in (disableHooks, noScripts, skipInitCompletion, only, skip)
* @returns {Promise<R>}
*/
KnexMigrator.prototype.init = function init(options) {
options = options || {};
let self = this,
disableHooks = options.disableHooks,
noScripts = options.noScripts,
skipInitCompletion = options.skipInitCompletion,
skippedTasks = [],
hooks = {};
try {
if (!disableHooks) {
hooks = require(path.join(self.migrationPath, '/hooks/init'));
}
} catch (err) {
debug('Hook Error: ' + err.message);
debug('No hooks found, no problem.');
}
this.connection = database.connect(this.dbConfig);
return database.createDatabaseIfNotExist(self.dbConfig)
.then(function () {
if (noScripts) {
return;
}
// @NOTE: create table outside of the transaction! (implicit)
return database.createMigrationsTable(self.connection).then(function () {
return migrations.run(self.connection).then(function () {
return locking.lock(self.connection)
.then(function () {
if (hooks.before) {
debug('Before hook');
return hooks.before({
connection: self.connection
});
}
})
.then(function executeMigrate() {
return self._migrateTo({
version: 'init',
only: options.only,
skip: options.skip
});
})
.then(function (response) {
skippedTasks = response.skippedTasks;
if (hooks.after) {
debug('After hook');
return hooks.after({
connection: self.connection
});
}
})
.then(function () {
const initTasks = utils.listFiles(path.join(self.migrationPath, 'init'));
/**
* CASE 1: You can disable init completion manually
* CASE 2: Only skip init completion if you have all init scripts in place already!!
*
* Example:
* - knex-migrator migrate --init should not execute init completion
* -> does not run init completion
*
* Example:
* - knex-migrator init (process was destroyed, you only have 1 init script in your db)
* - knex-migrator init (the other init script get's executed)
* -> run init completion
*
*/
if (skippedTasks.length === initTasks.length || skipInitCompletion) {
return Promise.resolve();
}
let versionsToMigrateTo;
// CASE: insert all migration files, otherwise you will run into problems
// e.g. you are on 1.2, you initialise the database, but there is 1.3 migration script
try {
versionsToMigrateTo = utils.readVersionFolders(
path.join(self.migrationPath, self.subfolder)) || [];
} catch (err) {
// CASE: versions folder does not exists
if (err.code === 'READ_FOLDERS') {
return Promise.resolve();
}
throw err;
}
return database.createTransaction(self.connection, async function (transacting) {
const existingMigrations = await transacting('migrations').select('name');
const existingMigrationsNames = existingMigrations.map(m => m.name);
const migrationsToAdd = [];
// CASE: Run over all migration scripts and add the file name to the database.
for (const versionToMigrateTo of versionsToMigrateTo) {
let versionPath = path.join(self.migrationPath, self.subfolder, versionToMigrateTo);
let filesToMigrateTo = utils.listFiles(versionPath) || [];
// CASE: check if migration exists, do not insert twice
for (const name of filesToMigrateTo) {
if (existingMigrationsNames.includes(name)) {
continue;
}
migrationsToAdd.push({
name,
version: versionToMigrateTo,
currentVersion: self.currentVersion
});
}
}
await self.connection
.batchInsert('migrations', migrationsToAdd)
.transacting(transacting);
});
})
.then(function () {
return locking.unlock(self.connection);
})
.catch(function (err) {
if (err instanceof errors.MigrationsAreLockedError) {
throw err;
}
if (err instanceof errors.LockError) {
throw err;
}
return locking.unlock(self.connection)
.then(function () {
throw err;
});
});
});
});
})
.then(function onInitSuccess() {
debug('Init Success');
})
.catch(function onInitError(err) {
// CASE: Do not rollback if migrations are locked
if (err instanceof errors.MigrationsAreLockedError) {
throw err;
}
// CASE: Do not rollback migration scripts, if lock error
if (err instanceof errors.LockError) {
throw err;
}
// CASE: ETIMEDOUT, ENOTFOUND
if (err instanceof errors.DatabaseError) {
throw err;
}
debug('Rolling back: ' + err.message);
return self._rollback({
version: 'init',
skippedTasks: {
init: skippedTasks
}
}).then(function () {
throw err;
}).catch(function (innerErr) {
if (errors.utils.isGhostError(innerErr)) {
throw err;
}
throw new errors.RollbackError({
message: innerErr.message,
err: innerErr,
context: `OuterError: ${err.message}`
});
});
})
.finally(function () {
let ops = [];
if (hooks.shutdown) {
ops.push(function shutdownHook() {
debug('Shutdown hook');
return hooks.shutdown({
executedFromShell: self.executedFromShell
});
});
}
ops.push(function destroyConnection() {
debug('Destroy connection');
return self.connection.destroy()
.then(function () {
debug('Destroyed connection');
});
});
return sequence(ops.map(op => () => {
return op.bind(self)();
}));
});
};
/**
* `knex-migrator migrate`
*
* @description This task will run migration scripts.
*
* The `migrate` task runs through the following steps:
*
* 1. Create a database knex connection.
* 2. Ensure connection works as expected.
* 3. Run table upgrades/migrations if available.
* 4. Lock the target tables to avoid running migrations in parallel.
* 5. Perform an integrity check to figure out which migrations need to run (compare files against database)
* 6. Execute migrations.
* 7. Unlock table.
* 8. Disconnect from database otherwise the CLI won't close properly.
*
* @param {Object} options - Custom options you can pass in (version, force, init, only, skip)
* @returns {Promise<any>}
*/
KnexMigrator.prototype.migrate = async function migrate(options) {
options = options || {};
let onlyVersion = options.version,
force = options.force,
init = options.init,
onlyFile = options.only,
versionsToMigrate = [],
hooks = {};
// CASE: you can only use only in combination with the version flag
if (onlyFile && !onlyVersion) {
onlyFile = null;
}
if (onlyVersion) {
debug('onlyVersion: ' + onlyVersion);
}
// CASE: `--init` flag is passed. Combo feature.
if (init) {
await this.init();
}
try {
hooks = require(path.join(this.migrationPath, '/hooks/migrate'));
} catch (err) {
debug('Hook Error: ' + err.message);
debug('No hooks found, no problem.');
}
this.connection = database.connect(this.dbConfig);
try {
await database.ensureConnectionWorks(this.connection);
await migrations.run(this.connection);
await locking.lock(this.connection);
const result = await this._integrityCheck({force});
_.each(result, function (_value, version) {
// CASE: Log which versions won't be executed based on the "only" flag
if (onlyVersion && version !== onlyVersion) {
debug('Do not execute: ' + version);
return;
}
});
if (onlyVersion) {
// CASE: filter out versions which should not run
let containsVersion = _.find(result, function (_obj, key) {
return key === onlyVersion;
});
if (!containsVersion) {
logging.warn('Cannot find requested version: ' + onlyVersion);
}
}
_.each(result, function (value, version) {
// CASE: compare files on disk with files in database
if (value.expected !== value.actual) {
debug('Need to execute migrations for: ' + version);
versionsToMigrate.push(version);
}
});
if (versionsToMigrate.length) {
if (hooks.before) {
debug('Before hook');
await hooks.before({
connection: this.connection
});
}
logging.info('Running migrations.');
for (const versionToMigrate of versionsToMigrate) {
try {
await this._migrateTo({
version: versionToMigrate,
only: onlyFile,
hooks: hooks
});
} catch (err) {
// CASE: Do not rollback if migrations are locked
if (err instanceof errors.MigrationsAreLockedError) {
throw err;
}
// CASE: Do not rollback migration scripts, if lock error
if (err instanceof errors.LockError) {
throw err;
}
// CASE: ETIMEDOUT, ENOTFOUND
if (err instanceof errors.DatabaseError) {
throw err;
}
if (err.context && err.context.name) {
debug(`Task failed: ${err.context.name}`);
}
logging.info(`Rolling back: ${err.message}.`);
const versionsMigrated = versionsToMigrate.slice(
0,
versionsToMigrate.indexOf(versionToMigrate) + 1
);
versionsMigrated.reverse();
try {
for (const versionMigrated of versionsMigrated) {
await this._rollback({version: versionMigrated, task: err.context});
}
logging.info(`Rollback was successful.`);
throw err;
} catch (innerErr) {
if (errors.utils.isGhostError(innerErr)) {
throw err;
}
throw new errors.RollbackError({
message: innerErr.message,
err: innerErr,
context: `OuterError: ${err.message}`
});
} finally {
await locking.unlock(this.connection);
}
}
}
if (hooks.after) {
debug('After hook');
await hooks.after({
connection: this.connection
});
}
}
await locking.unlock(this.connection);
} finally {
if (hooks.shutdown) {
debug('Shutdown hook');
await hooks.shutdown({
executedFromShell: this.executedFromShell
});
}
debug('Destroy connection');
await this.connection.destroy();
debug('Destroyed connection');
}
};
/**
* `knex-migrator reset`
*
* @description The rest command will do a full hard reset.
*
* It will:
*
* 1. Create a connection to the database.
* 2. Ensure connection works as expected.
* 3. Lock the table to avoid running reset in parallel.
* 4. Run table upgrades/migrations if available.
* 5. Drop the database.
*
* If you pass the "force" flag, you will skip step 2-4.
*
* @param {Object} options - Custom options the user can pass in (force)
* @returns {*}
*/
KnexMigrator.prototype.reset = function reset(options) {
options = options || {};
let self = this;
let force = options.force;
this.connection = database.connect(this.dbConfig);
// CASE: ignore lock completely and drop the db
if (force) {
return database.drop({
connection: self.connection,
dbConfig: self.dbConfig
}).catch(function onRestError(err) {
// Database does not exist. MySql.
if (err.errno === 1049) {
return Promise.resolve();
}
throw err;
}).finally(function () {
debug('Destroy connection');
return self.connection.destroy()
.then(function () {
debug('Destroyed connection');
});
});
}
return database.ensureConnectionWorks(this.connection)
.then(function () {
return migrations.run(self.connection);
})
.then(function () {
return locking.lock(self.connection);
})
.then(function () {
return database.drop({
connection: self.connection,
dbConfig: self.dbConfig
});
})
.catch(function onRestError(err) {
if (err instanceof errors.MigrationsAreLockedError) {
throw err;
}
// CASE: ETIMEDOUT, ENOTFOUND
if (err instanceof errors.DatabaseError) {
throw err;
}
debug('Reset error: ' + err.message);
// CASE: Database does not exist. For MySql.
if (err.errno === 1049) {
return Promise.resolve();
}
return locking.unlock(self.connection)
.then(function () {
throw err;
});
})
.finally(function () {
debug('Destroy connection');
return self.connection.destroy()
.then(function () {
debug('Destroyed connection');
});
});
};
/**
* `knex-migrator health`
*
* @description This task detects the (migration) state of your database.
*
* It asks the database if....
*
* - the database was initialised?
* - migration files need to be executed?
*
* The task runs through the following steps:
*
* 1. Create a database connection.
* 2. Ensure the connection works (credentials are correct)
* 3. Run table upgrades/migrations if available.
* 4. Asks if the database is locked and aborts if so.
* 5. Perform an integrity check to figure out which migrations need to run (compare files against database)
* 6. Returns result.
* 7. Destroy connection.
*
* @returns {Promise<any>}
*/
KnexMigrator.prototype.isDatabaseOK = function isDatabaseOK() {
let self = this;
this.connection = database.connect(this.dbConfig);
return database.ensureConnectionWorks(this.connection)
.then(function () {
return migrations.run(self.connection);
})
.then(function () {
return locking.isLocked(self.connection);
})
.then(function () {
return self._integrityCheck();
})
.then(function (result) {
// CASE: if an init script was removed, the health check will be positive (see #48)
if (result.init && result.init.expected > result.init.actual) {
throw new errors.DatabaseIsNotOkError({
message: 'Please run `yarn knex-migrator init`',
code: 'DB_NOT_INITIALISED'
});
}
_.each(_.omit(result, 'init'), function (value) {
// CASE: there are more migrations expected than have been run, database needs to be migrated
if (value.expected > value.actual) {
throw new errors.DatabaseIsNotOkError({
message: 'Migrations are missing. Please run `yarn knex-migrator migrate`.',
code: 'DB_NEEDS_MIGRATION',
help: `Expected: ${value.expected} items in migrations table, found: ${value.actual}`
});
// CASE: there are more actual migrations than expected, something has gone wrong :(
} else if (value.expected < value.actual) {
throw new errors.DatabaseIsNotOkError({
message: 'Detected more items in the migrations table than expected. Please manually inspect the migrations table.',
code: 'MIGRATION_STATE_ERROR',
help: `Expected: ${value.expected} items in migrations table, found: ${value.actual}`
});
}
});
})
.catch(function (err) {
// CASE: database does not exist
if (err.errno === 1049) {
throw new errors.DatabaseIsNotOkError({
message: 'Please run `yarn knex-migrator init`',
code: 'DB_NOT_INITIALISED'
});
}
throw err;
})
.finally(function () {
if (!self.connection) {
return;
}
debug('Destroy connection');
return self.connection.destroy()
.then(function () {
debug('Destroyed connection');
});
});
};
/**
* `knex-migrator rollback`
*
* @description This task will rollback the database to a version.
*
* It will:
*
* 1. Create a connection to the database.
* 2. Ensure the connection works (credentials are correct)
* 3. Asks the database if the lock is active.
* 4. If the lock is not active, you cannot rollback. This is the current default behaviour.
* 5. If you pass the "force" flag, you can rollback if the lock is inactive.
* 6. Executes rollback helper to rollback to a version.
* 7. Destroy connection.
*
* @param {Object} options - Custom options the user can pass in (force, version, v, disableHooks)
* @returns {Promise<any>}
*/
KnexMigrator.prototype.rollback = function rollback(options) {
options = options || {};
let self = this;
let force = options.force;
let version = options.version || options.v;
let disableHooks = options.disableHooks;
let hooks = {};
this.connection = database.connect(this.dbConfig);
const helper = function helper() {
return new Promise(function (resolve, reject) {
try {
if (!disableHooks) {
// @TODO: load init or migrate hooks
hooks = require(path.join(self.migrationPath, '/hooks/init'));
}
} catch (err) {
debug('Hook Error: ' + err.message);
debug('No hooks found, no problem.');
}
if (hooks.before) {
return hooks.before({
connection: self.connection
}).then(resolve).catch(reject);
}
resolve();
}).then(function () {
let whereQuery = {};
// CASE 1: rollback to specific version (query all and filter out)
// CASE 2: rollback current version you are on
if (version) {
debug(`Rollback to specific version: ${version}`);
whereQuery = {};
} else {
whereQuery = {
currentVersion: self.currentVersion
};
}
return self.connection('migrations')
.where(whereQuery)
.then(function (values) {
if (!values.length) {
throw new errors.IncorrectUsageError({
message: 'No migrations available to rollback.'
});
}
// CASE: filter out all versions which are smaller than the version we want to rollback to
if (version) {
values = _.filter(values, function (value) {
return utils.isGreaterThanVersion({
greaterVersion: value.version,
smallerVersion: version
});
});
}
// @NOTE: we never ever rollback init scripts for now.
// this can be very dangerous, because it removes tables
// @EXCEPTION: you run init scripts and they fail
values = _.filter(values, function (value) {
return value.version !== 'init';
});
values.reverse();
return sequence(values.map(value => () => {
return self._rollback({
version: value.version,
onlyTasks: [value.name]
});
}));
});
}).then(function () {
if (hooks.shutdown) {
return hooks.shutdown({
executedFromShell: self.executedFromShell
});
}
});
};
return database.ensureConnectionWorks(this.connection)
.then(function () {
return migrations.run(self.connection);
})
.then(function () {
return locking.isLocked(self.connection);
})
.then(function () {
// CASE: db is not locked, force
if (force) {
return helper();
}
throw new errors.IncorrectUsageError({
message: 'Rollback did not happen.',
help: 'Use --force if you want to force a rollback. By default, rollbacks are only allowed if your database is locked.'
});
})
.catch(function (err) {
if (err instanceof errors.MigrationsAreLockedError) {
return helper()
.then(function () {
return locking.unlock(self.connection);
});
}
throw err;
})
.finally(function () {
if (!self.connection) {
return;
}
debug('Destroy connection');
return self.connection.destroy()
.then(function () {
debug('Destroyed connection');
});
});
};
// @TODO: All of these functions below are helper functions. Source them out as part of https://github.com/TryGhost/knex-migrator/issues/95.
/**
* @description Private helper function for rolling back. It is called in various places to rollback to a state.
*
* Cases:
*
* 1. Init or migrate task failed, rollback the previous tasks too.
* 2. Rollback task is executed.
*
* It will:
*
* 1. Read the migration tasks from disk.
* 2. Call "down" fn of target migration script.
* 3. Delete migration entry from database.
*
* @param {Object} options - Custom options the user can pass in (version, skippedTasks, onlyTasks, task)
* @returns {Promise<IterableOrNever<R>>}
* @private
*/
KnexMigrator.prototype._rollback = function _rollback(options) {
let version = options.version;
let skippedTasks = options.skippedTasks || [];
let onlyTasks = options.onlyTasks || [];
const failedTask = options.task;
let tasks = [];
let self = this;
if (version !== 'init') {
tasks = utils.readTasks(path.join(this.migrationPath, this.subfolder, version));
} else {
try {
tasks = utils.readTasks(path.join(this.migrationPath, version));
} catch (err) {
if (err.code === 'MIGRATION_PATH') {
tasks = [];
} else {
throw err;
}
}
}
// CASE: rollback failed in one of the tasks in init or migrate
// CASE: if no task available, you are about to rollback manually `knex-migrator rollback`
if (failedTask) {
const newTasks = [];
for (let i = 0; i < tasks.length; i = i + 1) {
const task = tasks[i];
if (task.name !== failedTask.name) {
newTasks.push(task);
} else if (task.name === failedTask.name) {
/**
* @NOTE
*
* The task, which has failed, is never written to the database.
* But we have to double check if the target task was running in a transaction.
*
* Transaction: no need to rollback this task
* No Transaction: we have to rollback this task, because of implicit commits
*/
if (!failedTask.config || !failedTask.config.transaction) {
newTasks.push(task);
}
break;
}
}
tasks = newTasks;
}
// CASE: one of the migrations that are about to be rolled back is marked as irreversible. Exit early without performing any actions
const irreversibleMigrations = _.filter(tasks, function (task) {
return !!_.get(task, 'config.irreversible');
});
if (irreversibleMigrations.length) {
return Promise.reject(new errors.IrreversibleMigrationError({
message: 'Unable to rollback',
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.',
code: 'IRREVERSIBLE_MIGRATION'
}));
}
tasks.reverse();
return sequence(tasks.map(task => () => {
if (skippedTasks[version] && skippedTasks[version].indexOf(task.name) !== -1) {
return Promise.resolve();
}
if (onlyTasks.length && onlyTasks.indexOf(task.name) === -1) {
return Promise.resolve();
}
if (!task.down) {
debug('No down function provided', task.name);
return self.connection('migrations')
.where({
name: task.name,
version: version,
currentVersion: self.currentVersion
})
.delete();
}
debug('Rollback', task.name);
if (task.config && task.config.transaction) {
return database.createTransaction(self.connection, function (txn) {
return task.down({
transacting: txn
});
}).then(function () {
return self.connection('migrations')
.where({
name: task.name,
version: version
})
.delete();
});
}
return task.down({
connection: self.connection
}).then(function () {
return self.connection('migrations')
.where({
name: task.name,
version: version
})
.delete();
});
}));
};
/**
* @description Private migrate helper.
*
* Cases:
* 1. Init task will use this helper to migrate to "init".
* 2. Migrate task will use this helper to migrate to a version e.g. "1.1"
*
* It will:
* 1. Read the migration tasks from disk.
* 2. Execute hooks.
* 3. Create a transaction for the target migration file if configured. Each migration scripts can run in one transaction.
* If multiple versions/scripts are executed, we cannot run all of them in a single txn, because implicit commands can happen in between.
* 4. Execute "up" function of migration file.
* 5. Returns any skipped task. Skipped tasks are tasks which failed. Only one task is returned, the last one which failed.
*
* @param {Object} options - Custom options the user can pass in (version, hooks, only, skip)
* @returns {Promise<{skippedTasks: Array}>}
* @private
*/
KnexMigrator.prototype._migrateTo = function _migrateTo(options) {
options = options || {};
let self = this,
version = options.version,
hooks = options.hooks || {},
only = options.only || null,
skip = options.skip || null,
subfolder = this.subfolder,
skippedTasks = [],
tasks = [];
if (version !== 'init') {
tasks = utils.readTasks(path.join(self.migrationPath, subfolder, version));
} else {
try {
tasks = utils.readTasks(path.join(self.migrationPath, version));
} catch (err) {
if (err.code === 'MIGRATION_PATH') {
tasks = [];
} else {
throw err;
}
}
}
if (only !== null) {
debug('only: ' + only);
tasks = [tasks[only - 1]];
} else if (skip !== null) {
debug('skip: ' + skip);
tasks.splice(skip - 1, 1);
}
debug('Migrate: ' + version + ' with ' + tasks.length + ' tasks.');
debug('Tasks: ' + JSON.stringify(tasks));
return sequence(tasks.map(task => () => {
return self._beforeEach({
task: task.name,
version: version
}).then(function () {
if (hooks.beforeEach) {
return hooks.beforeEach({
connection: self.connection
});
}
}).then(function () {
debug('Running up: ' + task.name);
if (task.config && task.config.transaction) {
return database.createTransaction(self.connection, function (txn) {
return task.up({
transacting: txn
});
});
}
return task.up({
connection: self.connection
});
}).then(function () {
if (hooks.afterEach) {
return hooks.afterEach({
connection: self.connection
});
}
}).then(function () {
return self._afterEach({
task: task,
version: version
});
}).catch(function (err) {
if (err instanceof errors.MigrationExistsError) {
debug('Skipping:' + task.name);
skippedTasks.push(task.name);
return Promise.resolve();
}
/**
* @NOTE: When your database encoding is set to utf8mb4 and you set a field length > 191 characters,
* MySQL will throw an error, BUT it won't roll back the changes, because ALTER/CREATE table commands are
* implicit commands.
*
* https://bugs.mysql.com/bug.php?id=28727
* https://github.com/TryGhost/knex-migrator/issues/51
*/
if (err.code === 'ER_TOO_LONG_KEY') {
let match = err.message.match(/`\w+`/g);
let table = match[0];
let field = match[2];
throw new errors.MigrationScriptError({
message: 'Field length of %field% in %table% is too long!'.replace('%field%', field).replace('%table%', table),
context: 'This usually happens if your database encoding is utf8mb4.\n' +
'All unique fields and indexes must be lower than 191 characters.\n' +
'Please correct your field length and reset your database with `yarn knex-migrator reset`.\n',
help: 'Read more here: https://github.com/TryGhost/knex-migrator/issues/51\n',
err: err
});
}
throw new errors.MigrationScriptError({
message: err.message,
help: 'Error occurred while executing the following migration: ' + task.name,
context: task,
err: err
});
});
})).then(function () {
return {
skippedTasks: skippedTasks
};
});
};
/**
* @description Private helper to execute logic before each migration script is executed.
*
* It ensures that migration scripts are not executed twice.
*
* @param {Object} options
* @returns {*}
* @private
*/
KnexMigrator.prototype._beforeEach = function _beforeEach(options) {
options = options || {};
let task = options.task,
version = options.version;
return this.connection('migrations')
.then(function (migrations) {
if (!migrations.length) {
return;
}
if (_.find(migrations, {name: task, version: version})) {
throw new errors.MigrationExistsError();
}
});
};
/**
* @description Private helper to execute logic after each migration script is executed.
*
* It ensures that migration files are inserted into the database.
*
* @param {Object} options
* @returns {*}
* @private
*/
KnexMigrator.prototype._afterEach = function _afterEach(options) {
options = options || {};
let self = this;
let task = options.task;
let version = options.version;
return this.connection('migrations')
.insert({
name: task.name,
version: version,
currentVersion: self.currentVersion
});
};
/**
* @description Private helper to execute an integrity check. The integrity check compares files against entries in the
* database. It returns expected and actual database state.
*
* @param {Object} options - Custom user options (force)
* @returns {Promise<any>}
* @private
*/
KnexMigrator.prototype._integrityCheck = async function _integrityCheck(options) {
options = options || {};
let self = this,
subfolder = this.subfolder,
force = options.force,
folders = [],
toReturn = {},
futureVersions = [];
// CASE: we always fetch the init scripts and check them
// 1. to be able to add more init scripts
// 2. to check if migration scripts need's to be executed or not, see https://github.com/TryGhost/knex-migrator/issues/39
folders.push('init');
// CASE: no subfolder yet. You can tell knex-migrator if scripts live on a sub folder.
try {
folders = folders.concat(utils.readVersionFolders(path.join(self.migrationPath, subfolder)));
} catch (err) {
// ignore
}
try {
const dbMigrations = await this
.connection('migrations')
.select('version')
.count('version', {as: 'c'})
.groupBy('version');
_.each(folders, function (folder) {
// CASE: versions/1.1-members or versions/2.0-payments
if (folder !== 'init') {
try {
folder = folder.match(/([\d._]+)/)[0];
} catch (err) {
logging.warn('Cannot parse folder name.');
logging.warn('Ignore Folder: ' + folder);
return;
}
}
// CASE:
// if your current version is 1.0 and you add migration scripts for the next version 1.1
// we won't execute them until your current version changes to 1.1 or until you force KM to migrate to it
if (self.currentVersion && !force) {
if (utils.isGreaterThanVersion({smallerVersion: self.currentVersion, greaterVersion: folder})) {
futureVersions.push(folder);
}
}
let actual = 0;
let expected;
const migrationCount = dbMigrations.find(m => m.version === folder);
if (migrationCount) {
actual = migrationCount.c;
}
if (folder !== 'init') {
expected = utils.listFiles(path.join(self.migrationPath, subfolder, folder)).length;
} else {
expected = utils.listFiles(path.join(self.migrationPath, folder)).length;
}
debug('Version ' + folder + ' expected: ' + expected);
debug('Version ' + folder + ' actual: ' + actual);
toReturn[folder] = {
expected: expected,
actual: actual
};
});
// CASE: ensure that either you have to run `migrate --force` or they ran already
if (futureVersions.length) {
_.each(futureVersions, function (futureVersion) {
if (toReturn[futureVersion].actual !== toReturn[futureVersion].expected) {
logging.warn('knex-migrator is skipping ' + futureVersion);
logging.warn('Current version in MigratorConfig.js is smaller then requested version, use --force to proceed!');
logging.warn('Please run `yarn knex-migrator migrate --v ' + futureVersion + ' --force` to proceed!');
delete toReturn[futureVersion];
}
});
}
return toReturn;
} catch (err) {
// CASE: no database selected (database.connection.database="")
if (err.errno === 1046) {
throw new errors.DatabaseIsNotOkError({
message: 'Please define a target database in your configuration.',
help: 'database: {\n\tconnection:\n\t\tdatabase:"database_name"\n\t}\n}\n',
code: 'DB_NOT_INITIALISED'
});
}
// CASE: database does not exist
if (err.errno === 1049) {
throw new errors.DatabaseIsNotOkError({
message: 'Please run `yarn knex-migrator init`',
code: 'DB_NOT_INITIALISED'
});
}
// CASE: migration table does not exist
if (err.errno === 1 || err.errno === 1146) {
throw new errors.DatabaseIsNotOkError({
message: 'Please run `yarn knex-migrator init`',
code: 'MIGRATION_TABLE_IS_MISSING'
});
}
throw err;
}
};
module.exports = KnexMigrator;