Skip to content

Instantly share code, notes, and snippets.

@imekachi
Created September 18, 2019 02:43
Show Gist options
  • Save imekachi/c8903519449084dc9f170fac60564f92 to your computer and use it in GitHub Desktop.
Save imekachi/c8903519449084dc9f170fac60564f92 to your computer and use it in GitHub Desktop.
based on mongoose-paginate npm package, fixed deprecaion warning, add aggregateMode support
import _ from 'lodash'
// This is based on a PR which fixed collection.count deprecation warning
// https://github.com/edwardhotchkiss/mongoose-paginate/blob/e807d87b02aca2198f4fb2e5e896e6aad4b77f55/index.js
// but only god knows when the PR will get merged, so here we are, create our own fork.
// Now with aggregateMode support!!.
/**
* mongoose-paginate
* @param {Object|Array} [query={}] - this must be array when using aggregate mode (options.aggregateMode = true)
* @param {Object} [options={}]
* @param {Object|String} [options.select]
* @param {Object|String} [options.sort]
* @param {Array|Object|String} [options.populate]
* @param {Boolean} [options.lean=true]
* @param {Number} [options.offset=0] - Use offset or page to set skip position
* @param {Number} [options.page=1]
* @param {Number} [options.limit=10]
* @returns {Promise}
*/
async function paginate(query = {}, options = {}) {
const HARD_LIMIT = 100
const defaultOptions = {
aggregateMode: false,
lean: true,
limit: 10,
}
// eslint-disable-next-line no-param-reassign
options = _.merge({}, defaultOptions, paginate.options, options)
// destructuring some variables that can be used directly
const { select, sort, populate, lean, aggregateMode } = options
// limit won't go pass HARD_LIMIT
const limit = Math.min(options.limit, HARD_LIMIT)
let page
let offset
let skip
// if using option offset
if (options.offset) {
skip = Math.max(options.offset, 0) // skip won't go below 0
offset = skip
}
// if using options page
else if (options.page) {
page = Math.max(options.page, 1) // page won't go below 1
skip = (page - 1) * limit
}
// default case
else {
page = 1
skip = 0
offset = skip
}
/**
* Create query
*/
let docsQuery
if (aggregateMode) {
// The documents returned are plain javascript objects,
// not mongoose documents (since any shape of document can be returned).
// source: https://mongoosejs.com/docs/api.html#model_Model.aggregate
// So, there is no need.
docsQuery = this.aggregate(query)
// Sort has to be done like this.
// - if sort is undefined, mongoose will break internally with error "constructor of undefined"
// - if sort is an empty object, mongoose will throw that "$sort stage must have at least one sort key"
if (sort && !_.isEmpty(sort)) {
docsQuery = docsQuery.sort(sort)
}
docsQuery = docsQuery.skip(skip).limit(limit)
} else {
docsQuery = this.find(query)
.select(select)
.sort(sort)
.skip(skip)
.limit(limit)
.lean(lean)
}
if (populate && !aggregateMode) {
if (_.isArray(populate)) {
populate.forEach(item => docsQuery.populate(item))
} else {
docsQuery.populate(populate)
}
}
/**
* Count documents
*/
let countDocumentsQuery
if (aggregateMode) {
// using group to count documents from aggregate result
// source1: https://stackoverflow.com/a/20348364/7789516
// source2: https://github.com/Maheshkumar-Kakade/mongoose-aggregate-paginate/blob/master/lib/mongoose-aggregate-paginate.js#L28
countDocumentsQuery = this.aggregate(query).group({ _id: null, count: { $sum: 1 } })
} else {
countDocumentsQuery = this.countDocuments(query)
}
/**
* Execute the query and count documents simultaneously
*/
const [docs, itemCount] = await Promise.all([
docsQuery.exec(),
// you have to use "countDocuments" here to count the correct number of documents that matches filters
// if you use "estimatedDocumentCount", it will count all documents in the collection and doesn't respect the filter
countDocumentsQuery.exec(),
])
/**
* Prepare result object
*/
const result = { docs, total: itemCount, limit }
// In aggregate mode, itemCount is not a number but an array [{ _id: null, count: number }]
if (aggregateMode) {
result.total = _.get(itemCount, '0.count', 0)
}
// adding some properties
if (offset !== undefined) {
result.offset = offset
}
if (page !== undefined) {
result.page = page
result.pages = Math.ceil(result.total / limit) || 1
}
return result
}
/**
* @param {Schema} schema
*/
function pluginPaginate(schema) {
// eslint-disable-next-line no-param-reassign
schema.statics.paginate = paginate
}
export default pluginPaginate
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment