Spaces:
Paused
Paused
| ; | |
| /*! | |
| * Module dependencies. | |
| */ | |
| const CastError = require('./error/cast'); | |
| const StrictModeError = require('./error/strict'); | |
| const Types = require('./schema/index'); | |
| const cast$expr = require('./helpers/query/cast$expr'); | |
| const castString = require('./cast/string'); | |
| const castTextSearch = require('./schema/operators/text'); | |
| const get = require('./helpers/get'); | |
| const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue'); | |
| const isOperator = require('./helpers/query/isOperator'); | |
| const util = require('util'); | |
| const isObject = require('./helpers/isObject'); | |
| const isMongooseObject = require('./helpers/isMongooseObject'); | |
| const utils = require('./utils'); | |
| const ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon']; | |
| /** | |
| * Handles internal casting for query filters. | |
| * | |
| * @param {Schema} schema | |
| * @param {Object} obj Object to cast | |
| * @param {Object} [options] the query options | |
| * @param {Boolean|"throw"} [options.strict] Wheter to enable all strict options | |
| * @param {Boolean|"throw"} [options.strictQuery] Enable strict Queries | |
| * @param {Boolean} [options.sanitizeFilter] avoid adding implict query selectors ($in) | |
| * @param {Boolean} [options.upsert] | |
| * @param {Query} [context] passed to setters | |
| * @api private | |
| */ | |
| module.exports = function cast(schema, obj, options, context) { | |
| if (Array.isArray(obj)) { | |
| throw new Error('Query filter must be an object, got an array ', util.inspect(obj)); | |
| } | |
| if (obj == null) { | |
| return obj; | |
| } | |
| if (schema != null && schema.discriminators != null && obj[schema.options.discriminatorKey] != null) { | |
| schema = getSchemaDiscriminatorByValue(schema, obj[schema.options.discriminatorKey]) || schema; | |
| } | |
| const paths = Object.keys(obj); | |
| let i = paths.length; | |
| let _keys; | |
| let any$conditionals; | |
| let schematype; | |
| let nested; | |
| let path; | |
| let type; | |
| let val; | |
| options = options || {}; | |
| while (i--) { | |
| path = paths[i]; | |
| val = obj[path]; | |
| if (path === '$or' || path === '$nor' || path === '$and') { | |
| if (!Array.isArray(val)) { | |
| throw new CastError('Array', val, path); | |
| } | |
| for (let k = val.length - 1; k >= 0; k--) { | |
| if (val[k] == null || typeof val[k] !== 'object') { | |
| throw new CastError('Object', val[k], path + '.' + k); | |
| } | |
| const beforeCastKeysLength = Object.keys(val[k]).length; | |
| const discriminatorValue = val[k][schema.options.discriminatorKey]; | |
| if (discriminatorValue == null) { | |
| val[k] = cast(schema, val[k], options, context); | |
| } else { | |
| const discriminatorSchema = getSchemaDiscriminatorByValue(context.schema, discriminatorValue); | |
| val[k] = cast(discriminatorSchema ? discriminatorSchema : schema, val[k], options, context); | |
| } | |
| if (Object.keys(val[k]).length === 0 && beforeCastKeysLength !== 0) { | |
| val.splice(k, 1); | |
| } | |
| } | |
| // delete empty: {$or: []} -> {} | |
| if (val.length === 0) { | |
| delete obj[path]; | |
| } | |
| } else if (path === '$where') { | |
| type = typeof val; | |
| if (type !== 'string' && type !== 'function') { | |
| throw new Error('Must have a string or function for $where'); | |
| } | |
| if (type === 'function') { | |
| obj[path] = val.toString(); | |
| } | |
| continue; | |
| } else if (path === '$expr') { | |
| val = cast$expr(val, schema); | |
| continue; | |
| } else if (path === '$elemMatch') { | |
| val = cast(schema, val, options, context); | |
| } else if (path === '$text') { | |
| val = castTextSearch(val, path); | |
| } else if (path === '$comment' && !schema.paths.hasOwnProperty('$comment')) { | |
| val = castString(val, path); | |
| obj[path] = val; | |
| } else { | |
| if (!schema) { | |
| // no casting for Mixed types | |
| continue; | |
| } | |
| schematype = schema.path(path); | |
| // Check for embedded discriminator paths | |
| if (!schematype) { | |
| const split = path.split('.'); | |
| let j = split.length; | |
| while (j--) { | |
| const pathFirstHalf = split.slice(0, j).join('.'); | |
| const pathLastHalf = split.slice(j).join('.'); | |
| const _schematype = schema.path(pathFirstHalf); | |
| const discriminatorKey = _schematype && | |
| _schematype.schema && | |
| _schematype.schema.options && | |
| _schematype.schema.options.discriminatorKey; | |
| // gh-6027: if we haven't found the schematype but this path is | |
| // underneath an embedded discriminator and the embedded discriminator | |
| // key is in the query, use the embedded discriminator schema | |
| if (_schematype != null && | |
| (_schematype.schema && _schematype.schema.discriminators) != null && | |
| discriminatorKey != null && | |
| pathLastHalf !== discriminatorKey) { | |
| const discriminatorVal = get(obj, pathFirstHalf + '.' + discriminatorKey); | |
| const discriminators = _schematype.schema.discriminators; | |
| if (typeof discriminatorVal === 'string' && discriminators[discriminatorVal] != null) { | |
| schematype = discriminators[discriminatorVal].path(pathLastHalf); | |
| } else if (discriminatorVal != null && | |
| Object.keys(discriminatorVal).length === 1 && | |
| Array.isArray(discriminatorVal.$in) && | |
| discriminatorVal.$in.length === 1 && | |
| typeof discriminatorVal.$in[0] === 'string' && | |
| discriminators[discriminatorVal.$in[0]] != null) { | |
| schematype = discriminators[discriminatorVal.$in[0]].path(pathLastHalf); | |
| } | |
| } | |
| } | |
| } | |
| if (!schematype) { | |
| // Handle potential embedded array queries | |
| const split = path.split('.'); | |
| let j = split.length; | |
| let pathFirstHalf; | |
| let pathLastHalf; | |
| let remainingConds; | |
| // Find the part of the var path that is a path of the Schema | |
| while (j--) { | |
| pathFirstHalf = split.slice(0, j).join('.'); | |
| schematype = schema.path(pathFirstHalf); | |
| if (schematype) { | |
| break; | |
| } | |
| } | |
| // If a substring of the input path resolves to an actual real path... | |
| if (schematype) { | |
| // Apply the casting; similar code for $elemMatch in schema/array.js | |
| if (schematype.caster && schematype.caster.schema) { | |
| remainingConds = {}; | |
| pathLastHalf = split.slice(j).join('.'); | |
| remainingConds[pathLastHalf] = val; | |
| const ret = cast(schematype.caster.schema, remainingConds, options, context)[pathLastHalf]; | |
| if (ret === void 0) { | |
| delete obj[path]; | |
| } else { | |
| obj[path] = ret; | |
| } | |
| } else { | |
| obj[path] = val; | |
| } | |
| continue; | |
| } | |
| if (isObject(val)) { | |
| // handle geo schemas that use object notation | |
| // { loc: { long: Number, lat: Number } | |
| let geo = ''; | |
| if (val.$near) { | |
| geo = '$near'; | |
| } else if (val.$nearSphere) { | |
| geo = '$nearSphere'; | |
| } else if (val.$within) { | |
| geo = '$within'; | |
| } else if (val.$geoIntersects) { | |
| geo = '$geoIntersects'; | |
| } else if (val.$geoWithin) { | |
| geo = '$geoWithin'; | |
| } | |
| if (geo) { | |
| const numbertype = new Types.Number('__QueryCasting__'); | |
| let value = val[geo]; | |
| if (val.$maxDistance != null) { | |
| val.$maxDistance = numbertype.castForQuery( | |
| null, | |
| val.$maxDistance, | |
| context | |
| ); | |
| } | |
| if (val.$minDistance != null) { | |
| val.$minDistance = numbertype.castForQuery( | |
| null, | |
| val.$minDistance, | |
| context | |
| ); | |
| } | |
| if (geo === '$within') { | |
| const withinType = value.$center | |
| || value.$centerSphere | |
| || value.$box | |
| || value.$polygon; | |
| if (!withinType) { | |
| throw new Error('Bad $within parameter: ' + JSON.stringify(val)); | |
| } | |
| value = withinType; | |
| } else if (geo === '$near' && | |
| typeof value.type === 'string' && Array.isArray(value.coordinates)) { | |
| // geojson; cast the coordinates | |
| value = value.coordinates; | |
| } else if ((geo === '$near' || geo === '$nearSphere' || geo === '$geoIntersects') && | |
| value.$geometry && typeof value.$geometry.type === 'string' && | |
| Array.isArray(value.$geometry.coordinates)) { | |
| if (value.$maxDistance != null) { | |
| value.$maxDistance = numbertype.castForQuery( | |
| null, | |
| value.$maxDistance, | |
| context | |
| ); | |
| } | |
| if (value.$minDistance != null) { | |
| value.$minDistance = numbertype.castForQuery( | |
| null, | |
| value.$minDistance, | |
| context | |
| ); | |
| } | |
| if (isMongooseObject(value.$geometry)) { | |
| value.$geometry = value.$geometry.toObject({ | |
| transform: false, | |
| virtuals: false | |
| }); | |
| } | |
| value = value.$geometry.coordinates; | |
| } else if (geo === '$geoWithin') { | |
| if (value.$geometry) { | |
| if (isMongooseObject(value.$geometry)) { | |
| value.$geometry = value.$geometry.toObject({ virtuals: false }); | |
| } | |
| const geoWithinType = value.$geometry.type; | |
| if (ALLOWED_GEOWITHIN_GEOJSON_TYPES.indexOf(geoWithinType) === -1) { | |
| throw new Error('Invalid geoJSON type for $geoWithin "' + | |
| geoWithinType + '", must be "Polygon" or "MultiPolygon"'); | |
| } | |
| value = value.$geometry.coordinates; | |
| } else { | |
| value = value.$box || value.$polygon || value.$center || | |
| value.$centerSphere; | |
| if (isMongooseObject(value)) { | |
| value = value.toObject({ virtuals: false }); | |
| } | |
| } | |
| } | |
| _cast(value, numbertype, context); | |
| continue; | |
| } | |
| } | |
| if (schema.nested[path]) { | |
| continue; | |
| } | |
| const strict = 'strict' in options ? options.strict : schema.options.strict; | |
| const strictQuery = getStrictQuery(options, schema._userProvidedOptions, schema.options, context); | |
| if (options.upsert && strict) { | |
| if (strict === 'throw') { | |
| throw new StrictModeError(path); | |
| } | |
| throw new StrictModeError(path, 'Path "' + path + '" is not in ' + | |
| 'schema, strict mode is `true`, and upsert is `true`.'); | |
| } if (strictQuery === 'throw') { | |
| throw new StrictModeError(path, 'Path "' + path + '" is not in ' + | |
| 'schema and strictQuery is \'throw\'.'); | |
| } else if (strictQuery) { | |
| delete obj[path]; | |
| } | |
| } else if (val == null) { | |
| continue; | |
| } else if (utils.isPOJO(val)) { | |
| any$conditionals = Object.keys(val).some(isOperator); | |
| if (!any$conditionals) { | |
| obj[path] = schematype.castForQuery( | |
| null, | |
| val, | |
| context | |
| ); | |
| } else { | |
| const ks = Object.keys(val); | |
| let $cond; | |
| let k = ks.length; | |
| while (k--) { | |
| $cond = ks[k]; | |
| nested = val[$cond]; | |
| if ($cond === '$elemMatch') { | |
| if (nested && schematype != null && schematype.schema != null) { | |
| cast(schematype.schema, nested, options, context); | |
| } else if (nested && schematype != null && schematype.$isMongooseArray) { | |
| if (utils.isPOJO(nested) && nested.$not != null) { | |
| cast(schema, nested, options, context); | |
| } else { | |
| val[$cond] = schematype.castForQuery( | |
| $cond, | |
| nested, | |
| context | |
| ); | |
| } | |
| } | |
| } else if ($cond === '$not') { | |
| if (nested && schematype) { | |
| _keys = Object.keys(nested); | |
| if (_keys.length && isOperator(_keys[0])) { | |
| for (const key in nested) { | |
| nested[key] = schematype.castForQuery( | |
| key, | |
| nested[key], | |
| context | |
| ); | |
| } | |
| } else { | |
| val[$cond] = schematype.castForQuery( | |
| $cond, | |
| nested, | |
| context | |
| ); | |
| } | |
| continue; | |
| } | |
| } else { | |
| val[$cond] = schematype.castForQuery( | |
| $cond, | |
| nested, | |
| context | |
| ); | |
| } | |
| } | |
| } | |
| } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1 && !options.sanitizeFilter) { | |
| const casted = []; | |
| const valuesArray = val; | |
| for (const _val of valuesArray) { | |
| casted.push(schematype.castForQuery( | |
| null, | |
| _val, | |
| context | |
| )); | |
| } | |
| obj[path] = { $in: casted }; | |
| } else { | |
| obj[path] = schematype.castForQuery( | |
| null, | |
| val, | |
| context | |
| ); | |
| } | |
| } | |
| } | |
| return obj; | |
| }; | |
| function _cast(val, numbertype, context) { | |
| if (Array.isArray(val)) { | |
| val.forEach(function(item, i) { | |
| if (Array.isArray(item) || isObject(item)) { | |
| return _cast(item, numbertype, context); | |
| } | |
| val[i] = numbertype.castForQuery(null, item, context); | |
| }); | |
| } else { | |
| const nearKeys = Object.keys(val); | |
| let nearLen = nearKeys.length; | |
| while (nearLen--) { | |
| const nkey = nearKeys[nearLen]; | |
| const item = val[nkey]; | |
| if (Array.isArray(item) || isObject(item)) { | |
| _cast(item, numbertype, context); | |
| val[nkey] = item; | |
| } else { | |
| val[nkey] = numbertype.castForQuery({ val: item, context: context }); | |
| } | |
| } | |
| } | |
| } | |
| function getStrictQuery(queryOptions, schemaUserProvidedOptions, schemaOptions, context) { | |
| if ('strictQuery' in queryOptions) { | |
| return queryOptions.strictQuery; | |
| } | |
| if ('strictQuery' in schemaUserProvidedOptions) { | |
| return schemaUserProvidedOptions.strictQuery; | |
| } | |
| const mongooseOptions = context && | |
| context.mongooseCollection && | |
| context.mongooseCollection.conn && | |
| context.mongooseCollection.conn.base && | |
| context.mongooseCollection.conn.base.options; | |
| if (mongooseOptions) { | |
| if ('strictQuery' in mongooseOptions) { | |
| return mongooseOptions.strictQuery; | |
| } | |
| } | |
| return schemaOptions.strictQuery; | |
| } | |