UNPKG

@mother/mongoose-cursor-pagination

Version:

Easy-to-use, scalable, cursor pagination plugin for mongoose

118 lines (101 loc) 3.97 kB
const base64url = require('base64-url') const mongoose = require('mongoose') // We generate a base64-encoded JSON string exports.generateCursorStr = (cursorObj) => { const simplifiedCursorObj = Object.keys(cursorObj).reduce((result, cursorKey) => { const val = cursorObj[cursorKey] // This could be made more modular in the future if there // is a need to serialize other types. if (typeof val === 'object' && mongoose.Types.ObjectId.isValid(val)) { // Although this will be rehydrated as a string later on, mongoose // will automatically convert strings to ids when querying. result[cursorKey] = val.toString() } else if (val instanceof Date) { result[cursorKey] = val.getTime() } else if (typeof val !== 'object' || val === null) { result[cursorKey] = val } return result }, {}) return base64url.encode(JSON.stringify(simplifiedCursorObj)) } exports.parseCursorStr = (cursorStr, schema) => { const cursorPlainText = base64url.decode(cursorStr) const cursorObj = JSON.parse(cursorPlainText) Object.keys(cursorObj).forEach((cursorKey) => { // This could be made more modular in the future if there // is a need to hydrate other types. const schemaPath = schema.path(cursorKey) if (typeof schemaPath === 'object' && schemaPath.instance === 'ObjectID') { cursorObj[cursorKey] = new mongoose.Types.ObjectId(cursorObj[cursorKey]) } else if (typeof schemaPath === 'object' && schemaPath.instance === 'Date') { cursorObj[cursorKey] = new Date(cursorObj[cursorKey]) } }) return cursorObj } exports.transformCursorIntoConditions = ({ cursorObj = {}, sortObj = {} }) => { const cursorKeys = Object.keys(cursorObj) const sortKeys = Object.keys(sortObj) if (!cursorKeys.every(key => sortKeys.includes(key))) { throw new Error('Cursor keys must be a subset of sort keys') } // Create a new array that will contain our query conditions // for cursor offsets, then initialize the array // with empty objects. const cursorConditions = new Array(cursorKeys.length) for (let i = 0; i < cursorKeys.length; i += 1) { cursorConditions[i] = {} } for (let i = 0; i < cursorKeys.length; i += 1) { const comparisonOperator = sortObj[cursorKeys[i]] === -1 ? '$lt' : '$gt' cursorConditions[i][cursorKeys[i]] = { [comparisonOperator]: cursorObj[cursorKeys[i]] } for (let j = i + 1; j < cursorKeys.length; j += 1) { cursorConditions[j][cursorKeys[i]] = cursorObj[cursorKeys[i]] } } return cursorConditions } exports.applyConditionsToQuery = (cursorConditions = [], query) => { if (cursorConditions.length === 1) { query.where(cursorConditions[0]) } else if (cursorConditions.length > 1) { const queryConditions = query.getQuery() if (Array.isArray(queryConditions.$or)) { if (Array.isArray(queryConditions.$and)) { query.where({ $and: [ ...queryConditions.$and, { $or: queryConditions.$or }, { $or: cursorConditions } ] }) } else { query.where({ $and: [ { $or: queryConditions.$or }, { $or: cursorConditions } ] }) } // Get rid of stray $or // `setQuery` introduced in mongoose 5.2.9 if (typeof query.setQuery === 'function') { const newQueryConditions = query.getQuery() delete newQueryConditions.$or query.setQuery(newQueryConditions) } else { // The legacy way // TODO: Remove this at some point delete query._conditions.$or } } else { query.where({ $or: cursorConditions }) } } return query }