mongo-cursor-pagination
Version:
Make it easy to return cursor-paginated results from a Mongo collection
102 lines (87 loc) • 3.92 kB
text/typescript
import { ProjectionFieldSet } from 'projection-utils';
import _ from 'underscore';
/**
* Produce a ProjectionFieldSet from the given MongoDB projection, after validating it to ensure it
* doesn't have exclusion rules.
*
* @param projection - The projected fields.
* @param includeIdDefault - Whether to include _id by default (MongoDB's default behavior).
* @returns The synthesized field set.
*/
function fieldsFromMongo(
projection: Record<string, any> = {},
includeIdDefault = false
): ProjectionFieldSet {
const fields = _.reduce(
projection,
(memo, value, key) => {
if (key !== '_id' && value !== undefined && !value) {
throw new TypeError('projection includes exclusion, but we do not support that');
}
if (value || (key === '_id' && value === undefined && includeIdDefault)) {
memo.push(key);
}
return memo;
},
[]
);
return ProjectionFieldSet.fromDotted(fields);
}
/**
* Resolve the fields object, given potentially untrusted fields the user has provided, permitted
* fields defined by the application, and override fields that should always be provided.
*
* @param desiredFields - The fields in the request.
* @param allowedFields - A shallow fields object defining the fields permitted in desiredFields.
* If not provided, any field is allowed.
* @param overrideFields - A shallow fields object defining fields that should always be included
* or excluded as specified.
* @returns The resolved fields declaration, or null if no fields are valid.
*/
function resolveFields(
desiredFields?: string[] | null,
allowedFields?: Record<string, any> | null,
overrideFields?: Record<string, any> | null
): Record<string, any> | null {
if (desiredFields != null && !Array.isArray(desiredFields)) {
throw new TypeError('expected nullable array for desiredFields');
}
if (allowedFields != null && !_.isObject(allowedFields)) {
throw new TypeError('expected nullable plain object for allowedFields');
}
if (overrideFields !== undefined && !_.isObject(overrideFields)) {
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.
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
// fields defined in parameters. Add override fields from parameters.
const fields = desiredFieldset.intersect(allowedFieldset).union(fieldsFromMongo(overrideFields));
if (fields.isEmpty()) {
// This projection isn't representable as a mongo projection - nor should it be. We don't want
// to query mongo for zero fields.
return null;
}
// Generate the mongo projection.
const projection = fields.toMongo();
// Whether overrideFields explicitly removes _id.
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) {
// If the override excludes _id, then enforce that here. All other fields will be included by
// default, so we don't need to specify them individually, as we only support whitelisting
// fields, and do not support field blacklists.
projection._id = 0;
}
return projection;
}
export default resolveFields;