156 lines
5.3 KiB
JavaScript
156 lines
5.3 KiB
JavaScript
const _debug = require('@tryghost/debug')._base;
|
|
const debug = _debug('ghost-query');
|
|
const _ = require('lodash');
|
|
|
|
/**
|
|
* @param {import('bookshelf')} Bookshelf
|
|
*/
|
|
module.exports = function (Bookshelf) {
|
|
const modelProto = Bookshelf.Model.prototype;
|
|
|
|
const addCounts = function (options) {
|
|
if (!options) {
|
|
return;
|
|
}
|
|
if (!options.withRelated) {
|
|
return;
|
|
}
|
|
|
|
// Helper methods
|
|
// withRelated can be an object or an array of strings. We need to support handling both representations.
|
|
// ['user', 'replies']
|
|
// OR
|
|
// [
|
|
// {'user': function() {} }
|
|
// ]
|
|
|
|
function hasWithRelated(key) {
|
|
for (const item of options.withRelated) {
|
|
if (typeof item !== 'string') {
|
|
if (item[key] !== undefined) {
|
|
return true;
|
|
}
|
|
}
|
|
if (item === key) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function removeWithRelated(key) {
|
|
// VERY IMPORTANT HERE:
|
|
// We need to keep the reference to the withRelated array and not create a new array
|
|
// This is required to make eager relations work correctly (otherwise the updated withRelated won't get passed further)
|
|
const newItems = options.withRelated.filter((item) => {
|
|
if (typeof item === 'string') {
|
|
return item !== key;
|
|
}
|
|
return item[key] === undefined;
|
|
});
|
|
options.withRelated.splice(0, options.withRelated.length, ...newItems);
|
|
}
|
|
|
|
// This can run in both a model or in a collection
|
|
// We need access to the model's (optional) countRelations method.
|
|
let model = this.constructor;
|
|
if (this.model) {
|
|
model = this.model;
|
|
}
|
|
|
|
if (model.countRelations) {
|
|
const countRelations = model.countRelations();
|
|
for (const countRelation of Object.keys(countRelations)) {
|
|
if (hasWithRelated('count.' + countRelation)) {
|
|
// remove post_count from withRelated
|
|
removeWithRelated('count.' + countRelation);
|
|
|
|
// Call the query builder
|
|
countRelations[countRelation](this, options);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const Model = Bookshelf.Model.extend({
|
|
addCounts,
|
|
serialize: function serialize(options) {
|
|
const attrs = modelProto.serialize.call(this, options);
|
|
const countRegex = /^(count)(__)(.*)$/;
|
|
|
|
_.forOwn(attrs, function (value, key) {
|
|
const match = key.match(countRegex);
|
|
if (match) {
|
|
attrs[match[1]] = attrs[match[1]] || {};
|
|
attrs[match[1]][match[3]] = value;
|
|
delete attrs[key];
|
|
}
|
|
});
|
|
|
|
return attrs;
|
|
},
|
|
|
|
/**
|
|
* Instead of adding the counts in .fetch and .fetchAll,
|
|
* we need to do it in sync because Bookshelf doesn't call fetch for eagerRelations
|
|
* E.g. when trying to load counts on replies.count.likes, we wouldn't get an opportunity to load the counts on the replies relation.
|
|
*/
|
|
sync: function (options) {
|
|
if (!options.method || (options.method !== 'insert' && options.method !== 'update')) {
|
|
this.addCounts.apply(this, arguments);
|
|
}
|
|
|
|
if (_debug.enabled('ghost-query')) {
|
|
debug('QUERY', this.query().toQuery());
|
|
}
|
|
|
|
// Call parent fetchAll
|
|
return modelProto.sync.apply(this, arguments);
|
|
},
|
|
|
|
save: function save() {
|
|
// the count__ variables are not 'permitted' and will get removed after a save
|
|
// so this will make sure they are kept alive after a save (unless they are also still available after the save)
|
|
|
|
const savedAttributes = {};
|
|
const countRegex = /^(count)(__)(.*)$/;
|
|
|
|
for (const key of Object.keys(this.attributes)) {
|
|
const match = key.match(countRegex);
|
|
if (match) {
|
|
savedAttributes[key] = this.attributes[key];
|
|
}
|
|
}
|
|
|
|
return modelProto.save.apply(this, arguments).then((t) => {
|
|
// Set savedAttributes, but keep count__ variables if they stayed inside this.attributes
|
|
if (savedAttributes) {
|
|
Object.assign(this.attributes, savedAttributes, this.attributes);
|
|
}
|
|
return t;
|
|
});
|
|
}
|
|
});
|
|
|
|
Bookshelf.Model = Model;
|
|
|
|
const collectionProto = Bookshelf.Collection.prototype;
|
|
|
|
const Collection = Bookshelf.Collection.extend({
|
|
addCounts,
|
|
sync: function () {
|
|
// For now, only apply this for eager loaded collections
|
|
this.addCounts.apply(this, arguments);
|
|
|
|
if (_debug.enabled('ghost-query')) {
|
|
debug('QUERY', this.query().toQuery());
|
|
}
|
|
|
|
// Call parent fetchAll
|
|
return collectionProto.sync.apply(this, arguments);
|
|
}
|
|
});
|
|
|
|
Bookshelf.Collection = Collection;
|
|
};
|