mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-22 10:21:36 +03:00
611f696149
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
461 lines
14 KiB
JavaScript
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
|
|
*/
|