mongoose-paginate-v2
Version:
A custom pagination library for Mongoose with customizable labels.
394 lines (336 loc) • 11.4 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 {Boolean} [options.leanWithVirtuals=false]
* @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(query: Object=): Promise<number>)} [options.useCustomCountFn=false] - Use custom function for count datasets. Receives `query` as an optional argument.
* @param {Object} [options.read={}] - Determines the MongoDB nodes from which to read.
* @param {Function} [callback]
*
* @returns {Promise}
*/
const PaginationParametersHelper = require('./pagination-parameters');
const paginateSubDocsHelper = require('./pagination-subdocs');
const paginateQueryHelper = require('./pagination-queryHelper');
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,
leanWithVirtuals: false,
limit: 10,
projection: {},
select: '',
options: {},
pagination: true,
useEstimatedCount: false,
useCustomCountFn: false,
forceCountFn: false,
allowDiskUse: false,
customFind: 'find',
};
function hasOwn(obj, key) {
return Object.prototype.hasOwnProperty.call(obj || {}, key);
}
function isSessionInTransaction(session) {
if (session == null) {
return false;
}
if (typeof session.inTransaction === 'function') {
return session.inTransaction();
}
// Older driver sessions may expose the driver session via `session.session`.
const driverSession = session.session;
if (driverSession && typeof driverSession.inTransaction === 'function') {
return driverSession.inTransaction();
}
// Pre-inTransaction drivers: conservatively serialize because we cannot
// determine whether the session is transactional. These drivers are too old
// to support causal sessions reliably anyway.
return true;
}
function hasActiveSession(model, queryOptions) {
if (hasOwn(queryOptions, 'session')) {
return isSessionInTransaction(queryOptions.session);
}
const base = model && model.db && model.db.base;
const als = base && base.transactionAsyncLocalStorage;
if (!als || typeof als.getStore !== 'function') {
return false;
}
const store = als.getStore();
return isSessionInTransaction(store && store.session);
}
function paginate(query, options, callback) {
options = {
...defaultOptions,
...paginate.options,
...options,
};
query = query || {};
const {
collation,
lean,
leanWithId,
leanWithVirtuals,
populate,
projection,
read,
select,
sort,
pagination,
useEstimatedCount,
useCustomCountFn,
forceCountFn,
allowDiskUse,
customFind,
} = options;
const customLabels = {
...defaultOptions.customLabels,
...options.customLabels,
};
let limit = defaultOptions.limit;
if (pagination && !isNaN(Number(options.limit))) {
limit = parseInt(options.limit, 10) > 0 ? parseInt(options.limit, 10) : 0;
}
const isCallbackSpecified = typeof callback === 'function';
const findOptions = options.options;
// Create queryOptions with collation included to preserve session context in transactions
const queryOptions =
Object.keys(collation).length > 0
? { ...findOptions, collation }
: findOptions;
// For countDocuments, exclude limit/skip which MongoDB applies to the count result
// eslint-disable-next-line no-unused-vars
const { limit: _l, skip: _s, ...countOptions } = queryOptions || {};
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) > 0 ? parseInt(options.page, 10) : 1;
skip = (page - 1) * limit;
} else {
offset = 0;
page = 1;
skip = offset;
}
if (!pagination) {
page = 1;
}
let countPromise;
// Only run count when pagination is enabled
if (pagination) {
if (forceCountFn === true) {
// Deprecated since starting from MongoDB Node.JS driver v3.1
// Используем estimatedDocumentCount, если query пустой, иначе countDocuments
if (!query || Object.keys(query).length === 0) {
countPromise = this.estimatedDocumentCount().exec();
} else {
countPromise = this.countDocuments(query, countOptions).exec();
}
} else {
if (useEstimatedCount === true) {
countPromise = this.estimatedDocumentCount().exec();
} else if (typeof useCustomCountFn === 'function') {
countPromise = useCustomCountFn(query);
} else {
// Используем estimatedDocumentCount, если query пустой, иначе countDocuments
if (!query || Object.keys(query).length === 0) {
countPromise = this.estimatedDocumentCount().exec();
} else {
countPromise = this.countDocuments(query, countOptions).exec();
}
}
}
}
const model = this;
function buildDocsPromise() {
if (!limit) {
return Promise.resolve([]);
}
// Use queryOptions (which includes collation) to preserve session context in transactions
const mQuery = model[customFind](query, projection, queryOptions);
if (populate) {
mQuery.populate(populate);
}
mQuery.select(select);
mQuery.sort(sort);
if (lean) {
// use whit mongoose-lean-virtuals
if (leanWithVirtuals) {
mQuery.lean({ virtuals: leanWithVirtuals });
} else {
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);
}
if (pagination) {
mQuery.skip(skip);
mQuery.limit(limit);
}
try {
if (allowDiskUse === true) {
mQuery.allowDiskUse();
}
} catch (ex) {
console.error('Your MongoDB version does not support `allowDiskUse`.');
}
let execPromise = mQuery.exec();
if (lean && leanWithId) {
execPromise = execPromise.then((docs) => {
docs.forEach((doc) => {
if (doc._id) {
doc.id = String(doc._id);
}
});
return docs;
});
}
return execPromise;
}
function buildResult(count, docs) {
if (pagination !== true) {
count = docs.length;
}
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
if (labelLimit) meta[labelLimit] = count;
if (labelTotalPages) meta[labelTotalPages] = 1;
if (labelPage) meta[labelPage] = page;
if (labelPagingCounter) meta[labelPagingCounter] = (page - 1) * limit + 1;
if (labelHasPrevPage) meta[labelHasPrevPage] = false;
if (labelHasNextPage) meta[labelHasNextPage] = false;
if (labelPrevPage) meta[labelPrevPage] = null;
if (labelNextPage) meta[labelNextPage] = null;
if (pagination) {
if (labelLimit) meta[labelLimit] = limit;
if (labelTotalPages) meta[labelTotalPages] = pages;
// Set prev page
if (page > 1) {
if (labelHasPrevPage) meta[labelHasPrevPage] = true;
if (labelPrevPage) meta[labelPrevPage] = page - 1;
} else if (page == 1 && typeof offset !== 'undefined' && offset !== 0) {
if (labelHasPrevPage) meta[labelHasPrevPage] = true;
if (labelPrevPage) meta[labelPrevPage] = 1;
}
// Set next page
if (page < pages) {
if (labelHasNextPage) meta[labelHasNextPage] = true;
if (labelNextPage) meta[labelNextPage] = page + 1;
}
}
// Remove customLabels set to false
delete meta['false'];
if (limit == 0) {
if (labelLimit) meta[labelLimit] = 0;
if (labelTotalPages) meta[labelTotalPages] = 1;
if (labelPage) meta[labelPage] = 1;
if (labelPagingCounter) meta[labelPagingCounter] = 1;
if (labelPrevPage) meta[labelPrevPage] = null;
if (labelNextPage) meta[labelNextPage] = null;
if (labelHasPrevPage) meta[labelHasPrevPage] = false;
if (labelHasNextPage) meta[labelHasNextPage] = false;
}
if (labelMeta) {
result = {
[labelDocs]: docs,
[labelMeta]: meta,
};
} else {
result = {
[labelDocs]: docs,
...meta,
};
}
return result;
}
const hasSession = hasActiveSession(model, queryOptions);
let resultPromise;
if (hasSession) {
resultPromise = Promise.resolve(countPromise).then((count) => {
return buildDocsPromise().then((docs) => {
return buildResult(count, docs);
});
});
} else {
docsPromise = buildDocsPromise();
resultPromise = Promise.all([countPromise, docsPromise]).then((values) => {
return buildResult(values[0], values[1]);
});
}
return resultPromise
.then((result) => {
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;
schema.statics.paginateSubDocs = paginateSubDocsHelper;
schema.query.paginate = paginateQueryHelper;
};
module.exports.PaginationParameters = PaginationParametersHelper;
module.exports.paginateSubDocs = paginateSubDocsHelper;
module.exports.paginate = paginate;