mongo-cursor-pagination
Advanced tools
Comparing version 8.0.1 to 8.1.0
@@ -0,1 +1,8 @@ | ||
## [8.1.0](https://github.com/mixmaxhq/mongo-cursor-pagination/compare/v8.0.1...v8.1.0) (2022-08-25) | ||
### Features | ||
* update babel to v7 ([6a86084](https://github.com/mixmaxhq/mongo-cursor-pagination/commit/6a86084253a5b950e2549df3cb537e8c5eaef7c5)) | ||
### [8.0.1](https://github.com/mixmaxhq/mongo-cursor-pagination/compare/v8.0.0...v8.0.1) (2022-08-24) | ||
@@ -2,0 +9,0 @@ |
@@ -1,8 +0,14 @@ | ||
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } | ||
"use strict"; | ||
const _ = require('underscore'); | ||
const sanitizeParams = require('./utils/sanitizeParams'); | ||
const { prepareResponse, generateSort, generateCursorQuery } = require('./utils/query'); | ||
const { | ||
prepareResponse, | ||
generateSort, | ||
generateCursorQuery | ||
} = require('./utils/query'); | ||
const config = require('./config'); | ||
/** | ||
@@ -45,45 +51,60 @@ * Performs an aggregate() query on a passed-in Mongo collection, using criteria you specify. | ||
*/ | ||
module.exports = (() => { | ||
var _ref = _asyncToGenerator(function* (collection, params) { | ||
params = _.defaults((yield sanitizeParams(collection, params)), { aggregation: [] }); | ||
const $match = generateCursorQuery(params); | ||
const $sort = generateSort(params); | ||
const $limit = params.limit + 1; | ||
let aggregation; | ||
if (params.sortCaseInsensitive) { | ||
aggregation = params.aggregation.concat([{ $addFields: { __lc: { $toLower: '$' + params.paginatedField } } }, { $match }, { $sort }, { $limit }, { $project: { __lc: 0 } }]); | ||
} else { | ||
aggregation = params.aggregation.concat([{ $match }, { $sort }, { $limit }]); | ||
} | ||
module.exports = async function aggregate(collection, params) { | ||
params = _.defaults(await sanitizeParams(collection, params), { | ||
aggregation: [] | ||
}); | ||
const $match = generateCursorQuery(params); | ||
const $sort = generateSort(params); | ||
const $limit = params.limit + 1; | ||
let aggregation; | ||
// Aggregation options: | ||
// https://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html#aggregate | ||
// https://mongodb.github.io/node-mongodb-native/4.0/interfaces/aggregateoptions.html | ||
const options = Object.assign({}, params.options); | ||
/** | ||
* 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; | ||
if (collation && !isCollationNull) options.collation = collation; | ||
if (params.sortCaseInsensitive) { | ||
aggregation = params.aggregation.concat([{ | ||
$addFields: { | ||
__lc: { | ||
$toLower: '$' + params.paginatedField | ||
} | ||
} | ||
}, { | ||
$match | ||
}, { | ||
$sort | ||
}, { | ||
$limit | ||
}, { | ||
$project: { | ||
__lc: 0 | ||
} | ||
}]); | ||
} else { | ||
aggregation = params.aggregation.concat([{ | ||
$match | ||
}, { | ||
$sort | ||
}, { | ||
$limit | ||
}]); | ||
} // Aggregation options: | ||
// https://mongodb.github.io/node-mongodb-native/3.6/api/Collection.html#aggregate | ||
// https://mongodb.github.io/node-mongodb-native/4.0/interfaces/aggregateoptions.html | ||
// Support both the native 'mongodb' driver and 'mongoist'. See: | ||
// https://www.npmjs.com/package/mongoist#cursor-operations | ||
const aggregateMethod = collection.aggregateAsCursor ? 'aggregateAsCursor' : 'aggregate'; | ||
const results = yield collection[aggregateMethod](aggregation, options).toArray(); | ||
const options = Object.assign({}, params.options); | ||
/** | ||
* IMPORTANT | ||
* | ||
* If using collation, check the README: | ||
* https://github.com/mixmaxhq/mongo-cursor-pagination#important-note-regarding-collation | ||
*/ | ||
return prepareResponse(results, params); | ||
}); | ||
const isCollationNull = params.collation === null; | ||
const collation = params.collation || config.COLLATION; | ||
if (collation && !isCollationNull) options.collation = collation; // Support both the native 'mongodb' driver and 'mongoist'. See: | ||
// https://www.npmjs.com/package/mongoist#cursor-operations | ||
function aggregate(_x, _x2) { | ||
return _ref.apply(this, arguments); | ||
} | ||
return aggregate; | ||
})(); | ||
const aggregateMethod = collection.aggregateAsCursor ? 'aggregateAsCursor' : 'aggregate'; | ||
const results = await collection[aggregateMethod](aggregation, options).toArray(); | ||
return prepareResponse(results, params); | ||
}; |
@@ -0,1 +1,3 @@ | ||
"use strict"; | ||
module.exports = { | ||
@@ -2,0 +4,0 @@ /** |
@@ -1,9 +0,16 @@ | ||
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } | ||
"use strict"; | ||
const _ = require('underscore'); | ||
const sanitizeParams = require('./utils/sanitizeParams'); | ||
const { prepareResponse, generateSort, generateCursorQuery } = require('./utils/query'); | ||
const { | ||
prepareResponse, | ||
generateSort, | ||
generateCursorQuery | ||
} = require('./utils/query'); | ||
const aggregate = require('./aggregate'); | ||
const config = require('./config'); | ||
/** | ||
@@ -37,55 +44,51 @@ * Performs a find() query on a passed-in Mongo collection, using criteria you specify. The results | ||
*/ | ||
module.exports = (() => { | ||
var _ref = _asyncToGenerator(function* (collection, params) { | ||
const removePaginatedFieldInResponse = params.fields && !params.fields[params.paginatedField || '_id']; | ||
let response; | ||
if (params.sortCaseInsensitive) { | ||
// For case-insensitive sorting, we need to work with an aggregation: | ||
response = aggregate(collection, Object.assign({}, params, { | ||
aggregation: params.query ? [{ $match: params.query }] : [] | ||
})); | ||
} else { | ||
// Need to repeat `params.paginatedField` default value ('_id') since it's set in 'sanitizeParams()' | ||
params = _.defaults((yield sanitizeParams(collection, params)), { query: {} }); | ||
const cursorQuery = generateCursorQuery(params); | ||
const $sort = generateSort(params); | ||
module.exports = async function (collection, params) { | ||
const removePaginatedFieldInResponse = params.fields && !params.fields[params.paginatedField || '_id']; | ||
let response; | ||
// Support both the native 'mongodb' driver and 'mongoist'. See: | ||
// https://www.npmjs.com/package/mongoist#cursor-operations | ||
const findMethod = collection.findAsCursor ? 'findAsCursor' : 'find'; | ||
if (params.sortCaseInsensitive) { | ||
// For case-insensitive sorting, we need to work with an aggregation: | ||
response = aggregate(collection, Object.assign({}, params, { | ||
aggregation: params.query ? [{ | ||
$match: params.query | ||
}] : [] | ||
})); | ||
} else { | ||
// Need to repeat `params.paginatedField` default value ('_id') since it's set in 'sanitizeParams()' | ||
params = _.defaults(await sanitizeParams(collection, params), { | ||
query: {} | ||
}); | ||
const cursorQuery = generateCursorQuery(params); | ||
const $sort = generateSort(params); // Support both the native 'mongodb' driver and 'mongoist'. See: | ||
// https://www.npmjs.com/package/mongoist#cursor-operations | ||
const query = collection[findMethod]({ $and: [cursorQuery, params.query] }, params.fields); | ||
const findMethod = collection.findAsCursor ? 'findAsCursor' : 'find'; | ||
const query = collection[findMethod]({ | ||
$and: [cursorQuery, params.query] | ||
}, params.fields); | ||
/** | ||
* IMPORTANT | ||
* | ||
* If using collation, check the README: | ||
* https://github.com/mixmaxhq/mongo-cursor-pagination#important-note-regarding-collation | ||
*/ | ||
/** | ||
* 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 = yield cursor.toArray(); | ||
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. | ||
response = prepareResponse(results, params); | ||
} | ||
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); | ||
} // Remove fields that we added to the query (such as paginatedField and _id) that the user didn't ask for. | ||
// Remove fields that we added to the query (such as paginatedField and _id) that the user didn't ask for. | ||
if (removePaginatedFieldInResponse) { | ||
response.results = _.map(response.results, function (result) { | ||
return _.omit(result, params.paginatedField); | ||
}); | ||
} | ||
return response; | ||
}); | ||
if (removePaginatedFieldInResponse) { | ||
response.results = _.map(response.results, result => _.omit(result, params.paginatedField)); | ||
} | ||
return function (_x, _x2) { | ||
return _ref.apply(this, arguments); | ||
}; | ||
})(); | ||
return response; | ||
}; |
@@ -1,6 +0,6 @@ | ||
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } | ||
"use strict"; | ||
const find = require('./find'); | ||
const sanitizeQuery = require('./utils/sanitizeQuery'); | ||
/** | ||
@@ -26,14 +26,7 @@ * A wrapper around `find()` that make it easy to implement a basic HTTP API using Express. So your | ||
*/ | ||
module.exports = (() => { | ||
var _ref = _asyncToGenerator(function* (req, collection, params) { | ||
params = sanitizeQuery(req.query, params); | ||
return find(collection, params); | ||
}); | ||
function findWithReq(_x, _x2, _x3) { | ||
return _ref.apply(this, arguments); | ||
} | ||
return findWithReq; | ||
})(); | ||
module.exports = async function findWithReq(req, collection, params) { | ||
params = sanitizeQuery(req.query, params); | ||
return find(collection, params); | ||
}; |
@@ -0,8 +1,19 @@ | ||
"use strict"; | ||
const config = require('./config'); | ||
const aggregate = require('./aggregate'); | ||
const find = require('./find'); | ||
const findWithReq = require('./findWithReq'); | ||
const search = require('./search'); | ||
const sanitizeQuery = require('./utils/sanitizeQuery'); | ||
const { encodePaginationTokens } = require('./utils/query'); | ||
const { | ||
encodePaginationTokens | ||
} = require('./utils/query'); | ||
const mongoosePlugin = require('./mongoose.plugin'); | ||
@@ -9,0 +20,0 @@ |
@@ -0,5 +1,8 @@ | ||
"use strict"; | ||
const find = require('./find'); | ||
const search = require('./search'); | ||
const _ = require('underscore'); | ||
/** | ||
@@ -13,2 +16,3 @@ * Mongoose plugin | ||
module.exports = function (schema, options) { | ||
@@ -25,6 +29,4 @@ /** | ||
params = _.extend({}, params); | ||
return find(this.collection, params); | ||
}; | ||
/** | ||
@@ -35,2 +37,4 @@ * search function | ||
*/ | ||
const searchFn = function (searchString, params) { | ||
@@ -42,3 +46,2 @@ if (!this.collection) { | ||
params = _.extend({}, params); | ||
return search(this.collection, searchString, params); | ||
@@ -45,0 +48,0 @@ }; |
@@ -1,7 +0,8 @@ | ||
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } | ||
"use strict"; | ||
const _ = require('underscore'); | ||
const config = require('./config'); | ||
const bsonUrlEncoding = require('./utils/bsonUrlEncoding'); | ||
/** | ||
@@ -26,87 +27,77 @@ * Performs a search query on a Mongo collection and pages the results. This is different from | ||
*/ | ||
module.exports = (() => { | ||
var _ref = _asyncToGenerator(function* (collection, searchString, params) { | ||
if (_.isString(params.limit)) params.limit = parseInt(params.limit, 10); | ||
if (params.next) params.next = bsonUrlEncoding.decode(params.next); | ||
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; | ||
module.exports = async function (collection, searchString, params) { | ||
if (_.isString(params.limit)) params.limit = parseInt(params.limit, 10); | ||
if (params.next) params.next = bsonUrlEncoding.decode(params.next); | ||
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; // We must perform an aggregate query since Mongo can't query a range when using $text search. | ||
// We must perform an aggregate query since Mongo can't query a range when using $text search. | ||
const aggregate = [{ | ||
$match: _.extend({}, params.query, { | ||
$text: { | ||
$search: searchString | ||
} | ||
}) | ||
}, { | ||
$project: _.extend({}, params.fields, { | ||
_id: 1, | ||
score: { | ||
$meta: 'textScore' | ||
} | ||
}) | ||
}, { | ||
$sort: { | ||
score: { | ||
$meta: 'textScore' | ||
}, | ||
_id: -1 | ||
const aggregate = [{ | ||
$match: _.extend({}, params.query, { | ||
$text: { | ||
$search: searchString | ||
} | ||
}]; | ||
if (params.next) { | ||
aggregate.push({ | ||
$match: { | ||
$or: [{ | ||
score: { | ||
$lt: params.next[0] | ||
} | ||
}, { | ||
score: { | ||
$eq: params.next[0] | ||
}, | ||
_id: { | ||
$lt: params.next[1] | ||
} | ||
}] | ||
} | ||
}); | ||
}) | ||
}, { | ||
$project: _.extend({}, params.fields, { | ||
_id: 1, | ||
score: { | ||
$meta: 'textScore' | ||
} | ||
}) | ||
}, { | ||
$sort: { | ||
score: { | ||
$meta: 'textScore' | ||
}, | ||
_id: -1 | ||
} | ||
}]; | ||
if (params.next) { | ||
aggregate.push({ | ||
$limit: params.limit | ||
$match: { | ||
$or: [{ | ||
score: { | ||
$lt: params.next[0] | ||
} | ||
}, { | ||
score: { | ||
$eq: params.next[0] | ||
}, | ||
_id: { | ||
$lt: params.next[1] | ||
} | ||
}] | ||
} | ||
}); | ||
} | ||
let response; | ||
aggregate.push({ | ||
$limit: params.limit | ||
}); | ||
let response; // Support both the native 'mongodb' driver and 'mongoist'. See: | ||
// https://www.npmjs.com/package/mongoist#cursor-operations | ||
// Support both the native 'mongodb' driver and 'mongoist'. See: | ||
// https://www.npmjs.com/package/mongoist#cursor-operations | ||
const aggregateMethod = collection.aggregateAsCursor ? 'aggregateAsCursor' : 'aggregate'; | ||
const aggregateMethod = collection.aggregateAsCursor ? 'aggregateAsCursor' : 'aggregate'; | ||
const results = await collection[aggregateMethod](aggregate).toArray(); | ||
const fullPageOfResults = results.length === params.limit; | ||
const results = yield collection[aggregateMethod](aggregate).toArray(); | ||
if (fullPageOfResults) { | ||
response = { | ||
results, | ||
next: bsonUrlEncoding.encode([_.last(results).score, _.last(results)._id]) | ||
}; | ||
} else { | ||
response = { | ||
results | ||
}; | ||
} | ||
const fullPageOfResults = results.length === params.limit; | ||
if (fullPageOfResults) { | ||
response = { | ||
results, | ||
next: bsonUrlEncoding.encode([_.last(results).score, _.last(results)._id]) | ||
}; | ||
} else { | ||
response = { | ||
results | ||
}; | ||
} | ||
return response; | ||
}); | ||
return function (_x, _x2, _x3) { | ||
return _ref.apply(this, arguments); | ||
}; | ||
})(); | ||
return response; | ||
}; |
@@ -1,4 +0,8 @@ | ||
const { EJSON } = require('bson'); | ||
"use strict"; | ||
const { | ||
EJSON | ||
} = require('bson'); | ||
const base64url = require('base64-url'); | ||
/** | ||
@@ -9,2 +13,3 @@ * These will take a BSON object (an database result returned by the MongoDB library) and | ||
module.exports.encode = function (obj) { | ||
@@ -11,0 +16,0 @@ return base64url.encode(EJSON.stringify(obj)); |
@@ -0,9 +1,12 @@ | ||
"use strict"; | ||
module.exports = function getPropertyViaDotNotation(propertyName, object) { | ||
const parts = propertyName.split('.'); | ||
let prop = object; | ||
let prop = object; | ||
for (let i = 0; i < parts.length; i++) { | ||
prop = prop[parts[i]]; | ||
} | ||
return prop; | ||
}; |
@@ -0,4 +1,6 @@ | ||
"use strict"; | ||
const bsonUrlEncoding = require('./bsonUrlEncoding'); | ||
const objectPath = require('object-path'); | ||
/** | ||
@@ -19,2 +21,4 @@ * Helper function to encode pagination tokens. | ||
*/ | ||
function encodePaginationTokens(params, response) { | ||
@@ -26,2 +30,3 @@ const shouldSecondarySortOnId = params.paginatedField !== '_id'; | ||
if (params.sortCaseInsensitive) previousPaginatedField = previousPaginatedField.toLowerCase(); | ||
if (shouldSecondarySortOnId) { | ||
@@ -33,5 +38,7 @@ response.previous = bsonUrlEncoding.encode([previousPaginatedField, response.previous._id]); | ||
} | ||
if (response.next) { | ||
let nextPaginatedField = objectPath.get(response.next, params.paginatedField); | ||
if (params.sortCaseInsensitive) nextPaginatedField = nextPaginatedField.toLowerCase(); | ||
if (shouldSecondarySortOnId) { | ||
@@ -56,12 +63,9 @@ response.next = bsonUrlEncoding.encode([nextPaginatedField, response.next._id]); | ||
prepareResponse(results, params) { | ||
const hasMore = results.length > params.limit; | ||
// Remove the extra element that we added to 'peek' to see if there were more entries. | ||
const hasMore = results.length > params.limit; // Remove the extra element that we added to 'peek' to see if there were more entries. | ||
if (hasMore) results.pop(); | ||
const hasPrevious = !!params.next || !!(params.previous && hasMore); | ||
const hasNext = !!params.previous || hasMore; | ||
const hasNext = !!params.previous || hasMore; // If we sorted reverse to get the previous page, correct the sort order. | ||
// If we sorted reverse to get the previous page, correct the sort order. | ||
if (params.previous) results = results.reverse(); | ||
const response = { | ||
@@ -74,5 +78,3 @@ results, | ||
}; | ||
encodePaginationTokens(params, response); | ||
return response; | ||
@@ -116,7 +118,5 @@ }, | ||
if (!params.next && !params.previous) return {}; | ||
const sortAsc = !params.sortAscending && params.previous || params.sortAscending && !params.previous; | ||
const comparisonOp = sortAsc ? '$gt' : '$lt'; | ||
const comparisonOp = sortAsc ? '$gt' : '$lt'; // a `next` cursor will have precedence over a `previous` cursor. | ||
// a `next` cursor will have precedence over a `previous` cursor. | ||
const op = params.next || params.previous; | ||
@@ -148,2 +148,3 @@ | ||
} | ||
}; |
@@ -0,4 +1,8 @@ | ||
"use strict"; | ||
const _ = require('underscore'); | ||
const { ProjectionFieldSet } = require('projection-utils'); | ||
const { | ||
ProjectionFieldSet | ||
} = require('projection-utils'); | ||
/** | ||
@@ -12,2 +16,4 @@ * Produce a ProjectionFieldSet from the given mongo projection, after validating it to ensure it | ||
*/ | ||
function fieldsFromMongo(projection = {}, includeIdDefault = false) { | ||
@@ -18,2 +24,3 @@ const fields = _.reduce(projection, (memo, value, key) => { | ||
} | ||
if (value || key === '_id' && value === undefined && includeIdDefault) { | ||
@@ -28,3 +35,2 @@ memo.push(key); | ||
} | ||
/** | ||
@@ -41,2 +47,4 @@ * Resolve the fields object, given potentially untrusted fields the user has provided, permitted | ||
*/ | ||
function resolveFields(desiredFields, allowedFields, overrideFields) { | ||
@@ -53,13 +61,11 @@ if (desiredFields != null && !Array.isArray(desiredFields)) { | ||
throw new TypeError('expected optional plain object for overrideFields'); | ||
} | ||
} // If no desired fields are specified, we treat that as wanting the default set of fields. | ||
// If no desired fields are specified, we treat that as wanting the default set of fields. | ||
const desiredFieldset = _.isEmpty(desiredFields) ? new ProjectionFieldSet([[]]) : ProjectionFieldSet.fromDotted(desiredFields); | ||
// If allowedFields isn't provided, we treat that as not having restrictions. However, if it's an | ||
const desiredFieldset = _.isEmpty(desiredFields) ? new ProjectionFieldSet([[]]) : ProjectionFieldSet.fromDotted(desiredFields); // If allowedFields isn't provided, we treat that as not having restrictions. However, if it's an | ||
// empty array, we treat that as have no valid fields. | ||
const allowedFieldset = allowedFields ? fieldsFromMongo(allowedFields) : new ProjectionFieldSet([[]]); | ||
// Don't trust fields passed in the querystring, so whitelist them against the | ||
const allowedFieldset = allowedFields ? fieldsFromMongo(allowedFields) : new ProjectionFieldSet([[]]); // Don't trust fields passed in the querystring, so whitelist them against the | ||
// fields defined in parameters. Add override fields from parameters. | ||
const fields = desiredFieldset.intersect(allowedFieldset).union(fieldsFromMongo(overrideFields)); | ||
@@ -71,12 +77,10 @@ | ||
return null; | ||
} | ||
} // Generate the mongo projection. | ||
// Generate the mongo projection. | ||
const projection = fields.toMongo(); | ||
// Whether overrideFields explicitly removes _id. | ||
const disableIdOverride = overrideFields && overrideFields._id !== undefined && !overrideFields._id; | ||
const projection = fields.toMongo(); // Whether overrideFields explicitly removes _id. | ||
// Explicitly exclude the _id field (which mongo includes by default) if we don't allow it, or | ||
const disableIdOverride = overrideFields && overrideFields._id !== undefined && !overrideFields._id; // Explicitly exclude the _id field (which mongo includes by default) if we don't allow it, or | ||
// if we've disabled it in the override. | ||
if (!fields.contains(['_id']) || disableIdOverride) { | ||
@@ -88,2 +92,3 @@ // If the override excludes _id, then enforce that here. All other fields will be included by | ||
} | ||
return projection; | ||
@@ -90,0 +95,0 @@ } |
@@ -1,89 +0,93 @@ | ||
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } | ||
"use strict"; | ||
const _ = require('underscore'); | ||
const bsonUrlEncoding = require('./bsonUrlEncoding'); | ||
const getPropertyViaDotNotation = require('./getPropertyViaDotNotation'); | ||
const config = require('../config'); | ||
module.exports = (() => { | ||
var _ref = _asyncToGenerator(function* (collection, params) { | ||
if (params.previous) params.previous = bsonUrlEncoding.decode(params.previous); | ||
if (params.next) params.next = bsonUrlEncoding.decode(params.next); | ||
module.exports = async function sanitizeParams(collection, params) { | ||
if (params.previous) params.previous = bsonUrlEncoding.decode(params.previous); | ||
if (params.next) params.next = bsonUrlEncoding.decode(params.next); | ||
params = _.defaults(params, { | ||
limit: config.DEFAULT_LIMIT, | ||
paginatedField: '_id' | ||
}); | ||
if (params.limit < 1) params.limit = 1; | ||
if (params.limit > config.MAX_LIMIT) params.limit = config.MAX_LIMIT; // If the paginated field is not _id, then it might have duplicate values in it. This is bad | ||
// because then we can't exclusively use it for our range queries (that use $lt and $gt). So | ||
// to fix this, we secondarily sort on _id, which is always unique. | ||
params = _.defaults(params, { | ||
limit: config.DEFAULT_LIMIT, | ||
paginatedField: '_id' | ||
}); | ||
const shouldSecondarySortOnId = params.paginatedField !== '_id'; // | ||
// params.after - overides params.next | ||
// | ||
// 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. | ||
if (params.limit < 1) params.limit = 1; | ||
if (params.limit > config.MAX_LIMIT) params.limit = config.MAX_LIMIT; | ||
if (params.after) { | ||
if (shouldSecondarySortOnId) { | ||
// Since the primary sort field is not provided by the 'after' pagination cursor we | ||
// have to look it up when the paginated field is not _id. | ||
const doc = await collection.findOne({ | ||
_id: params.after | ||
}, { | ||
[params.paginatedField]: true, | ||
_id: false | ||
}); | ||
// If the paginated field is not _id, then it might have duplicate values in it. This is bad | ||
// because then we can't exclusively use it for our range queries (that use $lt and $gt). So | ||
// to fix this, we secondarily sort on _id, which is always unique. | ||
const shouldSecondarySortOnId = params.paginatedField !== '_id'; | ||
// | ||
// params.after - overides params.next | ||
// | ||
// 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. | ||
if (params.after) { | ||
if (shouldSecondarySortOnId) { | ||
// Since the primary sort field is not provided by the 'after' pagination cursor we | ||
// have to look it up when the paginated field is not _id. | ||
const doc = yield collection.findOne({ _id: params.after }, { [params.paginatedField]: true, _id: false }); | ||
if (doc) { | ||
// Handle usage of dot notation in paginatedField | ||
let prop = getPropertyViaDotNotation(params.paginatedField, doc); | ||
if (params.sortCaseInsensitive) prop = prop.toLowerCase(); | ||
params.next = [prop, params.after]; | ||
} | ||
} else { | ||
params.next = params.after; | ||
if (doc) { | ||
// Handle usage of dot notation in paginatedField | ||
let prop = getPropertyViaDotNotation(params.paginatedField, doc); | ||
if (params.sortCaseInsensitive) prop = prop.toLowerCase(); | ||
params.next = [prop, params.after]; | ||
} | ||
} else { | ||
params.next = params.after; | ||
} | ||
} // | ||
// params.before - overides params.previous | ||
// | ||
// 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. | ||
// | ||
// params.before - overides params.previous | ||
// | ||
// 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. | ||
if (params.before) { | ||
if (shouldSecondarySortOnId) { | ||
// Since the primary sort field is not provided by the 'before' pagination cursor we | ||
// have to look it up when the paginated field is not _id. | ||
const doc = yield collection.findOne({ _id: params.before }, { [params.paginatedField]: true, _id: false }); | ||
if (doc) { | ||
// Handle usage of dot notation in paginatedField | ||
let prop = getPropertyViaDotNotation(params.paginatedField, doc); | ||
if (params.sortCaseInsensitive) prop = prop.toLowerCase(); | ||
params.previous = [prop, params.before]; | ||
} | ||
} else { | ||
params.previous = params.before; | ||
} | ||
} | ||
// The query must always include the paginatedField so we can construct the cursor. | ||
if (params.fields) { | ||
params.fields = _.extend({ | ||
_id: 0 // Mongo includes this field by default, so don't request it unless the user wants it. | ||
}, params.fields); | ||
if (params.before) { | ||
if (shouldSecondarySortOnId) { | ||
// Since the primary sort field is not provided by the 'before' pagination cursor we | ||
// have to look it up when the paginated field is not _id. | ||
const doc = await collection.findOne({ | ||
_id: params.before | ||
}, { | ||
[params.paginatedField]: true, | ||
_id: false | ||
}); | ||
if (!params.fields[params.paginatedField]) { | ||
params.fields[params.paginatedField] = 1; | ||
if (doc) { | ||
// Handle usage of dot notation in paginatedField | ||
let prop = getPropertyViaDotNotation(params.paginatedField, doc); | ||
if (params.sortCaseInsensitive) prop = prop.toLowerCase(); | ||
params.previous = [prop, params.before]; | ||
} | ||
} else { | ||
params.previous = params.before; | ||
} | ||
} // The query must always include the paginatedField so we can construct the cursor. | ||
return params; | ||
}); | ||
function sanitizeParams(_x, _x2) { | ||
return _ref.apply(this, arguments); | ||
if (params.fields) { | ||
params.fields = _.extend({ | ||
_id: 0 // Mongo includes this field by default, so don't request it unless the user wants it. | ||
}, params.fields); | ||
if (!params.fields[params.paginatedField]) { | ||
params.fields[params.paginatedField] = 1; | ||
} | ||
} | ||
return sanitizeParams; | ||
})(); | ||
return params; | ||
}; |
@@ -0,4 +1,6 @@ | ||
"use strict"; | ||
const _ = require('underscore'); | ||
const resolveFields = require('./resolveFields'); | ||
/** | ||
@@ -14,4 +16,7 @@ * Normalize the given query parameter to an array, so we support both param=a,b and | ||
*/ | ||
function normalizeQueryArray(query, param) { | ||
const value = query[param]; | ||
if (Array.isArray(value)) { | ||
@@ -23,15 +28,18 @@ for (let i = 0; i < value.length; ++i) { | ||
} | ||
return value; | ||
} | ||
// This goes before _.isString so we don't split an empty string into ['']. The array option just | ||
} // This goes before _.isString so we don't split an empty string into ['']. The array option just | ||
// uses whatever the user provides. | ||
if (_.isEmpty(value)) { | ||
return []; | ||
} | ||
if (_.isString(value)) { | ||
return value.split(','); | ||
} | ||
throw new TypeError('expected string array or comma-separated string for ' + param); | ||
} | ||
/** | ||
@@ -53,2 +61,4 @@ * Sanitizes a `query` object received and merges it's changes to an optional `params` object | ||
*/ | ||
module.exports = function sanitizeQuery(query, params) { | ||
@@ -58,4 +68,4 @@ params = params || {}; | ||
if (!_.isEmpty(query.limit)) { | ||
const limit = parseInt(query.limit, 10); | ||
// Don't let the user specify a higher limit than params.limit, if defined. | ||
const limit = parseInt(query.limit, 10); // Don't let the user specify a higher limit than params.limit, if defined. | ||
if (!isNaN(limit) && (!params.limit || params.limit > limit)) { | ||
@@ -72,15 +82,15 @@ params.limit = limit; | ||
params.previous = query.previous; | ||
} | ||
} // Don't trust fields passed in the querystring, so whitelist them against the fields defined in | ||
// parameters. | ||
// Don't trust fields passed in the querystring, so whitelist them against the fields defined in | ||
// parameters. | ||
const fields = resolveFields(normalizeQueryArray(query, 'fields'), params.fields, params.overrideFields); | ||
if (fields === null) { | ||
throw new TypeError('no valid fields provided'); | ||
} | ||
} // Set fields to undefined if it's empty to avoid adding _id: 0 in find. | ||
// Set fields to undefined if it's empty to avoid adding _id: 0 in find. | ||
params.fields = _.isEmpty(fields) ? undefined : fields; | ||
return params; | ||
}; |
{ | ||
"name": "mongo-cursor-pagination", | ||
"version": "8.0.1", | ||
"version": "8.1.0", | ||
"description": "Make it easy to return cursor-paginated results from a Mongo collection", | ||
@@ -49,2 +49,5 @@ "main": "index.js", | ||
"devDependencies": { | ||
"@babel/cli": "^7.18.10", | ||
"@babel/core": "^7.18.13", | ||
"@babel/preset-env": "^7.18.10", | ||
"@commitlint/config-conventional": "^8.3.4", | ||
@@ -54,5 +57,3 @@ "@mixmaxhq/commitlint-jenkins": "^1.4.4", | ||
"@mixmaxhq/semantic-release-config": "^2.0.0", | ||
"babel-cli": "^6.26.0", | ||
"babel-core": "^6.26.0", | ||
"babel-plugin-transform-async-to-generator": "^6.24.1", | ||
"babel-jest": "^29.0.0", | ||
"cz-conventional-changelog": "^3.2.0", | ||
@@ -59,0 +60,0 @@ "eslint": "^6.8.0", |
1472
86064
19