Skip to content

Commit

Permalink
Add bookshelf-relations (#9252)
Browse files Browse the repository at this point in the history
no issue

- added https://github.com/TryGhost/bookshelf-relations as dependency
- remove existing tag handling

--- 

* Important: Ensure we trigger parent initialize function

- otherwise the plugin is unable to listen on model events
- important: event order for listeners is Ghost -> Plugin
- Ghost should be able to listen on the events as first instance
- e.g. be able to modify/validate relationships

* Fix tag validation

- we detect lower/update case slugs for tags manually
- this can't be taken over from the plugin obviously
- ensure we update the target model e.g. this.set('tags', ...)

* override base fn: `permittedAttributes`

- ensure we call the base
- put relations on top
- each relation is allowed to be passed
- the plugin will auto-unset any relations to it does not reach the database

* Ensure we run add/edit/delete within a transaction

- updating nested relationships requires sql queries
- all sql statements have to run in a single transaction to ensure we rollback everything if an error occurs
- use es6
  • Loading branch information
kirrg001 authored and kevinansfield committed Nov 21, 2017
1 parent 982a75d commit dfd4afe
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 1,237 deletions.
24 changes: 24 additions & 0 deletions core/server/models/base/index.js
Expand Up @@ -45,6 +45,27 @@ ghostBookshelf.plugin(plugins.pagination);
// Update collision plugin
ghostBookshelf.plugin(plugins.collision);

// Manages nested updates (relationships)
ghostBookshelf.plugin('bookshelf-relations', {
allowedOptions: ['context'],
unsetRelations: true,
hooks: {
belongsToMany: {
after: function (existing, targets, options) {
// reorder tags
return Promise.each(targets.models, function (target, index) {
return existing.updatePivot({
sort_order: index
}, _.extend({}, options, {query: {where: {tag_id: target.id}}}));
});
},
beforeRelationCreation: function onCreatingRelation(model, data) {
data.id = ObjectId.generate();
}
}
}
});

// Cache an instance of the base model prototype
proto = ghostBookshelf.Model.prototype;

Expand Down Expand Up @@ -119,6 +140,9 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
return Promise.resolve(self.onValidate.apply(self, args));
});
});

// NOTE: Please keep here. If we don't initialize the parent, bookshelf-relations won't work.
proto.initialize.call(this);
},

onValidate: function onValidate() {
Expand Down
67 changes: 1 addition & 66 deletions core/server/models/base/utils.js
Expand Up @@ -6,7 +6,7 @@ var _ = require('lodash'),
Promise = require('bluebird'),
ObjectId = require('bson-objectid'),
errors = require('../../errors'),
tagUpdate, attach;
attach;

/**
* Attach wrapper (please never call attach manual!)
Expand Down Expand Up @@ -63,69 +63,4 @@ attach = function attach(Model, effectedModelId, relation, modelsToAttach, optio
});
};

tagUpdate = {
fetchCurrentPost: function fetchCurrentPost(PostModel, id, options) {
return PostModel.forge({id: id}).fetch(_.extend({}, options, {withRelated: ['tags']}));
},

fetchMatchingTags: function fetchMatchingTags(TagModel, tagsToMatch, options) {
if (_.isEmpty(tagsToMatch)) {
return false;
}
return TagModel.forge()
.query('whereIn', 'name', _.map(tagsToMatch, 'name')).fetchAll(options);
},

detachTagFromPost: function detachTagFromPost(post, tag, options) {
return function () {
// See tgriesser/bookshelf#294 for an explanation of _.omit(options, 'query')
return post.tags().detach(tag.id, _.omit(options, 'query'));
};
},

attachTagToPost: function attachTagToPost(Post, postId, tag, index, options) {
return function () {
// See tgriesser/bookshelf#294 for an explanation of _.omit(options, 'query')
return attach(Post, postId, 'tags', [{tag_id: tag.id, sort_order: index}], _.omit(options, 'query'));
};
},

createTagThenAttachTagToPost: function createTagThenAttachTagToPost(PostModel, TagModel, post, tag, index, options) {
var fields = ['name', 'slug', 'description', 'feature_image', 'visibility', 'parent_id', 'meta_title', 'meta_description'];
return function () {
return TagModel.add(_.pick(tag, fields), options).then(function then(createdTag) {
return tagUpdate.attachTagToPost(PostModel, post.id, createdTag, index, options)();
});
};
},

updateTagOrderForPost: function updateTagOrderForPost(post, tag, index, options) {
return function () {
return post.tags().updatePivot(
{sort_order: index}, _.extend({}, options, {query: {where: {tag_id: tag.id}}})
);
};
},

// Test if two tags are the same, checking ID first, and falling back to name
tagsAreEqual: function tagsAreEqual(tag1, tag2) {
if (tag1.hasOwnProperty('id') && tag2.hasOwnProperty('id')) {
return tag1.id === tag2.id;
}
return tag1.name.toString() === tag2.name.toString();
},

tagSetsAreEqual: function tagSetsAreEqual(tags1, tags2) {
// If the lengths are different, they cannot be the same
if (tags1.length !== tags2.length) {
return false;
}
// Return if no item is not the same (double negative is horrible)
return !_.some(tags1, function (tag1, index) {
return !tagUpdate.tagsAreEqual(tag1, tags2[index]);
});
}
};

module.exports.attach = attach;
module.exports.tagUpdate = tagUpdate;

0 comments on commit dfd4afe

Please sign in to comment.