UNPKG

@conpago/mongo-cursor-pagination

Version:

Make it easy to return cursor-paginated results from a Mongo collection

134 lines (120 loc) 5.84 kB
import { defaults } from "underscore"; import { Collection } from "mongodb"; import aggregateMulti from "./aggregateMulti"; import config from "./config"; import { PaginationResponse, QueryParamsMulti } from "./types"; import { filterProjectedFields, generateCursorQueryMulti, generateSorts, prepareResponse, } from "./utils/query"; import { sanitizeMultiParamsMutate } from "./utils/sanitizeParams"; /** * Performs a find() query on a passed-in Mongo collection, using criteria you specify. The results * are ordered by the paginatedField. * * @param {Collection} collection A collection object returned from the MongoDB library's. * @param {QueryParamsMulti} params * @param {object} params.query The find query. * @param {Number} params.limit The page size. Must be between 1 and `config.MAX_LIMIT`. * @param {object} params.fields Fields to query in the Mongo object format, e.g. {_id: 1, timestamp :1}. * The default is to query all fields. * @param {String} params.paginatedField The field name to query the range for. The field must be: * 1. Orderable. We must sort by this value. If duplicate values for paginatedField field * exist, the results will be secondarily ordered by the _id. * 2. Indexed. For large collections, this should be indexed for query performance. * 3. Immutable. If the value changes between paged queries, it could appear twice. 4. Consistent. All values (except undefined and null values) must be of the same type. * The default is to use the Mongo built-in '_id' field, which satisfies the above criteria. * The only reason to NOT use the Mongo _id field is if you chose to implement your own ids. * @param {boolean} params.sortAscending Whether to sort in ascending order by the `paginatedField`. * @param {boolean} params.sortCaseInsensitive Whether to ignore case when sorting, in which case `paginatedField` * @param {boolean} params.getTotal Whether to fetch the total count for the query results * must be a string property. * @param {String} params.next The value to start querying the page. * @param {String} params.previous The value to start querying previous page. * @param {String} params.after The _id to start querying the page. * @param {String} params.before The _id to start querying previous page. * @param {String} params.hint An optional index hint to provide to the mongo query * @param {object} params.collation An optional collation to provide to the mongo query. E.g. { locale: 'en', strength: 2 }. When null, disables the global collation. */ export default async ( collection: Collection | any, params: QueryParamsMulti ): Promise<PaginationResponse> => { const projectedFields = params.fields; let response = {} as PaginationResponse; const isCaseInsensitive = params.paginatedFields?.some( pf => pf.sortCaseInsensitive ); // For case-insensitive sorting, we need to work with an aggregation: if (isCaseInsensitive || params.aggregationSearch) { const query = (() => { if (params.aggregationSearch) { return Array.isArray(params.query) ? params.query : []; } return params.query ? [{ $match: params.query }] : []; })(); response = await aggregateMulti( collection, Object.assign({}, params, { aggregation: query, }) ); if (params.getTotal) { if (params.aggregationSearch) { // NOTE: this mimics the behavior of countDocuments, but we can pass our aggregation first // https://www.mongodb.com/docs/manual/reference/method/db.collection.countDocuments/ const result = await (collection as Collection) .aggregate([...query, { $group: { _id: null, n: { $sum: 1 } } }]) .toArray(); response.totalCount = result?.[0]?.n ?? 0; } else { response.totalCount = await collection.countDocuments(params.query); } } } else { // Need to repeat `params.paginatedField` default value ('_id') since it's set in 'sanitizeParams()' params = defaults(await sanitizeMultiParamsMutate(collection, params), { query: {}, }); const cursorQuerys = generateCursorQueryMulti(params); const $sort = generateSorts(params); // Support both the native 'mongodb' driver and 'mongoist'. See: // https://www.npmjs.com/package/mongoist#cursor-operations const findMethod = collection.findAsCursor ? "findAsCursor" : "find"; const query = collection[findMethod]()?.project ? collection .find({ $and: [cursorQuerys, params.query] }) .project(params.fields) : collection[findMethod]( { $and: [cursorQuerys, params.query] }, params.fields ); /** * IMPORTANT * * If using collation, check the README: * https://github.com/mixmaxhq/mongo-cursor-pagination#important-note-regarding-collation */ const isCollationNull = params.collation === null; const collation = params.collation || config.COLLATION; const collatedQuery = collation && !isCollationNull ? query.collation(collation) : query; // Query one more element to see if there's another page. const cursor = collatedQuery.sort($sort).limit(params.limit + 1); if (params.hint) cursor.hint(params.hint); const results = await cursor.toArray(); response = prepareResponse(results, params, true); if (params.getTotal) response.totalCount = await collection.countDocuments(params.query); } // Remove fields that we added to the query (such as paginatedField and _id) that the user didn't ask for. const projectedResults = filterProjectedFields({ projectedFields, results: response.results, sortCaseInsensitive: isCaseInsensitive, }); return { ...response, results: projectedResults }; };