@skybackend/mongoose-paginate-v2
Version:
A cursor based custom pagination library for Mongoose with customizable labels.
288 lines (248 loc) • 7.68 kB
JavaScript
/**
* @param {Object} [query={}]
* @param {Object} [options={}]
* @param {Object|String} [options.select='']
* @param {Object|String} [options.projection={}]
* @param {Object} [options.options={}]
* @param {Object|String} [options.sort]
* @param {Object|String} [options.customLabels]
* @param {Object} [options.collation]
* @param {Array|Object|String} [options.populate]
* @param {Boolean} [options.lean=false]
* @param {Boolean} [options.leanWithId=true]
* @param {Number} [options.offset=0] - Use offset or page to set skip position
* @param {Number} [options.page=1]
* @param {Number} [options.limit=10]
* @param {Boolean} [options.useEstimatedCount=true] - Enable estimatedDocumentCount for larger datasets. As the name says, the count may not abe accurate.
* @param {Function} [options.useCustomCountFn=false] - use custom function for count datasets.
* @param {Object} [options.read={}] - Determines the MongoDB nodes from which to read.
* @param {Function} [callback]
*
* @returns {Promise}
*/
const { Mongoose } = require('mongoose');
const defaultOptions = {
customLabels: {
totalDocs: 'totalDocs',
limit: 'limit',
page: 'page',
totalPages: 'totalPages',
docs: 'docs',
nextPage: 'nextPage',
prevPage: 'prevPage',
pagingCounter: 'pagingCounter',
hasPrevPage: 'hasPrevPage',
hasNextPage: 'hasNextPage',
meta: null,
},
collation: {},
lean: false,
leanWithId: true,
limit: 10,
projection: {},
select: '',
options: {},
pagination: true,
useEstimatedCount: false,
useCustomCountFn: false,
forceCountFn: false,
};
function paginate(query, options, callback) {
options = {
...defaultOptions,
...paginate.options,
...options,
};
query = query || {};
const {
collation,
lean,
leanWithId,
populate,
projection,
read,
select,
sort,
pagination,
useEstimatedCount,
useCustomCountFn,
forceCountFn,
} = options;
const customLabels = {
...defaultOptions.customLabels,
...options.customLabels,
};
let limit = defaultOptions.limit;
if (pagination) {
limit = parseInt(options.limit, 10) > 0 ? parseInt(options.limit, 10) : 0;
}
const isCallbackSpecified = typeof callback === 'function';
const findOptions = options.options;
let offset;
let page;
let skip;
let docsPromise = [];
// Labels
const labelDocs = customLabels.docs;
const labelLimit = customLabels.limit;
const labelNextPage = customLabels.nextPage;
const labelPage = customLabels.page;
const labelPagingCounter = customLabels.pagingCounter;
const labelPrevPage = customLabels.prevPage;
const labelTotal = customLabels.totalDocs;
const labelTotalPages = customLabels.totalPages;
const labelHasPrevPage = customLabels.hasPrevPage;
const labelHasNextPage = customLabels.hasNextPage;
const labelMeta = customLabels.meta;
if (Object.prototype.hasOwnProperty.call(options, 'offset')) {
offset = parseInt(options.offset, 10);
skip = offset;
} else if (Object.prototype.hasOwnProperty.call(options, 'page')) {
page = parseInt(options.page, 10) < 1 ? 1 : parseInt(options.page, 10);
skip = (page - 1) * limit;
} else {
offset = 0;
page = 1;
skip = offset;
}
if (!pagination) {
page = 1;
}
let countPromise;
if (forceCountFn === true) {
// Deprecated since starting from MongoDB Node.JS driver v3.1
// Hack for mongo < v3.4
if (Object.keys(collation).length > 0) {
countPromise = this.count(query).collation(collation).exec();
} else {
countPromise = this.count(query).exec();
}
} else {
if (useEstimatedCount === true) {
countPromise = this.estimatedDocumentCount().exec();
} else if (typeof useCustomCountFn === 'function') {
countPromise = useCustomCountFn();
} else {
// Hack for mongo < v3.4
if (Object.keys(collation).length > 0) {
countPromise = this.countDocuments(query).collation(collation).exec();
} else {
countPromise = this.countDocuments(query).exec();
}
}
}
if (limit) {
const mQuery = this.find(query, projection, findOptions);
if (populate) {
mQuery.populate(populate);
}
mQuery.select(select);
mQuery.sort(sort);
mQuery.lean(lean);
if (read && read.pref) {
/**
* Determines the MongoDB nodes from which to read.
* @param read.pref one of the listed preference options or aliases
* @param read.tags optional tags for this query
*/
mQuery.read(read.pref, read.tags);
}
// Hack for mongo < v3.4
if (Object.keys(collation).length > 0) {
mQuery.collation(collation);
}
if (pagination) {
mQuery.skip(skip);
mQuery.limit(limit);
}
docsPromise = mQuery.exec();
if (lean && leanWithId) {
docsPromise = docsPromise.then((docs) => {
docs.forEach((doc) => {
if (doc._id) {
doc.id = String(doc._id);
}
});
return docs;
});
}
}
return Promise.all([countPromise, docsPromise])
.then((values) => {
const [count, docs] = values;
const meta = {
[labelTotal]: count,
};
let result = {};
if (typeof offset !== 'undefined') {
meta.offset = offset;
page = Math.ceil((offset + 1) / limit);
}
const pages = limit > 0 ? Math.ceil(count / limit) || 1 : null;
// Setting default values
meta[labelLimit] = count;
meta[labelTotalPages] = 1;
meta[labelPage] = page;
meta[labelPagingCounter] = (page - 1) * limit + 1;
meta[labelHasPrevPage] = false;
meta[labelHasNextPage] = false;
meta[labelPrevPage] = null;
meta[labelNextPage] = null;
if (pagination) {
meta[labelLimit] = limit;
meta[labelTotalPages] = pages;
// Set prev page
if (page > 1) {
meta[labelHasPrevPage] = true;
meta[labelPrevPage] = page - 1;
}
// Set next page
if (page < pages) {
meta[labelHasNextPage] = true;
meta[labelNextPage] = page + 1;
}
}
// Remove customLabels set to false
delete meta['false'];
if (limit == 0) {
meta[labelLimit] = 0;
meta[labelTotalPages] = 1;
meta[labelPage] = 1;
meta[labelPagingCounter] = 1;
meta[labelPrevPage] = null;
meta[labelNextPage] = null;
meta[labelHasPrevPage] = false;
meta[labelHasNextPage] = false;
}
if (labelMeta) {
result = {
[labelDocs]: docs,
[labelMeta]: meta,
};
} else {
result = {
[labelDocs]: docs,
...meta,
};
}
return isCallbackSpecified
? callback(null, result)
: Promise.resolve(result);
})
.catch((error) => {
return isCallbackSpecified ? callback(error) : Promise.reject(error);
});
}
/**
* @param {Schema} schema
*/
module.exports = (schema) => {
schema.statics.paginate = paginate;
};
module.exports.paginate = paginate;
module.exports.PaginateOptions = Mongoose.PaginateOptions;
module.exports.PaginateResult = Mongoose.PaginateResult;
module.exports.CustomLabels = Mongoose.CustomLabels;
module.exports.PaginateModel = Mongoose.PaginateModel;
module.exports.CollationOptions = Mongoose.CollationOptions;
module.QueryFindOptions = Mongoose.QueryFindOptions;