anyong / bookshelf-page

Simple pagination for Bookshelf.js
MIT License
5 stars 1 forks source link

custom the pagination metadata by developer #5

Open arden opened 8 years ago

arden commented 8 years ago

about the pageination metadata, this can custom by developer self?

anyong commented 8 years ago

What metadata do you want besides the page count and row count?

arden commented 8 years ago

i'm want to Pagination's metadata as below:

pagination: {
    page: 1,           // current page
    pageSize: 20,    // size for per page.
    total: 10,          // total rows
    pages: 1,         // total pages
    prePage: 1,     // pre page
    nextPage: 1    // next page
}

Because developers will have different habit for you, or, In order to legacy system. Now, i can only fork the code and modify by self.

arden commented 8 years ago

Below is my modified version that can custom pagination metadata by developer self:

'use strict';

import Promise from 'bluebird';
import { remove as _remove, assign as _assign } from 'lodash';

const DEFAULT_LIMIT = 10;
const DEFAULT_OFFSET = 0;
const DEFAULT_PAGE = 1;

let DEFAULT_METADATA = {
    rowCount: 'rowCount',
    pageCount: 'pageCount',
    page: 'page',
    pageSize: 'pageSize',
    prePage: 'prePage',
    nextPage: 'nextPage',
    offset: 'offset',
    limit: 'limit'
};

/**
 * Exports a plugin to pass into the bookshelf instance, i.e.:
 *
 *      import config from './knexfile';
 *      import knex from 'knex';
 *      import bookshelf from 'bookshelf';
 *
 *      const ORM = bookshelf(knex(config));
 *
 *      ORM.plugin('bookshelf-pagination-plugin');
 *
 *      export default ORM;
 *
 * The plugin attaches two instance methods to the bookshelf
 * Model object: orderBy and fetchPage.
 *
 * Model#orderBy calls the underlying query builder's orderBy method, and
 * is useful for ordering the paginated results.
 *
 * Model#fetchPage works like Model#fetchAll, but returns a single page of
 * results instead of all results, as well as the pagination information
 *
 * See methods below for details.
 */
module.exports = function paginationPlugin (bookshelf, customMetaData) {
    let _customMetaData = customMetaData || DEFAULT_METADATA;
    let orignCustomMetaData = _customMetaData;

    /**
     * @method Model#orderBy
     * @since 0.9.3
     * @description
     *
     * Specifies the column to sort on and sort order.
     *
     * The order parameter is optional, and defaults to 'ASC'. You may
     * also specify 'DESC' order by prepending a hyphen to the sort column
     * name. `orderBy("date", 'DESC')` is the same as `orderBy("-date")`.
     *
     * Unless specified using dot notation (i.e., "table.column"), the default
     * table will be the table name of the model `orderBy` was called on.
     *
     * @example
     *
     * Cars.forge().orderBy('color', 'ASC').fetchAll()
     *    .then(function (rows) { // ...
     *
     * @param sort {string}
     *   Column to sort on
     * @param order {string}
     *   Ascending ('ASC') or descending ('DESC') order
     */
    function orderBy (sort, order) {
        const tableName = this.constructor.prototype.tableName;
        const idAttribute = this.constructor.prototype.idAttribute ?
            this.constructor.prototype.idAttribute : 'id';

        let _sort;

        if (sort && sort.indexOf('-') === 0) {
            _sort = sort.slice(1);
        } else if (sort) {
            _sort = sort;
        } else {
            _sort = idAttribute;
        }

        const _order = order || (
                (sort && sort.indexOf('-') === 0) ? 'DESC' : 'ASC'
            );

        if (_sort.indexOf('.') === -1) {
            _sort = `${tableName}.${_sort}`;
        }

        return this.query(qb => {
            qb.orderBy(_sort, _order);
        });
    }

    /**
     * Similar to {@link Model#fetchAll}, but fetches a single page of results
     * as specified by the limit (page size) and offset or page number.
     *
     * Any options that may be passed to {@link Model#fetchAll} may also be passed
     * in the options to this method.
     *
     * To perform pagination, you may include *either* an `offset` and `limit`, **or**
     * a `page` and `pageSize`.
     *
     * By default, with no parameters or missing parameters, `fetchPage` will use an
     * options object of `{page: 1, pageSize: 10}`
     *
     *
     * Below is an example showing the user of a JOIN query with sort/ordering,
     * pagination, and related models.
     *
     * @example
     *
     * Car
     * .query(function (qb) {
     *    qb.innerJoin('manufacturers', 'cars.manufacturer_id', 'manufacturers.id');
     *    qb.groupBy('cars.id');
     *    qb.where('manufacturers.country', '=', 'Sweden');
     * })
     * .orderBy('-productionYear') // Same as .orderBy('cars.productionYear', 'DESC')
     * .fetchPage({
     *    pageSize: 15, // Defaults to 10 if not specified
     *    page: 3, // Defaults to 1 if not specified
     *
     *    // OR
     *    // limit: 15,
     *    // offset: 30,
     *
     *    withRelated: ['engine'] // Passed to Model#fetchAll
     * })
     * .then(function (results) {
     *    console.log(results); // Paginated results object with metadata example below
     * })
     *
     * // Pagination results:
     *
     * {
     *    models: [<Car>], // Regular bookshelf Collection
     *    // other standard Collection attributes
     *    ...
     *    pagination: {
     *        rowCount: 53, // Total number of rows found for the query before pagination
     *        pageCount: 4, // Total number of pages of results
     *        page: 3, // The requested page number
     *        pageSze: 15, // The requested number of rows per page
     *
     *  // OR, if limit/offset pagination is used instead of page/pageSize:
     *        // offset: 30, // The requested offset
     *        // limit: 15 // The requested limit
     *    }
     * }
     *
     * @param options {object}
     *    The pagination options, plus any additional options that will be passed to
     *    {@link Model#fetchAll}
     * @returns {Promise<Model|null>}
     */
    function fetchPage (options = {}) {
        const {page, pageSize, limit, offset, meta, ...fetchOptions} = options;

        let usingPageSize = false; // usingPageSize = false means offset/limit, true means page/pageSize
        let _page;
        let _pageSize;
        let _limit;
        let _offset;

        _customMetaData = meta || orignCustomMetaData;

        function ensureIntWithDefault (val, def) {
            if (!val) {
                return def;
            }

            const _val = parseInt(val);
            if (Number.isNaN(_val) || _val <= 0) {
                return def;
            }

            return _val;
        }

        if (!limit && !offset) {
            usingPageSize = true;

            _pageSize = ensureIntWithDefault(pageSize, DEFAULT_LIMIT);
            _page = ensureIntWithDefault(page, DEFAULT_PAGE);

            _limit = _pageSize;
            _offset = _limit * (_page - 1);
        } else {
            _pageSize = _limit; // not used, just for eslint `const` error
            _limit = ensureIntWithDefault(limit, DEFAULT_LIMIT);
            _offset = ensureIntWithDefault(offset, DEFAULT_OFFSET);
        }

        const tableName = this.constructor.prototype.tableName;
        const idAttribute = this.constructor.prototype.idAttribute ?
            this.constructor.prototype.idAttribute : 'id';

        const paginate = () => {
            // const pageQuery = clone(this.query());
            const pager = this.constructor.forge();

            return pager

                .query(qb => {
                    _assign(qb, this.query().clone());
                    qb.limit.apply(qb, [_limit]);
                    qb.offset.apply(qb, [_offset]);
                    return null;
                })

                .fetchAll(fetchOptions);
        };

        const count = () => {
            const notNeededQueries = [
                'orderByBasic',
                'orderByRaw',
                'groupByBasic',
                'groupByRaw'
            ];
            const counter = this.constructor.forge();

            return counter

                .query(qb => {
                    _assign(qb, this.query().clone());

                    // Remove grouping and ordering. Ordering is unnecessary
                    // for a count, and grouping returns the entire result set
                    // What we want instead is to use `DISTINCT`
                    _remove(qb._statements, statement => {
                        return (notNeededQueries.indexOf(statement.type) > -1) ||
                            statement.grouping === 'columns';
                    });
                    qb.countDistinct.apply(qb, [`${tableName}.${idAttribute}`]);
                })

                .fetchAll()

                .then(result => {
                    /*
                    const metadata = usingPageSize ?
                    {page: _page, pageSize: _limit} :
                    {offset: _offset, limit: _limit};
                    */
                    const metadata = {};
                    if (usingPageSize) {
                        metadata[_customMetaData.page] = _page;
                        metadata[_customMetaData.pageSize] = _pageSize;
                    } else {
                        metadata[_customMetaData.offset] = _offset;
                        metadata[_customMetaData.limit] = _limit;
                    }

                    if (result && result.length == 1) {
                        // We shouldn't have to do this, instead it should be
                        // result.models[0].get('count')
                        // but SQLite uses a really strange key name.
                        const count = result.models[0];
                        const keys = Object.keys(count.attributes);
                        if (keys.length === 1) {
                            const key = Object.keys(count.attributes)[0];
                            //metadata.total = parseInt(count.attributes[key]);
                            metadata[_customMetaData.rowCount] = parseInt(count.attributes[key]);
                        }
                    }

                    return metadata;
                });
        };

        return Promise.join(paginate(), count())
            .then(([rows, metadata]) => {
                const pages = Math.ceil(metadata[_customMetaData.rowCount] / _limit);
                let prePage = 1;
                if (_page > 1) {
                    prePage = _page - 1;
                }
                let nextPage = pages;
                if (_page < pages) {
                    nextPage = _page + 1;
                }
                metadata[_customMetaData.pageCount] = pages;
                metadata[_customMetaData.prePage] = prePage;
                metadata[_customMetaData.nextPage] = nextPage;
                //const pageData = _assign(metadata, {pages, prePage, nextPage});
                const pageData = metadata;
                return _assign(rows, {pagination: pageData});
            });
    }

    if (typeof bookshelf.Model.prototype.orderBy === 'undefined'){
        bookshelf.Model.prototype.orderBy = orderBy;
    }

    bookshelf.Model.prototype.fetchPage = fetchPage;

    bookshelf.Model.fetchPage = function (...args) {
        return this.forge().fetchPage(...args);
    }

    bookshelf.Collection.prototype.fetchPage = function (...args) {
        return fetchPage.apply(this.model.forge(), ...args);
    };

}

Use for Example:

const PAGINATION_METADATA = {
    rowCount: 'total',
    pageCount: 'pages',
    page: 'page',
    pageSize: 'pageSize',
    prePage: 'prePage',
    nextPage: 'nextPage'
};
tujiaoBookshelf.plugin('bookshelf-page', PAGINATION_METADATA);

OR in Query directly:

        let result = await Quiz.query({where: whereQuery})
            .orderBy(`-id`)
            .fetchPage({
                pageSize: pageSize,
                page: page,
                meta: {
                    rowCount: 'total',
                    pageCount: 'pages',
                    page: 'page',
                    pageSize: 'pageSize',
                    prePage: 'prePage',
                    nextPage: 'nextPage'
                }
            });
        let quizs = result.models;

Above code is Modified version. Now can custom pagination metadata by developer self.

anyong commented 8 years ago

I think the config option would work. Just need to think about how it should look. I'm thinking it should be an array of the key names you want.