TryGhost/Ghost

View on GitHub
ghost/core/core/server/data/migrations/utils/schema.js

Summary

Maintainability
F
3 days
Test Coverage
const logging = require('@tryghost/logging');
const {commands} = require('../../schema');
const DatabaseInfo = require('@tryghost/database-info');

const {createNonTransactionalMigration, createTransactionalMigration} = require('./migrations');

/**
 * @param {string} table
 * @param {string} column
 * @param {Object} columnDefinition
 *
 * @returns {Migration}
 */
function createAddColumnMigration(table, column, columnDefinition) {
    return createNonTransactionalMigration(
        // up
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === true,
            operation: commands.addColumn,
            operationVerb: 'Adding',
            columnDefinition
        }),
        // down
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === false,
            operation: commands.dropColumn,
            operationVerb: 'Removing',
            columnDefinition
        })
    );
}

/**
 * @param {string} table
 * @param {string} column
 * @param {Object} columnDefinition
 *
 * @returns {Migration}
 */
function createDropColumnMigration(table, column, columnDefinition) {
    return createNonTransactionalMigration(
        // up
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === false,
            operation: commands.dropColumn,
            operationVerb: 'Removing'
        }),
        // down
        commands.createColumnMigration({
            table,
            column,
            dbIsInCorrectState: hasColumn => hasColumn === true,
            operation: commands.addColumn,
            operationVerb: 'Adding',
            columnDefinition
        })
    );
}

/**
 * @param {string} table
 * @param {string} column
 * @param {Object} options
 * @param {boolean} options.disableForeignKeyChecks Disable foreign key checks for the down operation (when dropping nullable)
 * @returns {Migration}
 */
function createSetNullableMigration(table, column, options = {}) {
    return createTransactionalMigration(
        async function up(knex) {
            logging.info(`Setting nullable: ${table}.${column}`);
            await commands.setNullable(table, column, knex);
        },
        async function down(knex) {
            if (DatabaseInfo.isSQLite(knex)) {
                options.disableForeignKeyChecks = false;
            }
            logging.info(`Dropping nullable:  ${table}.${column}${options.disableForeignKeyChecks ? ' with foreign keys disabled' : ''}`);
            if (options.disableForeignKeyChecks) {
                await knex.raw('SET FOREIGN_KEY_CHECKS=0;').transacting(knex);
            }

            try {
                await commands.dropNullable(table, column, knex);
            } finally {
                if (options.disableForeignKeyChecks) {
                    await knex.raw('SET FOREIGN_KEY_CHECKS=1;').transacting(knex);
                }
            }
        }
    );
}

/**
 * @param {string} table
 * @param {string[]|string} columns One or multiple columns (in case the index should be for multiple columns)
 * @returns {Migration}
 */
function createAddIndexMigration(table, columns) {
    return createTransactionalMigration(
        async function up(knex) {
            await commands.addIndex(table, columns, knex);
        },
        async function down(knex) {
            await commands.dropIndex(table, columns, knex);
        }
    );
}

/**
 * @param {string} table
 * @param {string} from
 * @param {string} to
 *
 * @returns {Migration}
 */
function createRenameColumnMigration(table, from, to) {
    return createNonTransactionalMigration(
        async function up(knex) {
            const hasColumn = await knex.schema.hasColumn(table, to);
            if (hasColumn) {
                logging.warn(`Renaming ${table}.${from} to ${table}.${to} column - skipping as column ${table}.${to} already exists`);
            } else {
                await commands.renameColumn(table, from, to, knex);
            }
        },
        async function down(knex) {
            const hasColumn = await knex.schema.hasColumn(table, from);
            if (hasColumn) {
                logging.warn(`Renaming ${table}.${to} to ${table}.${from} column - skipping as column ${table}.${from} already exists`);
            } else {
                await commands.renameColumn(table, to, from, knex);
            }
        }
    );
}

/**
 * @param {string} table
 * @param {string} column
 * @param {Object} options
 * @param {boolean} options.disableForeignKeyChecks Disable foreign key checks for the up operation (when dropping nullable)
 * @returns {Migration}
 */
function createDropNullableMigration(table, column, options = {}) {
    return createTransactionalMigration(
        async function up(knex) {
            if (DatabaseInfo.isSQLite(knex)) {
                options.disableForeignKeyChecks = false;
            }
            logging.info(`Dropping nullable: ${table}.${column}${options.disableForeignKeyChecks ? ' with foreign keys disabled' : ''}`);

            if (options.disableForeignKeyChecks) {
                await knex.raw('SET FOREIGN_KEY_CHECKS=0;').transacting(knex);
            }

            try {
                await commands.dropNullable(table, column, knex);
            } finally {
                if (options.disableForeignKeyChecks) {
                    await knex.raw('SET FOREIGN_KEY_CHECKS=1;').transacting(knex);
                }
            }
        },
        async function down(knex) {
            logging.info(`Setting nullable: ${table}.${column}`);
            await commands.setNullable(table, column, knex);
        }
    );
}

module.exports = {
    createAddColumnMigration,
    createDropColumnMigration,
    createSetNullableMigration,
    createDropNullableMigration,
    createRenameColumnMigration,
    createAddIndexMigration
};

/**
 * @typedef {Object} TransactionalMigrationFunctionOptions
 *
 * @prop {import('knex').Knex} transacting
 */

/**
 * @typedef {(options: TransactionalMigrationFunctionOptions) => Promise<void>} TransactionalMigrationFunction
 */

/**
 * @typedef {Object} Migration
 *
 * @prop {Object} config
 * @prop {boolean} config.transaction
 *
 * @prop {TransactionalMigrationFunction} up
 * @prop {TransactionalMigrationFunction} down
 */