2020-04-29 18:44:27 +03:00
|
|
|
const moment = require('moment-timezone');
|
|
|
|
const Promise = require('bluebird');
|
|
|
|
const _ = require('lodash');
|
2020-05-22 21:22:20 +03:00
|
|
|
const errors = require('@tryghost/errors');
|
2017-04-19 16:53:23 +03:00
|
|
|
|
|
|
|
module.exports = function (Bookshelf) {
|
2020-04-29 18:44:27 +03:00
|
|
|
const ParentModel = Bookshelf.Model;
|
2017-04-19 16:53:23 +03:00
|
|
|
|
2020-04-29 18:44:27 +03:00
|
|
|
const Model = Bookshelf.Model.extend({
|
2017-04-19 16:53:23 +03:00
|
|
|
/**
|
|
|
|
* Update collision protection.
|
|
|
|
*
|
|
|
|
* IMPORTANT NOTES:
|
|
|
|
* The `sync` method is called for any query e.g. update, add, delete, fetch
|
|
|
|
*
|
|
|
|
* We had the option to override Bookshelf's `save` method, but hooking into the `sync` method gives us
|
|
|
|
* the ability to access the `changed` object. Bookshelf already knows which attributes has changed.
|
|
|
|
*
|
|
|
|
* Bookshelf's timestamp function can't be overridden, as it's synchronous, there is no way to return an Error.
|
|
|
|
*
|
|
|
|
* If we want to enable the collision plugin for other tables, the queries might need to run in a transaction.
|
|
|
|
* This depends on if we fetch the model before editing. Imagine two concurrent requests come in, both would fetch
|
|
|
|
* the same current database values and both would succeed to update and override each other.
|
|
|
|
*/
|
|
|
|
sync: function timestamp(options) {
|
2020-04-29 18:44:27 +03:00
|
|
|
const parentSync = ParentModel.prototype.sync.apply(this, arguments);
|
|
|
|
const originalUpdateSync = parentSync.update;
|
|
|
|
const self = this;
|
2017-04-19 16:53:23 +03:00
|
|
|
|
|
|
|
// CASE: only enabled for posts table
|
|
|
|
if (this.tableName !== 'posts' ||
|
|
|
|
!self.serverData ||
|
|
|
|
((options.method !== 'update' && options.method !== 'patch') || !options.method)
|
|
|
|
) {
|
|
|
|
return parentSync;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Only hook into the update sync
|
|
|
|
*
|
|
|
|
* IMPORTANT NOTES:
|
|
|
|
* Even if the client sends a different `id` property, it get's ignored by bookshelf.
|
|
|
|
* Because you can't change the `id` of an existing post.
|
|
|
|
*
|
|
|
|
* HTML is always auto generated, ignore.
|
|
|
|
*/
|
|
|
|
parentSync.update = function update() {
|
2019-02-02 12:24:53 +03:00
|
|
|
return originalUpdateSync.apply(this, arguments)
|
|
|
|
.then((response) => {
|
|
|
|
const changed = _.omit(self._changed, [
|
|
|
|
'created_at', 'updated_at', 'author_id', 'id',
|
|
|
|
'published_by', 'updated_by', 'html', 'plaintext'
|
|
|
|
]);
|
2017-04-19 16:53:23 +03:00
|
|
|
|
2019-02-02 12:24:53 +03:00
|
|
|
const clientUpdatedAt = moment(self.clientData.updated_at || self.serverData.updated_at || new Date());
|
|
|
|
const serverUpdatedAt = moment(self.serverData.updated_at || clientUpdatedAt);
|
2017-04-19 16:53:23 +03:00
|
|
|
|
2019-02-02 12:24:53 +03:00
|
|
|
if (Object.keys(changed).length) {
|
|
|
|
if (clientUpdatedAt.diff(serverUpdatedAt) !== 0) {
|
|
|
|
// @NOTE: This will rollback the update. We cannot know if relations were updated before doing the update.
|
2020-05-22 21:22:20 +03:00
|
|
|
return Promise.reject(new errors.UpdateCollisionError({
|
2019-02-02 12:24:53 +03:00
|
|
|
message: 'Saving failed! Someone else is editing this post.',
|
|
|
|
code: 'UPDATE_COLLISION',
|
|
|
|
level: 'critical',
|
2019-04-09 08:00:56 +03:00
|
|
|
errorDetails: {
|
2019-02-02 12:24:53 +03:00
|
|
|
clientUpdatedAt: self.clientData.updated_at,
|
2019-04-09 08:00:56 +03:00
|
|
|
serverUpdatedAt: self.serverData.updated_at
|
2019-03-07 07:58:44 +03:00
|
|
|
}
|
2019-02-02 12:24:53 +03:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return response;
|
|
|
|
});
|
2017-04-19 16:53:23 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
return parentSync;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* We have to remember current server data and client data.
|
|
|
|
* The `sync` method has no access to it.
|
|
|
|
* `updated_at` is already set to "Date.now" when the overridden `sync.update` is called.
|
|
|
|
* See https://github.com/tgriesser/bookshelf/blob/79c526870e618748caf94e7476a0bc796ee090a6/src/model.js#L955
|
|
|
|
*/
|
|
|
|
save: function save(data) {
|
|
|
|
this.clientData = _.cloneDeep(data) || {};
|
|
|
|
this.serverData = _.cloneDeep(this.attributes);
|
|
|
|
|
|
|
|
return ParentModel.prototype.save.apply(this, arguments);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
Bookshelf.Model = Model;
|
|
|
|
};
|