UNPKG

@conpago/mongo-cursor-pagination

Version:

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

154 lines (137 loc) 5.46 kB
import { Collection, ObjectId } from "mongodb"; import { AggregateInputParams, AggregateParams, QueryInputParams, QueryInputParamsMulti, QueryParams, } from "../types"; import config from "../config"; import { decode } from "./bsonUrlEncoding"; import getPropertyViaDotNotation from "./getPropertyViaDotNotation"; export default async ( collection: Collection | any, params: QueryInputParams | AggregateInputParams ): Promise<QueryParams | AggregateParams> => { // // set the params.paginatedField params.paginatedField ??= "_id"; // set the params.limit params.limit = (() => { const requestedLimit = params.limit; if (requestedLimit < 1) return 1; if (requestedLimit > config.MAX_LIMIT) return config.MAX_LIMIT; return requestedLimit || config.DEFAULT_LIMIT; })(); // set params.previous || params.next if (params.previous) params.previous = decode(params.previous as string); if (params.next) params.next = decode(params.next as string); // if after || before in params, overwrite an existing previous || next value if (params.after || params.before) await applyAfterOrBeforeToParams({ collection, params }); // set the params.fields, which are the requested projected fields PLUS the paginated field // (the latter required for sorting and constucting the cursor) if (params.fields) { const { fields: requestedFields, paginatedField } = params; params.fields = { _id: 0, // mongo projects _id by default, so ensure only projecting if user requests ...requestedFields, [paginatedField]: 1, }; } return params; }; export async function sanitizeMultiParamsMutate( collection: Collection | any, params: QueryInputParamsMulti ) { // add default _id paginate sort if (!params.paginatedFields?.length) { params.paginatedFields = []; params.paginatedFields.push({ paginatedField: "_id", sortAscending: false, sortCaseInsensitive: false, }); // add _id to the end of the fields } else if (!params.paginatedFields.find(f => f.paginatedField === "_id")) { const lastField = params?.paginatedFields[params.paginatedFields?.length - 1]; params.paginatedFields.push({ paginatedField: "_id", sortAscending: lastField?.sortAscending, sortCaseInsensitive: lastField?.sortCaseInsensitive, }); } // set the params.limit params.limit = (() => { const requestedLimit = params.limit; if (requestedLimit < 1) return 1; if (requestedLimit > config.MAX_LIMIT) return config.MAX_LIMIT; return requestedLimit || config.DEFAULT_LIMIT; })(); // set params.previous || params.next if (params.previous) params.previous = decode(params.previous as string); if (params.next) params.next = decode(params.next as string); // if after || before in params, overwrite an existing previous || next value if (params.after || params.before) await applyAfterOrBeforeToParams({ collection, params }); if (params.fields) { const { fields: requestedFields, paginatedFields } = params; params.fields = Object.assign( { _id: 0, }, requestedFields, ...paginatedFields.map(pf => ({ [pf.paginatedField]: 1 })) ); } return params; } /** * @description * The 'after' param sets the start position for the next page. This is similar to the * 'next' param, with the difference that 'after' takes a plain _id instead of an encoded * string of both _id and paginatedField values. * > a valid params.after will override params.next value * * The 'before' param sets the start position for the previous page. This is similar to the * 'previous' param, with the difference that 'before' takes a plain _id instead of an encoded * string of both _id and paginatedField values. * > a valid params.before will override params.previous value * * @returns undefined (but may update the given params.next || params.previous with a decoded cursor) */ async function applyAfterOrBeforeToParams({ collection, params, }: { collection: any; params: QueryInputParams; }) { const { after, before, sortCaseInsensitive, paginatedField } = params; if ((after && before) || (!after && !before)) return; // if the primary sort field is the _id, then the results are assured to have a unique // value, and after || before can immediately overwrite the next || previous param if (paginatedField === "_id") { after ? (params.next = after) : (params.previous = before); return; } // otherwise an alternative primary field may have duplicates affecting $gt | $lt sorts, so // will need to be secondarily sorted by _id. As 'after' && 'before' cursors ONLY hold an _id // value, the primary sort value needs to be established to create a valid next | previous cursor const document = await collection.findOne( { _id: new ObjectId(after || before) }, { [paginatedField]: true, _id: false } ); if (!document) return; // retrieve paginated field value (field can be single or dot-notation; such as "user.first_name") const paginatedFieldValue = getPropertyViaDotNotation({ propertyName: paginatedField, object: document, sortCaseInsensitive, }); // decoded next | previous cursor expection is [ <value of paginated field in document >, < _id of document >] if (after) params.next = [paginatedFieldValue, after]; if (before) params.previous = [paginatedFieldValue, before]; }