Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ability to notify and update url service about changes in related resources #10336

Merged
merged 18 commits into from
Jan 8, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/server/models/base/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,10 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
require('../plugins/has-posts').addHasPostsWhere(tableNames[modelName], shouldHavePosts)(query);
}

if (options.id) {
query.where({id: options.id});
}

return query.then((objects) => {
debug('fetched', modelName, filter);

Expand Down
60 changes: 59 additions & 1 deletion core/server/models/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ Post = ghostBookshelf.Model.extend({
// Fire edited if this wasn't a change between resourceType
model.emitChange('edited', options);
}

if (model.statusChanging && (model.isPublished || model.wasPublished)) {
this.handleStatusForAttachedModels(model, options);
}
},

onDestroyed: function onDestroyed(model, options) {
Expand All @@ -185,6 +189,58 @@ Post = ghostBookshelf.Model.extend({
model.emitChange('deleted', Object.assign({usePreviousAttribute: true}, options));
},

onDestroying: function onDestroyed(model) {
naz marked this conversation as resolved.
Show resolved Hide resolved
this.handleAttachedModels(model);
},

handleAttachedModels: function handleAttachedModels(model) {
/**
* @NOTE:
* Bookshelf only exposes the object that is being detached on `detaching`.
* For the reason above, `detached` handler is using the scope of `detaching`
* to access the models that are not present in `detached`.
*/
model.related('tags').once('detaching', function onDetached(collection, tag) {
model.related('tags').once('detached', function onDetached(detachedCollection, response, options) {
tag.emitChange('detached', options);
});
});

model.related('tags').once('attaching', function onDetached(collection, tags) {
model.related('tags').once('attached', function onDetached(detachedCollection, response, options) {
tags.forEach(tag => tag.emitChange('attached', options));
});
});

model.related('authors').once('detaching', function onDetached(collection, author) {
model.related('authors').once('detached', function onDetached(detachedCollection, response, options) {
author.emitChange('detached', options);
});
});

model.related('authors').once('attaching', function onDetached(collection, authors) {
model.related('authors').once('attached', function onDetached(detachedCollection, response, options) {
authors.forEach(author => author.emitChange('attached', options));
});
});
},

/**
* @NOTE:
* when status is changed from or to 'published' all related authors and tags
* have to trigger recalculation in URL service because status is applied in filters for
* these models
*/
handleStatusForAttachedModels: function handleStatusForAttachedModels(model, options) {
model.related('tags').forEach((tag) => {
tag.emitChange('attached', options);
naz marked this conversation as resolved.
Show resolved Hide resolved
});

model.related('authors').forEach((author) => {
author.emitChange('attached', options);
});
},

onSaving: function onSaving(model, attr, options) {
options = options || {};

Expand Down Expand Up @@ -260,6 +316,8 @@ Post = ghostBookshelf.Model.extend({
this.set('tags', tagsToSave);
}

this.handleAttachedModels(model);

ghostBookshelf.Model.prototype.onSaving.call(this, model, attr, options);

// do not allow generated fields to be overridden via the API
Expand Down Expand Up @@ -632,7 +690,7 @@ Post = ghostBookshelf.Model.extend({
* and updating resources. We won't return the relations by default for now.
*/
defaultRelations: function defaultRelations(methodName, options) {
if (['edit', 'add'].indexOf(methodName) !== -1) {
if (['edit', 'add', 'destroy'].indexOf(methodName) !== -1) {
naz marked this conversation as resolved.
Show resolved Hide resolved
options.withRelated = _.union(['authors', 'tags'], options.withRelated || []);
}

Expand Down
166 changes: 60 additions & 106 deletions core/server/services/url/Resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,17 @@ class Resources {
return this._onResourceAdded.bind(this)(resourceConfig.type, model);
});

this._listenOn(resourceConfig.events.update, (model) => {
return this._onResourceUpdated.bind(this)(resourceConfig.type, model);
});
if (_.isArray(resourceConfig.events.update)) {
resourceConfig.events.update.forEach((event) => {
this._listenOn(event, (model) => {
return this._onResourceUpdated.bind(this)(resourceConfig.type, model);
});
});
} else {
this._listenOn(resourceConfig.events.update, (model) => {
return this._onResourceUpdated.bind(this)(resourceConfig.type, model);
});
}

this._listenOn(resourceConfig.events.remove, (model) => {
return this._onResourceRemoved.bind(this)(resourceConfig.type, model);
Expand Down Expand Up @@ -111,59 +119,37 @@ class Resources {
});
}

_fetchSingle(resourceConfig, id) {
let modelOptions = _.cloneDeep(resourceConfig.modelOptions);
modelOptions.id = id;

return models.Base.Model.raw_knex.fetchAll(modelOptions);
}

_onResourceAdded(type, model) {
naz marked this conversation as resolved.
Show resolved Hide resolved
const resourceConfig = _.find(this.resourcesConfig, {type: type});
const exclude = resourceConfig.modelOptions.exclude;
const withRelatedFields = resourceConfig.modelOptions.withRelatedFields;
const obj = _.omit(model.toJSON(), exclude);

if (withRelatedFields) {
_.each(withRelatedFields, (fields, key) => {
if (!obj[key]) {
return;
}

obj[key] = _.map(obj[key], (relation) => {
const relationToReturn = {};
return Promise.resolve()
.then(() => {
return this._fetchSingle(resourceConfig, model.id);
})
.then(([dbResource]) => {
if (dbResource) {
const resource = new Resource(type, dbResource);

_.each(fields, (field) => {
const fieldSanitized = field.replace(/^\w+./, '');
relationToReturn[fieldSanitized] = relation[fieldSanitized];
});
debug('_onResourceAdded', type);
this.data[type].push(resource);

return relationToReturn;
});
this.queue.start({
event: 'added',
action: 'added:' + model.id,
eventData: {
id: model.id,
type: type
}
});
}
});

const withRelatedPrimary = resourceConfig.modelOptions.withRelatedPrimary;

if (withRelatedPrimary) {
_.each(withRelatedPrimary, (relation, primaryKey) => {
if (!obj[primaryKey] || !obj[relation]) {
return;
}

const targetTagKeys = Object.keys(obj[relation].find((item) => {
return item.id === obj[primaryKey].id;
}));
obj[primaryKey] = _.pick(obj[primaryKey], targetTagKeys);
});
}
}

const resource = new Resource(type, obj);

debug('_onResourceAdded', type);
this.data[type].push(resource);

this.queue.start({
event: 'added',
action: 'added:' + model.id,
eventData: {
id: model.id,
type: type
}
});
}

/**
Expand All @@ -183,67 +169,35 @@ class Resources {
_onResourceUpdated(type, model) {
debug('_onResourceUpdated', type);

this.data[type].every((resource) => {
if (resource.data.id === model.id) {
naz marked this conversation as resolved.
Show resolved Hide resolved
const resourceConfig = _.find(this.resourcesConfig, {type: type});
const exclude = resourceConfig.modelOptions.exclude;
const withRelatedFields = resourceConfig.modelOptions.withRelatedFields;
const obj = _.omit(model.toJSON(), exclude);

if (withRelatedFields) {
_.each(withRelatedFields, (fields, key) => {
if (!obj[key]) {
return;
}

obj[key] = _.map(obj[key], (relation) => {
const relationToReturn = {};

_.each(fields, (field) => {
const fieldSanitized = field.replace(/^\w+./, '');
relationToReturn[fieldSanitized] = relation[fieldSanitized];
});

return relationToReturn;
});
});

const withRelatedPrimary = resourceConfig.modelOptions.withRelatedPrimary;
const resourceConfig = _.find(this.resourcesConfig, {type: type});

if (withRelatedPrimary) {
_.each(withRelatedPrimary, (relation, primaryKey) => {
if (!obj[primaryKey] || !obj[relation]) {
return;
return Promise.resolve()
.then(() => {
return this._fetchSingle(resourceConfig, model.id);
})
.then(([dbResource]) => {
const resource = this.data[type].find(resource => (resource.data.id === model.id));

if (resource && dbResource) {
resource.update(dbResource);

// CASE: pretend it was added
if (!resource.isReserved()) {
this.queue.start({
event: 'added',
action: 'added:' + dbResource.id,
eventData: {
id: dbResource.id,
type: type
}

const targetTagKeys = Object.keys(obj[relation].find((item) => {
return item.id === obj[primaryKey].id;
}));
obj[primaryKey] = _.pick(obj[primaryKey], targetTagKeys);
});
}
} else if (!resource && dbResource) {
this._onResourceAdded(type, model);
} else if (resource && !dbResource) {
this._onResourceRemoved(type, model);
}

resource.update(obj);

// CASE: pretend it was added
if (!resource.isReserved()) {
this.queue.start({
event: 'added',
action: 'added:' + model.id,
eventData: {
id: model.id,
type: type
}
});
}

// break!
return false;
}

return true;
});
});
}

_onResourceRemoved(type, model) {
Expand Down
4 changes: 2 additions & 2 deletions core/server/services/url/configs/v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ module.exports = [
},
events: {
add: 'tag.added',
update: 'tag.edited',
update: ['tag.edited', 'tag.attached', 'tag.detached'],
naz marked this conversation as resolved.
Show resolved Hide resolved
remove: 'tag.deleted'
}
},
Expand Down Expand Up @@ -138,7 +138,7 @@ module.exports = [
},
events: {
add: 'user.activated',
update: 'user.activated.edited',
update: ['user.activated.edited', 'user.attached', 'user.detached'],
remove: 'user.deactivated'
}
}
Expand Down