ghost
Version:
The professional publishing platform
213 lines (183 loc) • 7.04 kB
JavaScript
const _ = require('lodash');
const errors = require('@tryghost/errors');
const logging = require('@tryghost/logging');
/**
* This plugin is used to add actions to the database. It backs the "audit log" feature we have in Ghost.
*
* The functions here are triggered by the `onCreated`, `onUpdated`, `onDeleted` functions in the `events`
* plugin, with some extra ones for niche other events.
*
* @param {import('bookshelf')} Bookshelf
*/
module.exports = function (Bookshelf) {
/**
* Insert an action into the database
*
* @param {Object} data - The data to insert
* @param {Object} options - The options object
*/
const insertAction = (data, options) => {
// CASE: model does not support action for target event
if (!data) {
return;
}
const insert = (action) => {
Bookshelf.model('Action')
.add(action, {autoRefresh: false})
.catch((err) => {
if (_.isArray(err)) {
err = err[0];
}
logging.error(new errors.InternalServerError({
err
}));
});
};
if (options.transacting) {
options.transacting.once('committed', (committed) => {
if (!committed) {
return;
}
insert(data);
});
} else {
insert(data);
}
};
/**
* Add an action to the database
*
* @param {import('bookshelf').Model} model - The model to add the action to
* @param {string} event - The event that triggered the action
* @param {Object} options - The options object
*/
const addAction = (model, event, options) => {
if (!model.wasChanged()) {
return;
}
// CASE: model does not support actions at all
if (!model.getAction) {
return;
}
const data = model.getAction(event, options);
insertAction(data, options);
};
Bookshelf.Model = Bookshelf.Model.extend({
/**
* Constructs data to be stored in the database with info
* on particular actions
*
* @param {string} event - The event that triggered the action
* @param {Object} options - The options object
* @returns {Object} The data to be stored in the database
*/
getAction(event, options) {
// Ignore internal updates (`options.context.internal`) for now
const actor = this.getActor(options);
if (!actor) {
return;
}
if (!this.actionsCollectCRUD) {
return;
}
const resourceType = this.actionsResourceType;
if (!resourceType) {
return;
}
let context = {
action_name: options.actionName
};
// Used to attach extra content to the action (ie. the key + group for settings changes)
if (this.actionsExtraContext && Array.isArray(this.actionsExtraContext)) {
for (const c of this.actionsExtraContext) {
context[c] = this.get(c) || this.previous(c);
}
}
const previousPrimaryName = this.previous('title') || this.previous('name') || this.previous('email');
const currentPrimaryName = this.get('title') || this.get('name') || this.get('email');
// Attach the primary name to the action (ie. the title or name of the model)
if (event === 'deleted') {
context.primary_name = previousPrimaryName;
} else if (['added', 'edited'].includes(event)) {
context.primary_name = currentPrimaryName || previousPrimaryName;
}
const data = {
event,
resource_id: this.id || this.previous('id'),
resource_type: resourceType,
actor_id: actor.id,
actor_type: actor.type
};
if (context && Object.keys(context).length) {
data.context = context;
}
return data;
},
addAction
}, {
addAction,
/**
* Add actions for bulk actions
*
* @param {string} event - The event that triggered the action
* @param {number[]} ids - The ids of the models that were affected
* @param {Object} options - The options object
*/
async addActions(event, ids, options) {
if (ids.length === 1) {
// We want to store an event for a single model in the actions table
// This is so we can include the name
const model = await this.findOne({[options.column ?? 'id']: ids[0]}, {require: true, transacting: options.transacting, context: {internal: true}});
this.addAction(model, event, options);
return;
}
const existingAction = this.getBulkAction(event, ids.length, options);
insertAction(existingAction, options);
},
/**
* Constructs data for bulk actions to be stored in the database
*
* @param {string} event - The event that triggered the action
* @param {number} count - The number of models that were affected
* @param {Object} options - The options object
* @returns {Object} The data to be stored in the database
*/
getBulkAction(event, count, options) {
// Ignore internal updates (`options.context.internal`) for now
const actor = this.prototype.getActor(options);
if (!actor) {
return;
}
// Models can opt-in to their CRUD actions being collected (we do this so we don't
// log every single action)
if (!this.prototype.actionsCollectCRUD) {
return;
}
const resourceType = this.prototype.actionsResourceType;
if (!resourceType) {
return;
}
let context = {
count,
action_name: options.actionName
};
if (this.getBulkActionExtraContext && typeof this.getBulkActionExtraContext === 'function') {
context = {
...context,
...this.getBulkActionExtraContext.bind(this)(options)
};
}
const data = {
event,
resource_id: null,
resource_type: resourceType,
actor_id: actor.id,
actor_type: actor.type
};
if (context && Object.keys(context).length) {
data.context = context;
}
return data;
}
});
};