mirror of https://github.com/TryGhost/Ghost.git
235 lines
9.4 KiB
JavaScript
235 lines
9.4 KiB
JavaScript
var _ = require('lodash'),
|
|
gql = require('ghost-gql'),
|
|
common = require('../../lib/common'),
|
|
filter,
|
|
filterUtils;
|
|
|
|
filterUtils = {
|
|
/**
|
|
* ## Combine Filters
|
|
* Util to combine the enforced, default and custom filters such that they behave accordingly
|
|
* @param {String|Object} enforced - filters which must ALWAYS be applied
|
|
* @param {String|Object} defaults - filters which must be applied if a matching filter isn't provided
|
|
* @param {...String|Object} [custom] - custom filters which are additional
|
|
* @returns {*}
|
|
*/
|
|
combineFilters: function combineFilters(enforced, defaults, custom /* ...custom */) {
|
|
custom = Array.prototype.slice.call(arguments, 2);
|
|
|
|
// Ensure everything has been run through the gql parser
|
|
try {
|
|
enforced = enforced ? (_.isString(enforced) ? gql.parse(enforced) : enforced) : null;
|
|
defaults = defaults ? (_.isString(defaults) ? gql.parse(defaults) : defaults) : null;
|
|
custom = _.map(custom, function (arg) {
|
|
return _.isString(arg) ? gql.parse(arg) : arg;
|
|
});
|
|
} catch (err) {
|
|
throw new common.errors.ValidationError({
|
|
err: err,
|
|
property: 'filter',
|
|
context: common.i18n.t('errors.models.plugins.filter.errorParsing'),
|
|
help: common.i18n.t('errors.models.plugins.filter.forInformationRead', {url: 'https://api.ghost.org/v1.22.0/docs/filter'})
|
|
});
|
|
}
|
|
|
|
// Merge custom filter options into a single set of statements
|
|
custom = gql.json.mergeStatements.apply(this, custom);
|
|
|
|
// if there is no enforced or default statements, return just the custom statements;
|
|
if (!enforced && !defaults) {
|
|
return custom;
|
|
}
|
|
|
|
// Reduce custom filters based on enforced filters
|
|
if (custom && !_.isEmpty(custom.statements) && enforced && !_.isEmpty(enforced.statements)) {
|
|
custom.statements = gql.json.rejectStatements(custom.statements, function (customStatement) {
|
|
return gql.json.findStatement(enforced.statements, customStatement, 'prop');
|
|
});
|
|
}
|
|
|
|
// Reduce default filters based on custom filters
|
|
if (defaults && !_.isEmpty(defaults.statements) && custom && !_.isEmpty(custom.statements)) {
|
|
defaults.statements = gql.json.rejectStatements(defaults.statements, function (defaultStatement) {
|
|
return gql.json.findStatement(custom.statements, defaultStatement, 'prop');
|
|
});
|
|
}
|
|
|
|
// Merge enforced and defaults
|
|
enforced = gql.json.mergeStatements(enforced, defaults);
|
|
|
|
if (_.isEmpty(custom.statements)) {
|
|
return enforced;
|
|
}
|
|
|
|
if (_.isEmpty(enforced.statements)) {
|
|
return custom;
|
|
}
|
|
|
|
return {
|
|
statements: [
|
|
{group: enforced.statements},
|
|
{group: custom.statements, func: 'and'}
|
|
]
|
|
};
|
|
}
|
|
};
|
|
|
|
filter = function filter(Bookshelf) {
|
|
var Model = Bookshelf.Model.extend({
|
|
// Cached copy of the filters setup for this model instance
|
|
_filters: null,
|
|
// Override these on the various models
|
|
enforcedFilters: function enforcedFilters() {
|
|
},
|
|
defaultFilters: function defaultFilters() {
|
|
},
|
|
|
|
preProcessFilters: function preProcessFilters() {
|
|
this._filters.statements = gql.json.replaceStatements(this._filters.statements, {prop: /primary_tag/}, function (statement) {
|
|
statement.prop = 'tags.slug';
|
|
return {
|
|
group: [
|
|
statement,
|
|
{prop: 'posts_tags.sort_order', op: '=', value: 0},
|
|
{prop: 'tags.visibility', op: '=', value: 'public'}
|
|
]
|
|
};
|
|
});
|
|
|
|
this._filters.statements = gql.json.replaceStatements(this._filters.statements, {prop: /primary_author/}, function (statement) {
|
|
statement.prop = 'authors.slug';
|
|
return {
|
|
group: [
|
|
statement,
|
|
{prop: 'posts_authors.sort_order', op: '=', value: 0},
|
|
{prop: 'authors.visibility', op: '=', value: 'public'}
|
|
]
|
|
};
|
|
});
|
|
},
|
|
|
|
/**
|
|
* ## Post process Filters
|
|
* Post Process filters looking for joins etc
|
|
* @TODO refactor this
|
|
* @param {object} options
|
|
*/
|
|
postProcessFilters: function postProcessFilters(options) {
|
|
var joinTables = this._filters.joins;
|
|
|
|
if (joinTables && joinTables.indexOf('tags') > -1) {
|
|
// We need to use leftOuterJoin to insure we still include posts which don't have tags in the result
|
|
// The where clause should restrict which items are returned
|
|
this
|
|
.query('leftOuterJoin', 'posts_tags', 'posts_tags.post_id', '=', 'posts.id')
|
|
.query('leftOuterJoin', 'tags', 'posts_tags.tag_id', '=', 'tags.id');
|
|
|
|
// The order override should ONLY happen if we are doing an "IN" query
|
|
// TODO move the order handling to the query building that is currently inside pagination
|
|
// TODO make the order handling in pagination handle orderByRaw
|
|
// TODO extend this handling to all joins
|
|
if (gql.json.findStatement(this._filters.statements, {prop: /^tags/, op: 'IN'})) {
|
|
// TODO make this count the number of MATCHING tags, not just the number of tags
|
|
this.query('orderByRaw', 'count(tags.id) DESC');
|
|
}
|
|
|
|
// We need to add a group by to counter the double left outer join
|
|
// TODO improve on the group by handling
|
|
options.groups = options.groups || [];
|
|
options.groups.push('posts.id');
|
|
}
|
|
|
|
if (joinTables && joinTables.indexOf('authors') > -1) {
|
|
// We need to use leftOuterJoin to insure we still include posts which don't have tags in the result
|
|
// The where clause should restrict which items are returned
|
|
this
|
|
.query('leftOuterJoin', 'posts_authors', 'posts_authors.post_id', '=', 'posts.id')
|
|
.query('leftOuterJoin', 'users as authors', 'posts_authors.author_id', '=', 'authors.id');
|
|
|
|
// The order override should ONLY happen if we are doing an "IN" query
|
|
// TODO move the order handling to the query building that is currently inside pagination
|
|
// TODO make the order handling in pagination handle orderByRaw
|
|
// TODO extend this handling to all joins
|
|
if (gql.json.findStatement(this._filters.statements, {prop: /^authors/, op: 'IN'})) {
|
|
// TODO make this count the number of MATCHING authors, not just the number of authors
|
|
this.query('orderByRaw', 'count(authors.id) DESC');
|
|
}
|
|
|
|
// We need to add a group by to counter the double left outer join
|
|
// TODO improve on the group by handling
|
|
options.groups = options.groups || [];
|
|
options.groups.push('posts.id');
|
|
}
|
|
|
|
/**
|
|
* @deprecated: `author`, will be removed in Ghost 2.0
|
|
*/
|
|
if (joinTables && joinTables.indexOf('author') > -1) {
|
|
this
|
|
.query('join', 'users as author', 'author.id', '=', 'posts.author_id');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* ## fetchAndCombineFilters
|
|
* Helper method, uses the combineFilters util to apply filters to the current model instance
|
|
* based on options and the set enforced/default filters for this resource
|
|
* @param {Object} options
|
|
* @returns {Bookshelf.Model}
|
|
*/
|
|
fetchAndCombineFilters: function fetchAndCombineFilters(options) {
|
|
options = options || {};
|
|
|
|
this._filters = filterUtils.combineFilters(
|
|
this.enforcedFilters(options),
|
|
this.defaultFilters(options),
|
|
options.filter,
|
|
options.where
|
|
);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* ## Apply Filters
|
|
* Method which makes the necessary query builder calls (through knex) for the filters set
|
|
* on this model instance
|
|
* @param {Object} options
|
|
* @returns {Bookshelf.Model}
|
|
*/
|
|
applyDefaultAndCustomFilters: function applyDefaultAndCustomFilters(options) {
|
|
var self = this;
|
|
|
|
// @TODO figure out a better place/way to trigger loading filters
|
|
if (!this._filters) {
|
|
this.fetchAndCombineFilters(options);
|
|
}
|
|
|
|
if (this._filters) {
|
|
if (this.debug) {
|
|
gql.json.printStatements(this._filters.statements);
|
|
}
|
|
|
|
this.preProcessFilters(options);
|
|
|
|
this.query(function (qb) {
|
|
gql.knexify(qb, self._filters);
|
|
});
|
|
|
|
// Replaces processGQLResult
|
|
this.postProcessFilters(options);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
});
|
|
|
|
Bookshelf.Model = Model;
|
|
};
|
|
|
|
/**
|
|
* ## Export Filter plugin
|
|
* @api public
|
|
*/
|
|
module.exports = filter;
|