2015-06-17 16:55:39 +03:00
|
|
|
// # Pagination
|
|
|
|
//
|
|
|
|
// Extends Bookshelf.Model with a `fetchPage` method. Handles everything to do with paginated requests.
|
2020-04-29 18:44:27 +03:00
|
|
|
const _ = require('lodash');
|
|
|
|
|
2020-05-22 21:22:20 +03:00
|
|
|
const {i18n} = require('../../lib/common');
|
|
|
|
const errors = require('@tryghost/errors');
|
2020-04-29 18:44:27 +03:00
|
|
|
let defaults;
|
|
|
|
let paginationUtils;
|
2015-06-17 16:55:39 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Default pagination values
|
|
|
|
* These are overridden via `options` passed to each function
|
|
|
|
* @typedef {Object} defaults
|
|
|
|
* @default
|
|
|
|
* @property {Number} `page` \- page in set to display (default: 1)
|
|
|
|
* @property {Number|String} `limit` \- no. results per page (default: 15)
|
|
|
|
*/
|
|
|
|
defaults = {
|
|
|
|
page: 1,
|
|
|
|
limit: 15
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ## Pagination Utils
|
|
|
|
* @api private
|
|
|
|
* @type {{parseOptions: Function, query: Function, formatResponse: Function}}
|
|
|
|
*/
|
|
|
|
paginationUtils = {
|
|
|
|
/**
|
|
|
|
* ### Parse Options
|
|
|
|
* Take the given options and ensure they are valid pagination options, else use the defaults
|
|
|
|
* @param {options} options
|
|
|
|
* @returns {options} options sanitised for pagination
|
|
|
|
*/
|
|
|
|
parseOptions: function parseOptions(options) {
|
|
|
|
options = _.defaults(options || {}, defaults);
|
|
|
|
|
|
|
|
if (options.limit !== 'all') {
|
|
|
|
options.limit = parseInt(options.limit, 10) || defaults.limit;
|
|
|
|
}
|
|
|
|
|
|
|
|
options.page = parseInt(options.page, 10) || defaults.page;
|
|
|
|
|
|
|
|
return options;
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* ### Query
|
|
|
|
* Apply the necessary parameters to paginate the query
|
2015-11-03 01:40:06 +03:00
|
|
|
* @param {bookshelf.Model} model
|
2015-06-17 16:55:39 +03:00
|
|
|
* @param {options} options
|
|
|
|
*/
|
2015-11-03 01:40:06 +03:00
|
|
|
addLimitAndOffset: function addLimitAndOffset(model, options) {
|
2015-06-17 16:55:39 +03:00
|
|
|
if (_.isNumber(options.limit)) {
|
2015-11-03 01:40:06 +03:00
|
|
|
model
|
2015-06-17 16:55:39 +03:00
|
|
|
.query('limit', options.limit)
|
|
|
|
.query('offset', options.limit * (options.page - 1));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Format Response
|
|
|
|
* Takes the no. items returned and original options and calculates all of the pagination meta data
|
|
|
|
* @param {Number} totalItems
|
|
|
|
* @param {options} options
|
|
|
|
* @returns {pagination} pagination metadata
|
|
|
|
*/
|
|
|
|
formatResponse: function formatResponse(totalItems, options) {
|
2020-04-29 18:44:27 +03:00
|
|
|
const calcPages = Math.ceil(totalItems / options.limit) || 0;
|
|
|
|
|
|
|
|
const pagination = {
|
|
|
|
page: options.page || defaults.page,
|
|
|
|
limit: options.limit,
|
|
|
|
pages: calcPages === 0 ? 1 : calcPages,
|
|
|
|
total: totalItems,
|
|
|
|
next: null,
|
|
|
|
prev: null
|
|
|
|
};
|
2015-06-17 16:55:39 +03:00
|
|
|
|
|
|
|
if (pagination.pages > 1) {
|
|
|
|
if (pagination.page === 1) {
|
|
|
|
pagination.next = pagination.page + 1;
|
|
|
|
} else if (pagination.page === pagination.pages) {
|
|
|
|
pagination.prev = pagination.page - 1;
|
|
|
|
} else {
|
|
|
|
pagination.next = pagination.page + 1;
|
|
|
|
pagination.prev = pagination.page - 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return pagination;
|
2020-09-24 04:32:40 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {Bookshelf.Model} model instance of Bookshelf model
|
|
|
|
* @param {string} propertyName property to be inspected and included in the relation
|
|
|
|
*/
|
|
|
|
handleRelation: function handleRelation(model, propertyName) {
|
|
|
|
const tableName = _.result(model.constructor.prototype, 'tableName');
|
|
|
|
|
|
|
|
const targetTable = propertyName.includes('.') && propertyName.split('.')[0];
|
|
|
|
|
|
|
|
if (targetTable && targetTable !== tableName) {
|
|
|
|
if (!model.eagerLoad) {
|
|
|
|
model.eagerLoad = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!model.eagerLoad.includes(targetTable)) {
|
|
|
|
model.eagerLoad.push(targetTable);
|
|
|
|
}
|
|
|
|
}
|
2015-06-17 16:55:39 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// ## Object Definitions
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Pagination Object
|
|
|
|
* @typedef {Object} pagination
|
2015-11-03 01:40:06 +03:00
|
|
|
* @property {Number} page \- page in set to display
|
|
|
|
* @property {Number|String} limit \- no. results per page, or 'all'
|
|
|
|
* @property {Number} pages \- total no. pages in the full set
|
|
|
|
* @property {Number} total \- total no. items in the full set
|
|
|
|
* @property {Number|null} next \- next page
|
|
|
|
* @property {Number|null} prev \- previous page
|
2015-06-17 16:55:39 +03:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Fetch Page Options
|
|
|
|
* @typedef {Object} options
|
2015-11-03 01:40:06 +03:00
|
|
|
* @property {Number} page \- page in set to display
|
|
|
|
* @property {Number|String} limit \- no. results per page, or 'all'
|
|
|
|
* @property {Object} order \- set of order by params and directions
|
2015-06-17 16:55:39 +03:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ### Fetch Page Response
|
|
|
|
* @typedef {Object} paginatedResult
|
2015-11-03 01:40:06 +03:00
|
|
|
* @property {Array} collection \- set of results
|
2015-06-17 16:55:39 +03:00
|
|
|
* @property {pagination} pagination \- pagination metadata
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ## Pagination
|
|
|
|
* Extends `bookshelf.Model` with `fetchPage`
|
|
|
|
* @param {Bookshelf} bookshelf \- the instance to plug into
|
|
|
|
*/
|
2020-10-20 02:02:56 +03:00
|
|
|
const pagination = function pagination(bookshelf) {
|
2015-06-17 16:55:39 +03:00
|
|
|
// Extend updates the first object passed to it, no need for an assignment
|
|
|
|
_.extend(bookshelf.Model.prototype, {
|
|
|
|
/**
|
2017-11-01 16:44:54 +03:00
|
|
|
* ### Fetch page
|
2015-06-17 16:55:39 +03:00
|
|
|
* A `fetch` extension to get a paginated set of items from a collection
|
2016-07-15 13:04:10 +03:00
|
|
|
*
|
|
|
|
* We trigger two queries:
|
|
|
|
* 1. count query to know how many pages left (important: we don't attach any group/order statements!)
|
|
|
|
* 2. the actualy fetch query with limit and page property
|
|
|
|
*
|
2015-06-17 16:55:39 +03:00
|
|
|
* @param {options} options
|
|
|
|
* @returns {paginatedResult} set of results + pagination metadata
|
|
|
|
*/
|
|
|
|
fetchPage: function fetchPage(options) {
|
|
|
|
// Setup pagination options
|
|
|
|
options = paginationUtils.parseOptions(options);
|
|
|
|
|
|
|
|
// Get the table name and idAttribute for this model
|
2020-04-29 18:44:27 +03:00
|
|
|
const tableName = _.result(this.constructor.prototype, 'tableName');
|
|
|
|
|
|
|
|
const idAttribute = _.result(this.constructor.prototype, 'idAttribute');
|
|
|
|
const self = this;
|
2019-11-27 13:00:27 +03:00
|
|
|
|
2015-07-29 21:07:58 +03:00
|
|
|
// #### Pre count clauses
|
|
|
|
// Add any where or join clauses which need to be included with the aggregate query
|
|
|
|
|
|
|
|
// Clone the base query & set up a promise to get the count of total items in the full set
|
2020-08-27 02:07:35 +03:00
|
|
|
// Necessary due to lack of support for `count distinct` in bookshelf's count()
|
|
|
|
// Skipped if limit='all' as we can use the length of the fetched data set
|
|
|
|
let countPromise = Promise.resolve();
|
|
|
|
if (options.limit !== 'all') {
|
|
|
|
const countQuery = this.query().clone();
|
|
|
|
|
|
|
|
if (options.transacting) {
|
|
|
|
countQuery.transacting(options.transacting);
|
|
|
|
}
|
|
|
|
|
|
|
|
countPromise = countQuery.select(
|
|
|
|
bookshelf.knex.raw('count(distinct ' + tableName + '.' + idAttribute + ') as aggregate')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-07-15 13:04:10 +03:00
|
|
|
return countPromise.then(function (countResult) {
|
|
|
|
// #### Post count clauses
|
|
|
|
// Add any where or join clauses which need to NOT be included with the aggregate query
|
|
|
|
|
|
|
|
// Setup the pagination parameters so that we return the correct items from the set
|
|
|
|
paginationUtils.addLimitAndOffset(self, options);
|
|
|
|
|
|
|
|
// Apply ordering options if they are present
|
|
|
|
if (options.order && !_.isEmpty(options.order)) {
|
|
|
|
_.forOwn(options.order, function (direction, property) {
|
|
|
|
if (property === 'count.posts') {
|
|
|
|
self.query('orderBy', 'count__posts', direction);
|
|
|
|
} else {
|
2020-09-24 04:32:40 +03:00
|
|
|
self.query('orderBy', property, direction);
|
|
|
|
|
|
|
|
paginationUtils.handleRelation(self, property);
|
2016-07-15 13:04:10 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
} else if (options.orderRaw) {
|
|
|
|
self.query(function (qb) {
|
|
|
|
qb.orderByRaw(options.orderRaw);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.groups && !_.isEmpty(options.groups)) {
|
|
|
|
_.each(options.groups, function (group) {
|
|
|
|
self.query('groupBy', group);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Setup the promise to do a fetch on our collection, running the specified query
|
|
|
|
// @TODO: ensure option handling is done using an explicit pick elsewhere
|
2020-09-24 04:32:40 +03:00
|
|
|
|
2016-07-15 13:04:10 +03:00
|
|
|
return self.fetchAll(_.omit(options, ['page', 'limit']))
|
|
|
|
.then(function (fetchResult) {
|
2020-08-27 02:07:35 +03:00
|
|
|
if (options.limit === 'all') {
|
|
|
|
countResult = [{aggregate: fetchResult.length}];
|
|
|
|
}
|
|
|
|
|
2016-07-15 13:04:10 +03:00
|
|
|
return {
|
|
|
|
collection: fetchResult,
|
|
|
|
pagination: paginationUtils.formatResponse(countResult[0] ? countResult[0].aggregate : 0, options)
|
|
|
|
};
|
2017-11-14 15:47:58 +03:00
|
|
|
})
|
|
|
|
.catch(function (err) {
|
|
|
|
// e.g. offset/limit reached max allowed integer value
|
|
|
|
if (err.errno === 20 || err.errno === 1064) {
|
2020-05-22 21:22:20 +03:00
|
|
|
throw new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')});
|
2017-11-14 15:47:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
throw err;
|
2016-07-15 13:04:10 +03:00
|
|
|
});
|
🐛 Fixed all known filter limitations (#10159)
refs #10105, closes #10108, closes https://github.com/TryGhost/Ghost/issues/9950, refs https://github.com/TryGhost/Ghost/issues/9923, refs https://github.com/TryGhost/Ghost/issues/9916, refs https://github.com/TryGhost/Ghost/issues/9574, refs https://github.com/TryGhost/Ghost/issues/6345, refs https://github.com/TryGhost/Ghost/issues/6309, refs https://github.com/TryGhost/Ghost/issues/6158, refs https://github.com/TryGhost/GQL/issues/16
- removed GQL dependency
- replaced GQL with our brand new NQL implementation
- fixed all known filter limitations
- GQL suffered from some underlying filter bugs, which NQL tried to fix
- the bugs were mostly in how we query the database for relation filtering
- the underlying problem was caused by a too simple implementation of querying the relations
- mongo-knex has implemented a more robust and complex filtering mechanism for relations
- replaced logic in our bookshelf filter plugin
- we pass the custom, default and override filters from Ghost to NQL, which then are getting parsed and merged into a mongo JSON object. The mongo JSON is getting attached by mongo-knex.
NQL: https://github.com/NexesJS/NQL
mongo-knex: https://github.com/NexesJS/mongo-knex
2018-12-11 13:53:40 +03:00
|
|
|
}).catch((err) => {
|
|
|
|
// CASE: SQL syntax is incorrect
|
|
|
|
if (err.errno === 1054 || err.errno === 1) {
|
2020-05-22 21:22:20 +03:00
|
|
|
throw new errors.BadRequestError({
|
|
|
|
message: i18n.t('errors.models.general.sql'),
|
🐛 Fixed all known filter limitations (#10159)
refs #10105, closes #10108, closes https://github.com/TryGhost/Ghost/issues/9950, refs https://github.com/TryGhost/Ghost/issues/9923, refs https://github.com/TryGhost/Ghost/issues/9916, refs https://github.com/TryGhost/Ghost/issues/9574, refs https://github.com/TryGhost/Ghost/issues/6345, refs https://github.com/TryGhost/Ghost/issues/6309, refs https://github.com/TryGhost/Ghost/issues/6158, refs https://github.com/TryGhost/GQL/issues/16
- removed GQL dependency
- replaced GQL with our brand new NQL implementation
- fixed all known filter limitations
- GQL suffered from some underlying filter bugs, which NQL tried to fix
- the bugs were mostly in how we query the database for relation filtering
- the underlying problem was caused by a too simple implementation of querying the relations
- mongo-knex has implemented a more robust and complex filtering mechanism for relations
- replaced logic in our bookshelf filter plugin
- we pass the custom, default and override filters from Ghost to NQL, which then are getting parsed and merged into a mongo JSON object. The mongo JSON is getting attached by mongo-knex.
NQL: https://github.com/NexesJS/NQL
mongo-knex: https://github.com/NexesJS/mongo-knex
2018-12-11 13:53:40 +03:00
|
|
|
err: err
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
throw err;
|
2015-06-17 16:55:39 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ## Export pagination plugin
|
|
|
|
* @api public
|
|
|
|
*/
|
|
|
|
module.exports = pagination;
|