mongoose-aggregate-paginate-v2
Version:
A page based custom aggregate pagination library for Mongoose with customizable labels.
252 lines (210 loc) • 6.25 kB
JavaScript
/**
* Mongoose Aggregate Paginate
* @param {Aggregate} aggregate
* @param {any} options
* @param {function} [callback]
* @returns {Promise}
*/
const { parseSort } = require("./util");
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,
countQuery: null,
useFacet: true,
};
const PREPAGINATION_PLACEHOLDER = "__PREPAGINATE__";
function aggregatePaginate(query, options, callback) {
options = {
...defaultOptions,
...aggregatePaginate.options,
...options,
};
const pipeline = Array.isArray(query) ? query : query._pipeline;
const customLabels = {
...defaultOptions.customLabels,
...options.customLabels,
};
const defaultLimit = 10;
// Custom Labels
const labelTotal = customLabels.totalDocs;
const labelLimit = customLabels.limit;
const labelPage = customLabels.page;
const labelTotalPages = customLabels.totalPages;
const labelDocs = customLabels.docs;
const labelNextPage = customLabels.nextPage;
const labelPrevPage = customLabels.prevPage;
const labelHasNextPage = customLabels.hasNextPage;
const labelHasPrevPage = customLabels.hasPrevPage;
const labelPagingCounter = customLabels.pagingCounter;
const labelMeta = customLabels.meta;
let page = parseInt(options.page || 1, 10) || 1;
let limit =
parseInt(options.limit, 10) > 0
? parseInt(options.limit, 10)
: defaultLimit;
// const skip = (page - 1) * limit;
let skip;
let offset;
if (Object.prototype.hasOwnProperty.call(options, "offset")) {
offset = Math.abs(parseInt(options.offset, 10));
skip = offset;
} else if (Object.prototype.hasOwnProperty.call(options, "page")) {
page = Math.abs(parseInt(options.page, 10)) || 1;
skip = (page - 1) * limit;
} else {
offset = 0;
page = 1;
skip = offset;
}
const sort = options.sort;
const allowDiskUse = options.allowDiskUse || false;
const isPaginationEnabled = options.pagination === false ? false : true;
let q = this.aggregate();
if (query.options) q.options = query.options;
if (allowDiskUse) {
q.allowDiskUse(true);
}
if (sort) {
pipeline.push({ $sort: parseSort(sort) });
}
let promise;
if (options.useFacet && !options.countQuery) {
const prepaginationIndex = pipeline.findIndex(
(stage) => stage === PREPAGINATION_PLACEHOLDER
);
if (prepaginationIndex !== -1) {
promise = q
.append(pipeline.slice(0, prepaginationIndex))
.facet({
docs: [
...(isPaginationEnabled
? [{ $skip: skip }, { $limit: limit }]
: []),
...pipeline.slice(prepaginationIndex + 1),
],
count: [{ $count: "count" }],
})
.then(([{ docs, count }]) => [docs, count]);
} else {
promise = q
.append(pipeline)
.facet({
docs: isPaginationEnabled ? [{ $skip: skip }, { $limit: limit }] : [],
count: [{ $count: "count" }],
})
.then(([{ docs, count }]) => [docs, count]);
}
} else {
let cleanedPipeline = pipeline.filter(
(stage) => stage !== PREPAGINATION_PLACEHOLDER
);
const countPipeline = [...cleanedPipeline, { $count: "count" }];
if (isPaginationEnabled) {
let foundPrepagination = false;
cleanedPipeline = pipeline.flatMap((stage) => {
if (stage === PREPAGINATION_PLACEHOLDER) {
foundPrepagination = true;
return [{ $skip: skip }, { $limit: limit }];
}
return stage;
});
if (!foundPrepagination) {
cleanedPipeline.push({ $skip: skip }, { $limit: limit });
}
}
let countQuery = options.countQuery
? options.countQuery
: this.aggregate(countPipeline);
if (options.countQuery) {
countQuery = countQuery.group({
_id: null,
count: {
$sum: 1,
},
});
}
if (allowDiskUse) {
countQuery.allowDiskUse(true);
}
const q = this.aggregate(cleanedPipeline);
if (query.options) q.options = query.options;
promise = Promise.all([q.exec(), countQuery.exec()]);
}
return promise
.then(function (values) {
var count = values[1][0] ? values[1][0].count : 0;
if (isPaginationEnabled === false) {
limit = count;
page = 1;
}
const pages = Math.ceil(count / limit) || 1;
var result = {
[labelDocs]: values[0],
};
var meta = {
[labelTotal]: count,
[labelLimit]: limit,
[labelPage]: page,
[labelTotalPages]: pages,
[labelPagingCounter]: (page - 1) * limit + 1,
[labelHasPrevPage]: false,
[labelHasNextPage]: false,
};
if (typeof offset !== "undefined") {
page = Math.ceil((offset + 1) / limit);
meta.offset = offset;
meta[labelPage] = Math.ceil((offset + 1) / limit);
meta[labelPagingCounter] = offset + 1;
}
// Set prev page
if (page > 1) {
meta[labelHasPrevPage] = true;
meta[labelPrevPage] = page - 1;
} else {
meta[labelPrevPage] = null;
}
// Set next page
if (page < pages) {
meta[labelHasNextPage] = true;
meta[labelNextPage] = page + 1;
} else {
meta[labelNextPage] = null;
}
if (labelMeta) {
result[labelMeta] = meta;
} else {
result = Object.assign(result, meta);
}
if (typeof callback === "function") {
return callback(null, result);
}
return Promise.resolve(result);
})
.catch(function (reject) {
if (typeof callback === "function") {
return callback(reject);
}
return Promise.reject(reject);
});
}
module.exports = aggregatePaginate;
module.exports.PREPAGINATION_PLACEHOLDER = PREPAGINATION_PLACEHOLDER;