diff --git a/core/server/models/base/bookshelf.js b/core/server/models/base/bookshelf.js index 822f3dad56..e9639891f9 100644 --- a/core/server/models/base/bookshelf.js +++ b/core/server/models/base/bookshelf.js @@ -62,6 +62,8 @@ ghostBookshelf.plugin(require('./plugins/user-type')); ghostBookshelf.plugin(require('./plugins/data-manipulation')); +ghostBookshelf.plugin(require('./plugins/overrides')); + // Manages nested updates (relationships) ghostBookshelf.plugin('bookshelf-relations', { allowedOptions: ['context', 'importing', 'migrating'], diff --git a/core/server/models/base/index.js b/core/server/models/base/index.js index 8e02c68856..193ac321f2 100644 --- a/core/server/models/base/index.js +++ b/core/server/models/base/index.js @@ -6,18 +6,14 @@ // accesses the models directly. // All other parts of Ghost, including the frontend & admin UI are only allowed to access data via the API. -const _ = require('lodash'); - const moment = require('moment'); const ObjectId = require('bson-objectid'); const schema = require('../../data/schema'); const ghostBookshelf = require('./bookshelf'); -let proto; - // Cache an instance of the base model prototype -proto = ghostBookshelf.Model.prototype; +const proto = ghostBookshelf.Model.prototype; // ## ghostBookshelf.Model // The Base Model which other Ghost objects will inherit from, @@ -51,88 +47,11 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ proto.initialize.call(this); }, - /** - * Bookshelf's .format() is run when fetching as well as saving. - * We need a way to transform attributes only on save so we override - * .sync() which is run on every database operation where we can - * run any transforms needed only on insert and update operations - */ - sync: function sync() { - const parentSync = proto.sync.apply(this, arguments); - const originalUpdateSync = parentSync.update; - const originalInsertSync = parentSync.insert; - const self = this; - - // deep clone attrs to avoid modifying underlying model attributes by reference - parentSync.update = function update(attrs) { - attrs = self.formatOnWrite(_.cloneDeep(attrs)); - return originalUpdateSync.apply(this, [attrs]); - }; - - parentSync.insert = function insert(attrs) { - attrs = self.formatOnWrite(_.cloneDeep(attrs)); - return originalInsertSync.apply(this, [attrs]); - }; - - return parentSync; - }, - - // format date before writing to DB, bools work - format: function format(attrs) { - return this.fixDatesWhenSave(attrs); - }, - // overridable function for models to format attrs only when saving to db formatOnWrite: function formatOnWrite(attrs) { return attrs; }, - // format data and bool when fetching from DB - parse: function parse(attrs) { - return this.fixBools(this.fixDatesWhenFetch(attrs)); - }, - - /** - * `shallow` - won't return relations - * `omitPivot` - won't return pivot fields - * - * `toJSON` calls `serialize`. - * - * @param unfilteredOptions - * @returns {*} - */ - toJSON: function toJSON(unfilteredOptions) { - const options = ghostBookshelf.Model.filterOptions(unfilteredOptions, 'toJSON'); - options.omitPivot = true; - - /** - * removes null relations coming from `hasOne` - https://bookshelfjs.org/api.html#Model-instance-hasOne - * Based on https://github.com/bookshelf/bookshelf/issues/72#issuecomment-25164617 - */ - _.each(this.relations, (value, key) => { - if (_.isEmpty(value)) { - delete this.relations[key]; - } - }); - // CASE: get JSON of previous attrs - if (options.previous) { - const clonedModel = _.cloneDeep(this); - clonedModel.attributes = this._previousAttributes; - - if (this.relationships) { - this.relationships.forEach((relation) => { - if (this._previousRelations && Object.prototype.hasOwnProperty.call(this._previousRelations, relation)) { - clonedModel.related(relation).models = this._previousRelations[relation].models; - } - }); - } - - return proto.toJSON.call(clonedModel, options); - } - - return proto.toJSON.call(this, options); - }, - hasDateChanged: function (attr) { return moment(this.get(attr)).diff(moment(this.previous(attr))) !== 0; }, diff --git a/core/server/models/base/plugins/overrides.js b/core/server/models/base/plugins/overrides.js new file mode 100644 index 0000000000..5cb11dcf0f --- /dev/null +++ b/core/server/models/base/plugins/overrides.js @@ -0,0 +1,87 @@ +const _ = require('lodash'); + +/** + * @param {import('bookshelf')} Bookshelf + */ +module.exports = function (Bookshelf) { + const ParentModel = Bookshelf.Model; + + Bookshelf.Model = Bookshelf.Model.extend({ + /** + * Bookshelf's .format() is run when fetching as well as saving. + * We need a way to transform attributes only on save so we override + * .sync() which is run on every database operation where we can + * run any transforms needed only on insert and update operations + */ + sync: function sync() { + const parentSync = ParentModel.prototype.sync.apply(this, arguments); + const originalUpdateSync = parentSync.update; + const originalInsertSync = parentSync.insert; + const self = this; + + // deep clone attrs to avoid modifying underlying model attributes by reference + parentSync.update = function update(attrs) { + attrs = self.formatOnWrite(_.cloneDeep(attrs)); + return originalUpdateSync.apply(this, [attrs]); + }; + + parentSync.insert = function insert(attrs) { + attrs = self.formatOnWrite(_.cloneDeep(attrs)); + return originalInsertSync.apply(this, [attrs]); + }; + + return parentSync; + }, + + // format date before writing to DB, bools work + format: function format(attrs) { + return this.fixDatesWhenSave(attrs); + }, + + // format data and bool when fetching from DB + parse: function parse(attrs) { + return this.fixBools(this.fixDatesWhenFetch(attrs)); + }, + + /** + * `shallow` - won't return relations + * `omitPivot` - won't return pivot fields + * + * `toJSON` calls `serialize`. + * + * @param unfilteredOptions + * @returns {*} + */ + toJSON: function toJSON(unfilteredOptions) { + const options = Bookshelf.Model.filterOptions(unfilteredOptions, 'toJSON'); + options.omitPivot = true; + + /** + * removes null relations coming from `hasOne` - https://bookshelfjs.org/api.html#Model-instance-hasOne + * Based on https://github.com/bookshelf/bookshelf/issues/72#issuecomment-25164617 + */ + _.each(this.relations, (value, key) => { + if (_.isEmpty(value)) { + delete this.relations[key]; + } + }); + // CASE: get JSON of previous attrs + if (options.previous) { + const clonedModel = _.cloneDeep(this); + clonedModel.attributes = this._previousAttributes; + + if (this.relationships) { + this.relationships.forEach((relation) => { + if (this._previousRelations && Object.prototype.hasOwnProperty.call(this._previousRelations, relation)) { + clonedModel.related(relation).models = this._previousRelations[relation].models; + } + }); + } + + return ParentModel.prototype.toJSON.call(clonedModel, options); + } + + return ParentModel.prototype.toJSON.call(this, options); + } + }); +};