UNPKG

mongo-cursor-pagination

Version:

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

105 lines (92 loc) 2.95 kB
import { Collection, Document } from 'mongodb'; import _ from 'underscore'; import config from './config'; import bsonUrlEncoding, { Encodable } from './utils/bsonUrlEncoding'; export interface SearchParams { query?: Record<string, any>; limit?: number; fields?: Record<string, number>; next?: Encodable; } export interface SearchResponse<T = Document> { results: T[]; next?: string; previous?: string; } /** * Performs a search query on a Mongo collection and pages the results. This is different from * find() in that the results are ordered by their relevancy, and as such, it does not take * a paginatedField parameter. Note that this is less performant than find() because it must * perform the full search on each call to this function. * @param collection - A MongoDB collection object. This MUST have a Mongo $text index. * @param searchString - The string to search for. * @param params - Search parameters: * - `query`: Additional query filters. * - `limit`: Number of results per page (1 to `config.MAX_LIMIT`). * - `fields`: Fields to include in the results. * - `next`: Cursor to continue pagination. * @returns A paginated response containing results and a cursor for the next page. */ export default async function search<T = Document>( collection: Collection<T>, searchString: string, params: SearchParams ): Promise<SearchResponse<T>> { if (_.isString(params.limit)) params.limit = parseInt((params.limit as any) as string, 10); if (params.next) { params.next = bsonUrlEncoding.decode(params.next as string); } params = _.defaults(params, { query: {}, limit: config.MAX_LIMIT, }); if (params.limit! < 1) params.limit = 1; if (params.limit! > config.MAX_LIMIT) params.limit = config.MAX_LIMIT; const aggregatePipeline: Document[] = [ { $match: { ...params.query, $text: { $search: searchString }, }, }, { $project: { ...params.fields, _id: 1, score: { $meta: 'textScore' }, }, }, { $sort: { score: { $meta: 'textScore' }, _id: -1, }, }, ]; if (params.next) { aggregatePipeline.push({ $match: { $or: [ { score: { $lt: params.next[0] } }, { score: { $eq: params.next[0] }, _id: { $lt: params.next[1] } }, ], }, }); } aggregatePipeline.push({ $limit: params.limit!, }); const aggregateMethod = 'aggregateAsCursor' in collection ? 'aggregateAsCursor' : 'aggregate'; const results = await collection[aggregateMethod](aggregatePipeline).toArray(); const fullPageOfResults = results.length === params.limit; const response: SearchResponse<T> = { results, }; if (fullPageOfResults) { const lastResult = _.last(results); if (lastResult) { response.next = bsonUrlEncoding.encode([lastResult.score, lastResult._id]); } } return response; }