rittenhop-dev/versions/5.94.2/node_modules/@tryghost/bookshelf-include-count/lib/bookshelf-include-count.js

156 lines
5.3 KiB
JavaScript
Raw Normal View History

2024-09-23 19:40:12 -04:00
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;
};