You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					365 lines
				
				12 KiB
			
		
		
			
		
	
	
					365 lines
				
				12 KiB
			| 
											3 years ago
										 | 'use strict'; | ||
|  | 
 | ||
|  | /*! | ||
|  |  * Module dependencies. | ||
|  |  */ | ||
|  | 
 | ||
|  | const CastError = require('./error/cast'); | ||
|  | const StrictModeError = require('./error/strict'); | ||
|  | const Types = require('./schema/index'); | ||
|  | const castTextSearch = require('./schema/operators/text'); | ||
|  | const get = require('./helpers/get'); | ||
|  | const getConstructorName = require('./helpers/getConstructorName'); | ||
|  | 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 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 {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; | ||
|  |   } | ||
|  | 
 | ||
|  |   // bson 1.x has the unfortunate tendency to remove filters that have a top-level
 | ||
|  |   // `_bsontype` property. But we should still allow ObjectIds because
 | ||
|  |   // `Collection#find()` has a special case to support `find(objectid)`.
 | ||
|  |   // Should remove this when we upgrade to bson 4.x. See gh-8222, gh-8268
 | ||
|  |   if (obj.hasOwnProperty('_bsontype') && obj._bsontype !== 'ObjectID') { | ||
|  |     delete obj._bsontype; | ||
|  |   } | ||
|  | 
 | ||
|  |   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 = 0; k < val.length; ++k) { | ||
|  |         if (val[k] == null || typeof val[k] !== 'object') { | ||
|  |           throw new CastError('Object', val[k], path + '.' + k); | ||
|  |         } | ||
|  |         val[k] = cast(schema, val[k], options, context); | ||
|  |       } | ||
|  |     } 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 === '$elemMatch') { | ||
|  |       val = cast(schema, val, options, context); | ||
|  |     } else if (path === '$text') { | ||
|  |       val = castTextSearch(val, path); | ||
|  |     } 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 = get(_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 && | ||
|  |               get(_schematype, 'schema.discriminators') != null && | ||
|  |               discriminatorKey != null && | ||
|  |               pathLastHalf !== discriminatorKey) { | ||
|  |             const discriminatorVal = get(obj, pathFirstHalf + '.' + discriminatorKey); | ||
|  |             if (discriminatorVal != null) { | ||
|  |               schematype = _schematype.schema.discriminators[discriminatorVal]. | ||
|  |                 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; | ||
|  |             obj[path] = cast(schematype.caster.schema, remainingConds, options, context)[pathLastHalf]; | ||
|  |           } 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.castForQueryWrapper({ | ||
|  |                 val: val.$maxDistance, | ||
|  |                 context: context | ||
|  |               }); | ||
|  |             } | ||
|  |             if (val.$minDistance != null) { | ||
|  |               val.$minDistance = numbertype.castForQueryWrapper({ | ||
|  |                 val: val.$minDistance, | ||
|  |                 context: 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.castForQueryWrapper({ | ||
|  |                   val: value.$maxDistance, | ||
|  |                   context: context | ||
|  |                 }); | ||
|  |               } | ||
|  |               if (value.$minDistance != null) { | ||
|  |                 value.$minDistance = numbertype.castForQueryWrapper({ | ||
|  |                   val: value.$minDistance, | ||
|  |                   context: 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; | ||
|  |         } | ||
|  |         if (options.upsert && options.strict) { | ||
|  |           if (options.strict === 'throw') { | ||
|  |             throw new StrictModeError(path); | ||
|  |           } | ||
|  |           throw new StrictModeError(path, 'Path "' + path + '" is not in ' + | ||
|  |             'schema, strict mode is `true`, and upsert is `true`.'); | ||
|  |         } else if (options.strictQuery === 'throw') { | ||
|  |           throw new StrictModeError(path, 'Path "' + path + '" is not in ' + | ||
|  |             'schema and strictQuery is \'throw\'.'); | ||
|  |         } else if (options.strictQuery) { | ||
|  |           delete obj[path]; | ||
|  |         } | ||
|  |       } else if (val == null) { | ||
|  |         continue; | ||
|  |       } else if (getConstructorName(val) === 'Object') { | ||
|  |         any$conditionals = Object.keys(val).some(isOperator); | ||
|  | 
 | ||
|  |         if (!any$conditionals) { | ||
|  |           obj[path] = schematype.castForQueryWrapper({ | ||
|  |             val: val, | ||
|  |             context: context | ||
|  |           }); | ||
|  |         } else { | ||
|  |           const ks = Object.keys(val); | ||
|  |           let $cond; | ||
|  | 
 | ||
|  |           let k = ks.length; | ||
|  | 
 | ||
|  |           while (k--) { | ||
|  |             $cond = ks[k]; | ||
|  |             nested = val[$cond]; | ||
|  | 
 | ||
|  |             if ($cond === '$not') { | ||
|  |               if (nested && schematype && !schematype.caster) { | ||
|  |                 _keys = Object.keys(nested); | ||
|  |                 if (_keys.length && isOperator(_keys[0])) { | ||
|  |                   for (const key in nested) { | ||
|  |                     nested[key] = schematype.castForQueryWrapper({ | ||
|  |                       $conditional: key, | ||
|  |                       val: nested[key], | ||
|  |                       context: context | ||
|  |                     }); | ||
|  |                   } | ||
|  |                 } else { | ||
|  |                   val[$cond] = schematype.castForQueryWrapper({ | ||
|  |                     $conditional: $cond, | ||
|  |                     val: nested, | ||
|  |                     context: context | ||
|  |                   }); | ||
|  |                 } | ||
|  |                 continue; | ||
|  |               } | ||
|  |               cast(schematype.caster ? schematype.caster.schema : schema, nested, options, context); | ||
|  |             } else { | ||
|  |               val[$cond] = schematype.castForQueryWrapper({ | ||
|  |                 $conditional: $cond, | ||
|  |                 val: nested, | ||
|  |                 context: context | ||
|  |               }); | ||
|  |             } | ||
|  |           } | ||
|  |         } | ||
|  |       } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1) { | ||
|  |         const casted = []; | ||
|  |         const valuesArray = val; | ||
|  | 
 | ||
|  |         for (const _val of valuesArray) { | ||
|  |           casted.push(schematype.castForQueryWrapper({ | ||
|  |             val: _val, | ||
|  |             context: context | ||
|  |           })); | ||
|  |         } | ||
|  | 
 | ||
|  |         obj[path] = { $in: casted }; | ||
|  |       } else { | ||
|  |         obj[path] = schematype.castForQueryWrapper({ | ||
|  |           val: val, | ||
|  |           context: 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.castForQueryWrapper({ val: item, context: 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 }); | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | } |