Ghost/core/server/data/migrations/utils.js
Fabien O'Carroll 611f696149 Removed bluebird import from migration utils
no-issue

The bluebird library is unecessary in this module, as all uses of it
were wrapped in `async` functions which will return a native Promise
rather than a bluebird one.

refs https://ghost.slack.com/archives/C02G9E68C/p1629822160273500
2021-08-25 23:30:14 +02:00

461 lines
14 KiB
JavaScript

const ObjectId = require('bson-objectid').default;
const logging = require('@tryghost/logging');
const errors = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const commands = require('../schema').commands;
const MIGRATION_USER = 1;
const messages = {
permissionRoleActionError: 'Cannot {action} permission({permission}) with role({role}) - {resource} does not exist'
};
/**
* Creates a migrations which will add a new table from schema.js to the database
* @param {string} name - table name
* @param {Object} tableSpec - copy of table schema definition as defined in schema.js at the moment of writing the migration,
* this parameter MUST be present, otherwise @daniellockyer will hunt you down
*
* @returns {Object} migration object returning config/up/down properties
*/
function addTable(name, tableSpec) {
return createNonTransactionalMigration(
async function up(connection) {
const tableExists = await connection.schema.hasTable(name);
if (tableExists) {
logging.warn(`Skipping adding table: ${name} - table already exists`);
return;
}
logging.info(`Adding table: ${name}`);
return commands.createTable(name, connection, tableSpec);
},
async function down(connection) {
const tableExists = await connection.schema.hasTable(name);
if (!tableExists) {
logging.warn(`Skipping dropping table: ${name} - table does not exist`);
return;
}
logging.info(`Dropping table: ${name}`);
return commands.deleteTable(name, connection);
}
);
}
/**
* Creates migration which will drop a table
*
* @param {[string]} names - names of the tables to drop
*/
function dropTables(names) {
return createIrreversibleMigration(
async function up(connection) {
for (const name of names) {
const exists = await connection.schema.hasTable(name);
if (!exists) {
logging.warn(`Failed to drop table: ${name} - table does not exist`);
} else {
logging.info(`Dropping table: ${name}`);
await commands.deleteTable(name, connection);
}
}
}
);
}
/**
* Creates a migration which will add a permission to the database
*
* @param {Object} config
* @param {string} config.name - The name of the permission
* @param {string} config.action - The action_type of the permission
* @param {string} config.object - The object_type of the permission
*
* @returns {Migration}
*/
function addPermission(config) {
return createTransactionalMigration(
async function up(connection) {
const existingPermission = await connection('permissions').where({
name: config.name,
action_type: config.action,
object_type: config.object
}).first();
if (existingPermission) {
logging.warn(`Permission for ${config.action}:${config.object} already added`);
return;
}
logging.info(`Adding permission for ${config.action}:${config.object}`);
const date = connection.raw('CURRENT_TIMESTAMP');
await connection('permissions').insert({
id: ObjectId().toHexString(),
name: config.name,
action_type: config.action,
object_type: config.object,
created_at: date,
created_by: MIGRATION_USER,
updated_at: date,
updated_by: MIGRATION_USER
});
},
async function down(connection) {
const existingPermission = await connection('permissions').where({
name: config.name,
action_type: config.action,
object_type: config.object
}).first();
if (!existingPermission) {
logging.warn(`Permission for ${config.action}:${config.object} already removed`);
return;
}
logging.info(`Removing permission for ${config.action}:${config.object}`);
await connection('permissions').where({
action_type: config.action,
object_type: config.object
}).del();
}
);
}
/**
* Creates a migration which will link a permission to a role in the database
*
* @param {Object} config
* @param {string} config.permission - The name of the permission
* @param {string} config.role - The name of the role
*
* @returns {Migration}
*/
function addPermissionToRole(config) {
return createTransactionalMigration(
async function up(connection) {
const permission = await connection('permissions').where({
name: config.permission
}).first();
if (!permission) {
throw new errors.GhostError({
message: tpl(messages.permissionRoleActionError, {
action: 'add',
permission: config.permission,
role: config.role,
resource: 'permission'
})
});
}
const role = await connection('roles').where({
name: config.role
}).first();
if (!role) {
throw new errors.GhostError({
message: tpl(messages.permissionRoleActionError, {
action: 'add',
permission: config.permission,
role: config.role,
resource: 'role'
})
});
}
const existingRelation = await connection('permissions_roles').where({
permission_id: permission.id,
role_id: role.id
}).first();
if (existingRelation) {
logging.warn(`Adding permission(${config.permission}) to role(${config.role}) - already exists`);
return;
}
logging.warn(`Adding permission(${config.permission}) to role(${config.role})`);
await connection('permissions_roles').insert({
id: ObjectId().toHexString(),
permission_id: permission.id,
role_id: role.id
});
},
async function down(connection) {
const permission = await connection('permissions').where({
name: config.permission
}).first();
if (!permission) {
throw new errors.GhostError({
message: tpl(messages.permissionRoleActionError, {
action: 'remove',
permission: config.permission,
role: config.role,
resource: 'permission'
})
});
}
const role = await connection('roles').where({
name: config.role
}).first();
if (!role) {
throw new errors.GhostError({
message: tpl(messages.permissionRoleActionError, {
action: 'remove',
permission: config.permission,
role: config.role,
resource: 'role'
})
});
}
const existingRelation = await connection('permissions_roles').where({
permission_id: permission.id,
role_id: role.id
}).first();
if (!existingRelation) {
logging.warn(`Removing permission(${config.permission}) from role(${config.role}) - already removed`);
return;
}
logging.info(`Removing permission(${config.permission}) from role(${config.role})`);
await connection('permissions_roles').where({
permission_id: permission.id,
role_id: role.id
}).del();
}
);
}
/**
* Creates a migration which will add a permission to the database, and then link it to roles
*
* @param {Object} config
* @param {string} config.name - The name of the permission
* @param {string} config.action - The action_type of the permission
* @param {string} config.object - The object_type of the permission
*
* @param {string[]} roles - A list of role names
*
* @returns {Migration}
*/
function addPermissionWithRoles(config, roles) {
return combineTransactionalMigrations(
addPermission(config),
...roles.map((role => addPermissionToRole({permission: config.name, role})))
);
}
/**
* @param {(connection: import('knex')) => Promise<void>} up
* @param {(connection: import('knex')) => Promise<void>} down
*
* @returns {Migration}
*/
function createNonTransactionalMigration(up, down) {
return {
config: {
transaction: false
},
async up(config) {
await up(config.connection);
},
async down(config) {
await down(config.connection);
}
};
}
/**
* @param {(connection: import('knex')) => Promise<void>} up
*
* @returns {Migration}
*/
function createIrreversibleMigration(up) {
return {
config: {
irreversible: true
},
async up(config) {
await up(config.connection);
},
async down() {
return Promise.reject();
}
};
}
/**
* @param {(connection: import('knex')) => Promise<void>} up
* @param {(connection: import('knex')) => Promise<void>} down
*
* @returns {Migration}
*/
function createTransactionalMigration(up, down) {
return {
config: {
transaction: true
},
async up(config) {
await up(config.transacting);
},
async down(config) {
await down(config.transacting);
}
};
}
/**
* @param {Migration[]} migrations
*
* @returns {Migration}
*/
function combineTransactionalMigrations(...migrations) {
return {
config: {
transaction: true
},
async up(config) {
for (const migration of migrations) {
await migration.up(config);
}
},
async down(config) {
// Down migrations must be run backwards!!
const reverseMigrations = migrations.slice().reverse();
for (const migration of reverseMigrations) {
await migration.down(config);
}
}
};
}
/**
* @param {Migration[]} migrations
*
* @returns {Migration}
*/
function combineNonTransactionalMigrations(...migrations) {
return {
config: {
transaction: false
},
async up(config) {
for (const migration of migrations) {
await migration.up(config);
}
},
async down(config) {
// Down migrations must be run backwards!!
const reverseMigrations = migrations.slice().reverse();
for (const migration of reverseMigrations) {
await migration.down(config);
}
}
};
}
/**
* @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'
})
);
}
/**
* @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
})
);
}
module.exports = {
addTable,
dropTables,
addPermission,
addPermissionToRole,
addPermissionWithRoles,
createTransactionalMigration,
createNonTransactionalMigration,
createIrreversibleMigration,
combineTransactionalMigrations,
combineNonTransactionalMigrations,
createAddColumnMigration,
createDropColumnMigration,
meta: {
MIGRATION_USER
}
};
/**
* @typedef {Object} TransactionalMigrationFunctionOptions
*
* @prop {import('knex')} transacting
*/
/**
* @typedef {(options: TransactionalMigrationFunctionOptions) => Promise<void>} TransactionalMigrationFunction
*/
/**
* @typedef {Object} Migration
*
* @prop {Object} config
* @prop {boolean} config.transaction
*
* @prop {TransactionalMigrationFunction} up
* @prop {TransactionalMigrationFunction} down
*/