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.
444 lines
15 KiB
444 lines
15 KiB
'use strict'; |
|
|
|
/*! |
|
* 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; |
|
}
|
|
|