text: // # Pagination
  //
  // Extends Bookshelf.Model with a `fetchPage` method. Handles everything to do with paginated requests.
  const _ = require('lodash');
  
  const {i18n} = require('../../lib/common');
  const errors = require('@tryghost/errors');
  let defaults;
  let paginationUtils;
  let pagination;
  
  /**
   * ### 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
       * @param {bookshelf.Model} model
       * @param {options} options
       */
      addLimitAndOffset: function addLimitAndOffset(model, options) {
          if (_.isNumber(options.limit)) {
              model
                  .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) {
          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
          };
  
          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;
      },
  
      /**
       *
       * @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);
              }
          }
      }
  };
  
  // ## Object Definitions
  
  /**
   * ### Pagination Object
   * @typedef {Object} pagination
   * @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
   */
  
  /**
   * ### Fetch Page Options
   * @typedef {Object} options
   * @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
   */
  
  /**
   * ### Fetch Page Response
   * @typedef {Object} paginatedResult
   * @property {Array} collection \- set of results
   * @property {pagination} pagination \- pagination metadata
   */
  
  /**
   * ## Pagination
   * Extends `bookshelf.Model` with `fetchPage`
   * @param {Bookshelf} bookshelf \- the instance to plug into
   */
  pagination = function pagination(bookshelf) {
      // Extend updates the first object passed to it, no need for an assignment
      _.extend(bookshelf.Model.prototype, {
          /**
           * ### Fetch page
           * A `fetch` extension to get a paginated set of items from a collection
           *
           * 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
           *
           * @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
              const tableName = _.result(this.constructor.prototype, 'tableName');
  
              const idAttribute = _.result(this.constructor.prototype, 'idAttribute');
              const self = this;
  
              // #### 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
              // 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')
                  );
              }
  
              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 {
                              self.query('orderBy', property, direction);
  
                              paginationUtils.handleRelation(self, property);
                          }
                      });
                  } 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
  
                  return self.fetchAll(_.omit(options, ['page', 'limit']))
                      .then(function (fetchResult) {
                          if (options.limit === 'all') {
                              countResult = [{aggregate: fetchResult.length}];
                          }
  
                          return {
                              collection: fetchResult,
                              pagination: paginationUtils.formatResponse(countResult[0] ? countResult[0].aggregate : 0, options)
                          };
                      })
                      .catch(function (err) {
                          // e.g. offset/limit reached max allowed integer value
                          if (err.errno === 20 || err.errno === 1064) {
                              throw new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')});
                          }
  
                          throw err;
                      });
              }).catch((err) => {
                  // CASE: SQL syntax is incorrect
                  if (err.errno === 1054 || err.errno === 1) {
                      throw new errors.BadRequestError({
                          message: i18n.t('errors.models.general.sql'),
                          err: err
                      });
                  }
  
                  throw err;
              });
          }
      });
  };
  
  /**
   * ## Export pagination plugin
   * @api public
   */
  module.exports = pagination;
  