mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-01 23:37:43 +03:00
Extracted Bookshelf events code to plugin
no issue - this commit extracts event related code from the Base model into a plugin - in particular: - events initialization - the `on*` events - `emitChange` - I'm not sure about this one but it __is__ event related
This commit is contained in:
parent
93c00b1ab7
commit
6ce1b11a15
@ -46,6 +46,8 @@ ghostBookshelf.plugin(require('./crud'));
|
||||
|
||||
ghostBookshelf.plugin(require('./actions'));
|
||||
|
||||
ghostBookshelf.plugin(require('./events'));
|
||||
|
||||
// Manages nested updates (relationships)
|
||||
ghostBookshelf.plugin('bookshelf-relations', {
|
||||
allowedOptions: ['context', 'importing', 'migrating'],
|
||||
|
261
core/server/models/base/events.js
Normal file
261
core/server/models/base/events.js
Normal file
@ -0,0 +1,261 @@
|
||||
const _ = require('lodash');
|
||||
const debug = require('@tryghost/debug')('models:base:model-events');
|
||||
|
||||
const events = require('../../lib/common/events');
|
||||
const schema = require('../../data/schema');
|
||||
|
||||
module.exports = function (Bookshelf) {
|
||||
Bookshelf.Model = Bookshelf.Model.extend({
|
||||
initializeEvents: function () {
|
||||
// NOTE: triggered before `creating`/`updating`
|
||||
this.on('saving', function onSaving(newObj, attrs, options) {
|
||||
if (options.method === 'insert') {
|
||||
// id = 0 is still a valid value for external usage
|
||||
if (_.isUndefined(newObj.id) || _.isNull(newObj.id)) {
|
||||
newObj.setId();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.on('fetched', this.onFetched);
|
||||
this.on('fetching', this.onFetching);
|
||||
this.on('fetched:collection', this.onFetchedCollection);
|
||||
this.on('fetching:collection', this.onFetchingCollection);
|
||||
this.on('creating', this.onCreating);
|
||||
this.on('created', this.onCreated);
|
||||
this.on('updating', this.onUpdating);
|
||||
this.on('updated', this.onUpdated);
|
||||
this.on('destroying', this.onDestroying);
|
||||
this.on('destroyed', this.onDestroyed);
|
||||
this.on('saving', this.onSaving);
|
||||
this.on('saved', this.onSaved);
|
||||
},
|
||||
|
||||
/**
|
||||
* @NOTE
|
||||
* We have to remember the `_previousAttributes` attributes, because when destroying resources
|
||||
* We listen on the `onDestroyed` event and Bookshelf resets these properties right after the event.
|
||||
* If the query runs in a txn, `_previousAttributes` will be empty.
|
||||
*/
|
||||
emitChange: function (model, event, options) {
|
||||
const _emit = (ghostEvent, _model, opts) => {
|
||||
if (!_model.wasChanged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
debug(_model.tableName, ghostEvent);
|
||||
|
||||
// @NOTE: Internal Ghost events. These are very granular e.g. post.published
|
||||
events.emit(ghostEvent, _model, opts);
|
||||
};
|
||||
|
||||
if (!options.transacting) {
|
||||
return _emit(event, model, options);
|
||||
}
|
||||
|
||||
if (!model.ghostEvents) {
|
||||
model.ghostEvents = [];
|
||||
|
||||
// CASE: when importing, deleting or migrating content, lot's of model queries are happening in one transaction
|
||||
// lot's of model events will be triggered. we ensure we set the max listeners to infinity.
|
||||
// we are using `once` - we auto remove the listener afterwards
|
||||
if (options.importing || options.destroyAll || options.migrating) {
|
||||
options.transacting.setMaxListeners(0);
|
||||
}
|
||||
|
||||
options.transacting.once('committed', (committed) => {
|
||||
if (!committed) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(this.ghostEvents, (obj) => {
|
||||
_emit(obj.event, model, obj.options);
|
||||
});
|
||||
|
||||
delete model.ghostEvents;
|
||||
});
|
||||
}
|
||||
|
||||
model.ghostEvents.push({
|
||||
event: event,
|
||||
options: {
|
||||
importing: options.importing,
|
||||
context: options.context
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Do not call `toJSON`. This can remove properties e.g. password.
|
||||
* @returns {*}
|
||||
*/
|
||||
onValidate: function onValidate(model, columns, options) {
|
||||
this.setEmptyValuesToNull();
|
||||
return schema.validate(this.tableName, this, options);
|
||||
},
|
||||
|
||||
onFetched() {},
|
||||
|
||||
/**
|
||||
* http://knexjs.org/#Builder-forUpdate
|
||||
* https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
|
||||
*
|
||||
* Lock target collection/model for further update operations.
|
||||
* This avoids collisions and possible content override cases.
|
||||
*/
|
||||
onFetching: function onFetching(model, columns, options) {
|
||||
if (options.forUpdate && options.transacting) {
|
||||
options.query.forUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
onFetchedCollection() {},
|
||||
|
||||
onFetchingCollection: function onFetchingCollection(model, columns, options) {
|
||||
if (options.forUpdate && options.transacting) {
|
||||
options.query.forUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
onCreated(model, attrs, options) {
|
||||
this.addAction(model, 'added', options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adding resources implies setting these properties on the server side
|
||||
* - set `created_by` based on the context
|
||||
* - set `updated_by` based on the context
|
||||
* - the bookshelf `timestamps` plugin sets `created_at` and `updated_at`
|
||||
* - if plugin is disabled (e.g. import) we have a fallback condition
|
||||
*
|
||||
* Exceptions: internal context or importing
|
||||
*/
|
||||
onCreating: function onCreating(model, attr, options) {
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'created_by')) {
|
||||
if (!options.importing || (options.importing && !this.get('created_by'))) {
|
||||
this.set('created_by', String(this.contextUser(options)));
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'updated_by')) {
|
||||
if (!options.importing) {
|
||||
this.set('updated_by', String(this.contextUser(options)));
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'created_at')) {
|
||||
if (!model.get('created_at')) {
|
||||
model.set('created_at', new Date());
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'updated_at')) {
|
||||
if (!model.get('updated_at')) {
|
||||
model.set('updated_at', new Date());
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(this.onValidate(model, attr, options))
|
||||
.then(() => {
|
||||
/**
|
||||
* @NOTE:
|
||||
*
|
||||
* The API requires only specific attributes to send. If we don't set the rest explicitly to null,
|
||||
* we end up in a situation that on "created" events the field set is incomplete, which is super confusing
|
||||
* and hard to work with if you trigger internal events, which rely on full field set. This ensures consistency.
|
||||
*
|
||||
* @NOTE:
|
||||
*
|
||||
* Happens after validation to ensure we don't set fields which are not nullable on db level.
|
||||
*/
|
||||
_.each(Object.keys(schema.tables[this.tableName]).filter(key => key.indexOf('@@') === -1), (columnKey) => {
|
||||
if (model.get(columnKey) === undefined) {
|
||||
model.set(columnKey, null);
|
||||
}
|
||||
});
|
||||
|
||||
model._changed = _.cloneDeep(model.changed);
|
||||
});
|
||||
},
|
||||
|
||||
onUpdated(model, attrs, options) {
|
||||
this.addAction(model, 'edited', options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Changing resources implies setting these properties on the server side
|
||||
* - set `updated_by` based on the context
|
||||
* - ensure `created_at` never changes
|
||||
* - ensure `created_by` never changes
|
||||
* - the bookshelf `timestamps` plugin sets `updated_at` automatically
|
||||
*
|
||||
* Exceptions:
|
||||
* - importing data
|
||||
* - internal context
|
||||
* - if no context
|
||||
*
|
||||
* @deprecated: x_by fields (https://github.com/TryGhost/Ghost/issues/10286)
|
||||
*/
|
||||
onUpdating: function onUpdating(model, attr, options) {
|
||||
if (this.relationships) {
|
||||
model.changed = _.omit(model.changed, this.relationships);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'updated_by')) {
|
||||
if (!options.importing && !options.migrating) {
|
||||
this.set('updated_by', String(this.contextUser(options)));
|
||||
}
|
||||
}
|
||||
|
||||
if (options && options.context && !options.context.internal && !options.importing) {
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'created_at')) {
|
||||
if (model.hasDateChanged('created_at', {beforeWrite: true})) {
|
||||
model.set('created_at', this.previous('created_at'));
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'created_by')) {
|
||||
if (model.hasChanged('created_by')) {
|
||||
model.set('created_by', String(this.previous('created_by')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: do not allow setting only the `updated_at` field, exception: importing
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'updated_at') && !options.importing) {
|
||||
if (options.migrating) {
|
||||
model.set('updated_at', model.previous('updated_at'));
|
||||
} else if (Object.keys(model.changed).length === 1 && model.changed.updated_at) {
|
||||
model.set('updated_at', model.previous('updated_at'));
|
||||
delete model.changed.updated_at;
|
||||
}
|
||||
}
|
||||
|
||||
model._changed = _.cloneDeep(model.changed);
|
||||
|
||||
return Promise.resolve(this.onValidate(model, attr, options));
|
||||
},
|
||||
|
||||
onSaved() {},
|
||||
|
||||
onSaving: function onSaving() {
|
||||
// Remove any properties which don't belong on the model
|
||||
this.attributes = this.pick(this.permittedAttributes());
|
||||
},
|
||||
|
||||
onDestroying() {},
|
||||
|
||||
onDestroyed(model, options) {
|
||||
if (!model._changed) {
|
||||
model._changed = {};
|
||||
}
|
||||
|
||||
// @NOTE: Bookshelf destroys ".changed" right after this event, but we should not throw away the information
|
||||
// It is useful for webhooks, events etc.
|
||||
// @NOTE: Bookshelf returns ".changed = {empty...}" on destroying (https://github.com/bookshelf/bookshelf/issues/1943)
|
||||
Object.assign(model._changed, _.cloneDeep(model.changed));
|
||||
|
||||
this.addAction(model, 'deleted', options);
|
||||
}
|
||||
});
|
||||
};
|
@ -13,7 +13,6 @@ const Promise = require('bluebird');
|
||||
const ObjectId = require('bson-objectid');
|
||||
const debug = require('@tryghost/debug')('models:base');
|
||||
const db = require('../../data/db');
|
||||
const events = require('../../lib/common/events');
|
||||
const errors = require('@tryghost/errors');
|
||||
const security = require('@tryghost/security');
|
||||
const schema = require('../../data/schema');
|
||||
@ -64,86 +63,9 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* @NOTE
|
||||
* We have to remember the `_previousAttributes` attributes, because when destroying resources
|
||||
* We listen on the `onDestroyed` event and Bookshelf resets these properties right after the event.
|
||||
* If the query runs in a txn, `_previousAttributes` will be empty.
|
||||
*/
|
||||
emitChange: function (model, event, options) {
|
||||
const _emit = (ghostEvent, _model, opts) => {
|
||||
if (!_model.wasChanged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
debug(_model.tableName, ghostEvent);
|
||||
|
||||
// @NOTE: Internal Ghost events. These are very granular e.g. post.published
|
||||
events.emit(ghostEvent, _model, opts);
|
||||
};
|
||||
|
||||
if (!options.transacting) {
|
||||
return _emit(event, model, options);
|
||||
}
|
||||
|
||||
if (!model.ghostEvents) {
|
||||
model.ghostEvents = [];
|
||||
|
||||
// CASE: when importing, deleting or migrating content, lot's of model queries are happening in one transaction
|
||||
// lot's of model events will be triggered. we ensure we set the max listeners to infinity.
|
||||
// we are using `once` - we auto remove the listener afterwards
|
||||
if (options.importing || options.destroyAll || options.migrating) {
|
||||
options.transacting.setMaxListeners(0);
|
||||
}
|
||||
|
||||
options.transacting.once('committed', (committed) => {
|
||||
if (!committed) {
|
||||
return;
|
||||
}
|
||||
|
||||
_.each(this.ghostEvents, (obj) => {
|
||||
_emit(obj.event, model, obj.options);
|
||||
});
|
||||
|
||||
delete model.ghostEvents;
|
||||
});
|
||||
}
|
||||
|
||||
model.ghostEvents.push({
|
||||
event: event,
|
||||
options: {
|
||||
importing: options.importing,
|
||||
context: options.context
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Bookshelf `initialize` - declare a constructor-like method for model creation
|
||||
initialize: function initialize() {
|
||||
const self = this;
|
||||
|
||||
// NOTE: triggered before `creating`/`updating`
|
||||
this.on('saving', function onSaving(newObj, attrs, options) {
|
||||
if (options.method === 'insert') {
|
||||
// id = 0 is still a valid value for external usage
|
||||
if (_.isUndefined(newObj.id) || _.isNull(newObj.id)) {
|
||||
newObj.setId();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.on('fetched', self.onFetched);
|
||||
self.on('fetching', self.onFetching);
|
||||
self.on('fetched:collection', self.onFetchedCollection);
|
||||
self.on('fetching:collection', self.onFetchingCollection);
|
||||
self.on('creating', self.onCreating);
|
||||
self.on('created', self.onCreated);
|
||||
self.on('updating', self.onUpdating);
|
||||
self.on('updated', self.onUpdated);
|
||||
self.on('destroying', self.onDestroying);
|
||||
self.on('destroyed', self.onDestroyed);
|
||||
self.on('saving', self.onSaving);
|
||||
self.on('saved', self.onSaved);
|
||||
this.initializeEvents();
|
||||
|
||||
// @NOTE: Please keep here. If we don't initialize the parent, bookshelf-relations won't work.
|
||||
proto.initialize.call(this);
|
||||
@ -175,179 +97,6 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
return parentSync;
|
||||
},
|
||||
|
||||
/**
|
||||
* Do not call `toJSON`. This can remove properties e.g. password.
|
||||
* @returns {*}
|
||||
*/
|
||||
onValidate: function onValidate(model, columns, options) {
|
||||
this.setEmptyValuesToNull();
|
||||
return schema.validate(this.tableName, this, options);
|
||||
},
|
||||
|
||||
onFetched() {},
|
||||
|
||||
/**
|
||||
* http://knexjs.org/#Builder-forUpdate
|
||||
* https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
|
||||
*
|
||||
* Lock target collection/model for further update operations.
|
||||
* This avoids collisions and possible content override cases.
|
||||
*/
|
||||
onFetching: function onFetching(model, columns, options) {
|
||||
if (options.forUpdate && options.transacting) {
|
||||
options.query.forUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
onFetchedCollection() {},
|
||||
|
||||
onFetchingCollection: function onFetchingCollection(model, columns, options) {
|
||||
if (options.forUpdate && options.transacting) {
|
||||
options.query.forUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
onCreated(model, attrs, options) {
|
||||
this.addAction(model, 'added', options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adding resources implies setting these properties on the server side
|
||||
* - set `created_by` based on the context
|
||||
* - set `updated_by` based on the context
|
||||
* - the bookshelf `timestamps` plugin sets `created_at` and `updated_at`
|
||||
* - if plugin is disabled (e.g. import) we have a fallback condition
|
||||
*
|
||||
* Exceptions: internal context or importing
|
||||
*/
|
||||
onCreating: function onCreating(model, attr, options) {
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'created_by')) {
|
||||
if (!options.importing || (options.importing && !this.get('created_by'))) {
|
||||
this.set('created_by', String(this.contextUser(options)));
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'updated_by')) {
|
||||
if (!options.importing) {
|
||||
this.set('updated_by', String(this.contextUser(options)));
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'created_at')) {
|
||||
if (!model.get('created_at')) {
|
||||
model.set('created_at', new Date());
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'updated_at')) {
|
||||
if (!model.get('updated_at')) {
|
||||
model.set('updated_at', new Date());
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(this.onValidate(model, attr, options))
|
||||
.then(() => {
|
||||
/**
|
||||
* @NOTE:
|
||||
*
|
||||
* The API requires only specific attributes to send. If we don't set the rest explicitly to null,
|
||||
* we end up in a situation that on "created" events the field set is incomplete, which is super confusing
|
||||
* and hard to work with if you trigger internal events, which rely on full field set. This ensures consistency.
|
||||
*
|
||||
* @NOTE:
|
||||
*
|
||||
* Happens after validation to ensure we don't set fields which are not nullable on db level.
|
||||
*/
|
||||
_.each(Object.keys(schema.tables[this.tableName]).filter(key => key.indexOf('@@') === -1), (columnKey) => {
|
||||
if (model.get(columnKey) === undefined) {
|
||||
model.set(columnKey, null);
|
||||
}
|
||||
});
|
||||
|
||||
model._changed = _.cloneDeep(model.changed);
|
||||
});
|
||||
},
|
||||
|
||||
onUpdated(model, attrs, options) {
|
||||
this.addAction(model, 'edited', options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Changing resources implies setting these properties on the server side
|
||||
* - set `updated_by` based on the context
|
||||
* - ensure `created_at` never changes
|
||||
* - ensure `created_by` never changes
|
||||
* - the bookshelf `timestamps` plugin sets `updated_at` automatically
|
||||
*
|
||||
* Exceptions:
|
||||
* - importing data
|
||||
* - internal context
|
||||
* - if no context
|
||||
*
|
||||
* @deprecated: x_by fields (https://github.com/TryGhost/Ghost/issues/10286)
|
||||
*/
|
||||
onUpdating: function onUpdating(model, attr, options) {
|
||||
if (this.relationships) {
|
||||
model.changed = _.omit(model.changed, this.relationships);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'updated_by')) {
|
||||
if (!options.importing && !options.migrating) {
|
||||
this.set('updated_by', String(this.contextUser(options)));
|
||||
}
|
||||
}
|
||||
|
||||
if (options && options.context && !options.context.internal && !options.importing) {
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'created_at')) {
|
||||
if (model.hasDateChanged('created_at', {beforeWrite: true})) {
|
||||
model.set('created_at', this.previous('created_at'));
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'created_by')) {
|
||||
if (model.hasChanged('created_by')) {
|
||||
model.set('created_by', String(this.previous('created_by')));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: do not allow setting only the `updated_at` field, exception: importing
|
||||
if (Object.prototype.hasOwnProperty.call(schema.tables[this.tableName], 'updated_at') && !options.importing) {
|
||||
if (options.migrating) {
|
||||
model.set('updated_at', model.previous('updated_at'));
|
||||
} else if (Object.keys(model.changed).length === 1 && model.changed.updated_at) {
|
||||
model.set('updated_at', model.previous('updated_at'));
|
||||
delete model.changed.updated_at;
|
||||
}
|
||||
}
|
||||
|
||||
model._changed = _.cloneDeep(model.changed);
|
||||
|
||||
return Promise.resolve(this.onValidate(model, attr, options));
|
||||
},
|
||||
|
||||
onSaved() {},
|
||||
|
||||
onSaving: function onSaving() {
|
||||
// Remove any properties which don't belong on the model
|
||||
this.attributes = this.pick(this.permittedAttributes());
|
||||
},
|
||||
|
||||
onDestroying() {},
|
||||
|
||||
onDestroyed(model, options) {
|
||||
if (!model._changed) {
|
||||
model._changed = {};
|
||||
}
|
||||
|
||||
// @NOTE: Bookshelf destroys ".changed" right after this event, but we should not throw away the information
|
||||
// It is useful for webhooks, events etc.
|
||||
// @NOTE: Bookshelf returns ".changed = {empty...}" on destroying (https://github.com/bookshelf/bookshelf/issues/1943)
|
||||
Object.assign(model._changed, _.cloneDeep(model.changed));
|
||||
|
||||
this.addAction(model, 'deleted', options);
|
||||
},
|
||||
|
||||
/**
|
||||
* before we insert dates into the database, we have to normalize
|
||||
* date format is now in each db the same
|
||||
|
Loading…
Reference in New Issue
Block a user