UNPKG

@conpago/mongo-cursor-pagination

Version:

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

106 lines (91 loc) 3.96 kB
import { ProjectionFieldSet } from "projection-utils"; import _ from "underscore"; /** * Produce a ProjectionFieldSet from the given mongo projection, after validating it to ensure it * doesn't have exclusion rules. * * @param {Object<String, *>} projection The projected fields. * @param {Boolean=} includeIdDefault Whether to include _id by default (mongo's default behavior). * @returns {ProjectionFieldSet} The synthesized field set. */ function fieldsFromMongo( projection: object = {}, includeIdDefault: boolean = 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 {String[]} desiredFields The fields in the request. * @param {?Object<String, *>=} allowedFields A shallow fields object defining the fields permitted * in desiredFields. If not provided, we just allow any field. * @param {Object<String, *>=} overrideFields A shallow fields object defining fields that should * always be configured as specified. * @returns {?Object<String, *>=} The resolved fields declaration. */ function resolveFields( desiredFields?: string[], allowedFields?: object | null, overrideFields?: { [key: string]: string | number } ): (object | null) | undefined { 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;