// # Update Database // Handles migrating a database between two different database versions var Promise = require('bluebird'), _ = require('lodash'), backup = require('./backup'), fixtures = require('./fixtures'), errors = require('../../errors'), logging = require('../../logging'), i18n = require('../../i18n'), db = require('../../data/db'), versioning = require('../schema').versioning, sequence = function sequence(tasks, modelOptions, logger) { // utils/sequence.js does not offer an option to pass cloned arguments return Promise.reduce(tasks, function (results, task) { return task(_.cloneDeep(modelOptions), logger) .then(function (result) { results.push(result); return results; }); }, []); }, updateDatabaseSchema, migrateToDatabaseVersion, execute, logger, isDatabaseOutOfDate; logger = { info: function info(message) { logging.info('Migrations:' + message); }, warn: function warn(message) { logging.warn('Skipping Migrations:' + message); } }; /** * update database schema for one single version */ updateDatabaseSchema = function (tasks, logger, modelOptions) { if (!tasks.length) { return Promise.resolve(); } return sequence(tasks, modelOptions, logger); }; /** * update each database version as one transaction * if a version fails, rollback * if a version fails, stop updating more versions */ migrateToDatabaseVersion = function migrateToDatabaseVersion(version, logger, modelOptions) { return new Promise(function (resolve, reject) { db.knex.transaction(function (transaction) { var migrationTasks = versioning.getUpdateDatabaseTasks(version, logger), fixturesTasks = versioning.getUpdateFixturesTasks(version, logger); logger.info('Updating database to ' + version); modelOptions.transacting = transaction; updateDatabaseSchema(migrationTasks, logger, modelOptions) .then(function () { return fixtures.update(fixturesTasks, logger, modelOptions); }) .then(function () { return versioning.setDatabaseVersion(transaction, version); }) .then(function () { transaction.commit(); resolve(); }) .catch(function (err) { logger.warn('rolling back because of: ' + err.stack); transaction.rollback(); }); }).catch(function () { reject(); }); }); }; /** * ## Update * Does a backup, then updates the database and fixtures */ execute = function execute(options) { options = options || {}; var fromVersion = options.fromVersion, toVersion = options.toVersion, forceMigration = options.forceMigration, versionsToUpdate, modelOptions = { context: { internal: true } }; fromVersion = forceMigration ? versioning.canMigrateFromVersion : fromVersion; // Figure out which versions we're updating through. // This shouldn't include the from/current version (which we're already on) versionsToUpdate = versioning.getMigrationVersions(fromVersion, toVersion).slice(1); return backup(logger) .then(function () { logger.info('Migration required from ' + fromVersion + ' to ' + toVersion); return Promise.mapSeries(versionsToUpdate, function (versionToUpdate) { return migrateToDatabaseVersion(versionToUpdate, logger, modelOptions); }); }) .then(function () { logger.info('Finished!'); }); }; isDatabaseOutOfDate = function isDatabaseOutOfDate(options) { options = options || {}; var fromVersion = options.fromVersion, toVersion = options.toVersion, forceMigration = options.forceMigration; // CASE: current database version is lower then we support if (fromVersion < versioning.canMigrateFromVersion) { return { error: new errors.DatabaseVersionError({ message: i18n.t('errors.data.versioning.index.cannotMigrate.error'), context: i18n.t('errors.data.versioning.index.cannotMigrate.context'), help: i18n.t('common.seeLinkForInstructions', {link: 'http://support.ghost.org/how-to-upgrade/'}) }) }; } // CASE: the database exists but is out of date else if (fromVersion < toVersion || forceMigration) { return {migrate: true}; } // CASE: database is up-to-date else if (fromVersion === toVersion) { return {migrate: false}; } // CASE: we don't understand the version else { return {error: new errors.DatabaseVersionError({message: i18n.t('errors.data.versioning.index.dbVersionNotRecognized')})}; } }; exports.execute = execute; exports.isDatabaseOutOfDate = isDatabaseOutOfDate;