mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-21 01:41:46 +03:00
3b90b1f335
refs https://github.com/TryGhost/Team/issues/807 The launch wizard completed flag was previously stored at per user level in accessibility column of user table, so an administrator still got the option to complete the launch wizard even if the owner had completed it previously, which is not expected pattern. This change moves the launch complete flag for Admin to common settings from per user level so a site only needs to complete the launch wizard once irrespective of which user completes it - adds new `editor_is_launch_complete` setting to track if a site launch steps are completed in Admin - adds new migration util to easily allow adding new setting - adds migration to introduce new `editor_is_launch_complete` setting - adds migration to update launch complete flag for a site if any of the users have already completed the launch steps
500 lines
15 KiB
JavaScript
500 lines
15 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) {
|
|
logging.warn(`Removing permission(${config.permission}) from role(${config.role}) - Permission not found.`);
|
|
return;
|
|
}
|
|
|
|
const role = await connection('roles').where({
|
|
name: config.role
|
|
}).first();
|
|
|
|
if (!role) {
|
|
logging.warn(`Removing permission(${config.permission}) from role(${config.role}) - Role not found.`);
|
|
return;
|
|
}
|
|
|
|
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
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Creates a migration which will insert a new setting in settings table
|
|
* @param {object} settingSpec - setting key, value, group and type
|
|
*
|
|
* @returns {Object} migration object returning config/up/down properties
|
|
*/
|
|
function addSetting({key, value, type, group}) {
|
|
return createTransactionalMigration(
|
|
async function up(connection) {
|
|
const settingExists = await connection('settings')
|
|
.where('key', '=', key)
|
|
.first();
|
|
if (settingExists) {
|
|
logging.warn(`Skipping adding setting: ${key} - setting already exists`);
|
|
return;
|
|
}
|
|
|
|
logging.info(`Adding setting: ${key}`);
|
|
const now = connection.raw('CURRENT_TIMESTAMP');
|
|
|
|
return connection('settings')
|
|
.insert({
|
|
id: ObjectId().toHexString(),
|
|
key,
|
|
value,
|
|
group,
|
|
type,
|
|
created_at: now,
|
|
created_by: MIGRATION_USER,
|
|
updated_at: now,
|
|
updated_by: MIGRATION_USER
|
|
});
|
|
},
|
|
async function down(connection) {
|
|
const settingExists = await connection('settings')
|
|
.where('key', '=', key)
|
|
.first();
|
|
if (!settingExists) {
|
|
logging.warn(`Skipping dropping setting: ${key} - setting does not exist`);
|
|
return;
|
|
}
|
|
|
|
logging.info(`Dropping setting: ${key}`);
|
|
return connection('settings')
|
|
.where('key', '=', key)
|
|
.del();
|
|
}
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
addTable,
|
|
dropTables,
|
|
addPermission,
|
|
addPermissionToRole,
|
|
addPermissionWithRoles,
|
|
addSetting,
|
|
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
|
|
*/
|