UNPKG

ghost

Version:

The professional publishing platform

213 lines (183 loc) 7.04 kB
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; } }); };