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.
286 lines
7.5 KiB
286 lines
7.5 KiB
'use strict'; |
|
|
|
const CastError = require('../../error/cast'); |
|
const StrictModeError = require('../../error/strict'); |
|
const castNumber = require('../../cast/number'); |
|
const omitUndefined = require('../omitUndefined'); |
|
|
|
const booleanComparison = new Set(['$and', '$or']); |
|
const comparisonOperator = new Set(['$cmp', '$eq', '$lt', '$lte', '$gt', '$gte']); |
|
const arithmeticOperatorArray = new Set([ |
|
// avoid casting '$add' or '$subtract', because expressions can be either number or date, |
|
// and we don't have a good way of inferring which arguments should be numbers and which should |
|
// be dates. |
|
'$multiply', |
|
'$divide', |
|
'$log', |
|
'$mod', |
|
'$trunc', |
|
'$avg', |
|
'$max', |
|
'$min', |
|
'$stdDevPop', |
|
'$stdDevSamp', |
|
'$sum' |
|
]); |
|
const arithmeticOperatorNumber = new Set([ |
|
'$abs', |
|
'$exp', |
|
'$ceil', |
|
'$floor', |
|
'$ln', |
|
'$log10', |
|
'$sqrt', |
|
'$sin', |
|
'$cos', |
|
'$tan', |
|
'$asin', |
|
'$acos', |
|
'$atan', |
|
'$atan2', |
|
'$asinh', |
|
'$acosh', |
|
'$atanh', |
|
'$sinh', |
|
'$cosh', |
|
'$tanh', |
|
'$degreesToRadians', |
|
'$radiansToDegrees' |
|
]); |
|
const arrayElementOperators = new Set([ |
|
'$arrayElemAt', |
|
'$first', |
|
'$last' |
|
]); |
|
const dateOperators = new Set([ |
|
'$year', |
|
'$month', |
|
'$week', |
|
'$dayOfMonth', |
|
'$dayOfYear', |
|
'$hour', |
|
'$minute', |
|
'$second', |
|
'$isoDayOfWeek', |
|
'$isoWeekYear', |
|
'$isoWeek', |
|
'$millisecond' |
|
]); |
|
const expressionOperator = new Set(['$not']); |
|
|
|
module.exports = function cast$expr(val, schema, strictQuery) { |
|
if (typeof val !== 'object' || val === null) { |
|
throw new Error('`$expr` must be an object'); |
|
} |
|
|
|
return _castExpression(val, schema, strictQuery); |
|
}; |
|
|
|
function _castExpression(val, schema, strictQuery) { |
|
// Preserve the value if it represents a path or if it's null |
|
if (isPath(val) || val === null) { |
|
return val; |
|
} |
|
|
|
if (val.$cond != null) { |
|
if (Array.isArray(val.$cond)) { |
|
val.$cond = val.$cond.map(expr => _castExpression(expr, schema, strictQuery)); |
|
} else { |
|
val.$cond.if = _castExpression(val.$cond.if, schema, strictQuery); |
|
val.$cond.then = _castExpression(val.$cond.then, schema, strictQuery); |
|
val.$cond.else = _castExpression(val.$cond.else, schema, strictQuery); |
|
} |
|
} else if (val.$ifNull != null) { |
|
val.$ifNull.map(v => _castExpression(v, schema, strictQuery)); |
|
} else if (val.$switch != null) { |
|
if (Array.isArray(val.$switch.branches)) { |
|
val.$switch.branches = val.$switch.branches.map(v => _castExpression(v, schema, strictQuery)); |
|
} |
|
if ('default' in val.$switch) { |
|
val.$switch.default = _castExpression(val.$switch.default, schema, strictQuery); |
|
} |
|
} |
|
|
|
const keys = Object.keys(val); |
|
for (const key of keys) { |
|
if (booleanComparison.has(key)) { |
|
val[key] = val[key].map(v => _castExpression(v, schema, strictQuery)); |
|
} else if (comparisonOperator.has(key)) { |
|
val[key] = castComparison(val[key], schema, strictQuery); |
|
} else if (arithmeticOperatorArray.has(key)) { |
|
val[key] = castArithmetic(val[key], schema, strictQuery); |
|
} else if (arithmeticOperatorNumber.has(key)) { |
|
val[key] = castNumberOperator(val[key], schema, strictQuery); |
|
} else if (expressionOperator.has(key)) { |
|
val[key] = _castExpression(val[key], schema, strictQuery); |
|
} |
|
} |
|
|
|
if (val.$in) { |
|
val.$in = castIn(val.$in, schema, strictQuery); |
|
} |
|
if (val.$size) { |
|
val.$size = castNumberOperator(val.$size, schema, strictQuery); |
|
} |
|
if (val.$round) { |
|
const $round = val.$round; |
|
if (!Array.isArray($round) || $round.length < 1 || $round.length > 2) { |
|
throw new CastError('Array', $round, '$round'); |
|
} |
|
val.$round = $round.map(v => castNumberOperator(v, schema, strictQuery)); |
|
} |
|
|
|
omitUndefined(val); |
|
|
|
return val; |
|
} |
|
|
|
// { $op: <number> } |
|
function castNumberOperator(val) { |
|
if (!isLiteral(val)) { |
|
return val; |
|
} |
|
|
|
try { |
|
return castNumber(val); |
|
} catch (err) { |
|
throw new CastError('Number', val); |
|
} |
|
} |
|
|
|
function castIn(val, schema, strictQuery) { |
|
const path = val[1]; |
|
if (!isPath(path)) { |
|
return val; |
|
} |
|
const search = val[0]; |
|
|
|
const schematype = schema.path(path.slice(1)); |
|
if (schematype === null) { |
|
if (strictQuery === false) { |
|
return val; |
|
} else if (strictQuery === 'throw') { |
|
throw new StrictModeError('$in'); |
|
} |
|
|
|
return void 0; |
|
} |
|
|
|
if (!schematype.$isMongooseArray) { |
|
throw new Error('Path must be an array for $in'); |
|
} |
|
|
|
return [ |
|
schematype.$isMongooseDocumentArray ? schematype.$embeddedSchemaType.cast(search) : schematype.caster.cast(search), |
|
path |
|
]; |
|
} |
|
|
|
// { $op: [<number>, <number>] } |
|
function castArithmetic(val) { |
|
if (!Array.isArray(val)) { |
|
if (!isLiteral(val)) { |
|
return val; |
|
} |
|
try { |
|
return castNumber(val); |
|
} catch (err) { |
|
throw new CastError('Number', val); |
|
} |
|
} |
|
|
|
return val.map(v => { |
|
if (!isLiteral(v)) { |
|
return v; |
|
} |
|
try { |
|
return castNumber(v); |
|
} catch (err) { |
|
throw new CastError('Number', v); |
|
} |
|
}); |
|
} |
|
|
|
// { $op: [expression, expression] } |
|
function castComparison(val, schema, strictQuery) { |
|
if (!Array.isArray(val) || val.length !== 2) { |
|
throw new Error('Comparison operator must be an array of length 2'); |
|
} |
|
|
|
val[0] = _castExpression(val[0], schema, strictQuery); |
|
const lhs = val[0]; |
|
|
|
if (isLiteral(val[1])) { |
|
let path = null; |
|
let schematype = null; |
|
let caster = null; |
|
if (isPath(lhs)) { |
|
path = lhs.slice(1); |
|
schematype = schema.path(path); |
|
} else if (typeof lhs === 'object' && lhs != null) { |
|
for (const key of Object.keys(lhs)) { |
|
if (dateOperators.has(key) && isPath(lhs[key])) { |
|
path = lhs[key].slice(1) + '.' + key; |
|
caster = castNumber; |
|
} else if (arrayElementOperators.has(key) && isPath(lhs[key])) { |
|
path = lhs[key].slice(1) + '.' + key; |
|
schematype = schema.path(lhs[key].slice(1)); |
|
if (schematype != null) { |
|
if (schematype.$isMongooseDocumentArray) { |
|
schematype = schematype.$embeddedSchemaType; |
|
} else if (schematype.$isMongooseArray) { |
|
schematype = schematype.caster; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
const is$literal = typeof val[1] === 'object' && val[1] != null && val[1].$literal != null; |
|
if (schematype != null) { |
|
if (is$literal) { |
|
val[1] = { $literal: schematype.cast(val[1].$literal) }; |
|
} else { |
|
val[1] = schematype.cast(val[1]); |
|
} |
|
} else if (caster != null) { |
|
if (is$literal) { |
|
try { |
|
val[1] = { $literal: caster(val[1].$literal) }; |
|
} catch (err) { |
|
throw new CastError(caster.name.replace(/^cast/, ''), val[1], path + '.$literal'); |
|
} |
|
} else { |
|
try { |
|
val[1] = caster(val[1]); |
|
} catch (err) { |
|
throw new CastError(caster.name.replace(/^cast/, ''), val[1], path); |
|
} |
|
} |
|
} else if (path != null && strictQuery === true) { |
|
return void 0; |
|
} else if (path != null && strictQuery === 'throw') { |
|
throw new StrictModeError(path); |
|
} |
|
} else { |
|
val[1] = _castExpression(val[1]); |
|
} |
|
|
|
return val; |
|
} |
|
|
|
function isPath(val) { |
|
return typeof val === 'string' && val[0] === '$'; |
|
} |
|
|
|
function isLiteral(val) { |
|
if (typeof val === 'string' && val[0] === '$') { |
|
return false; |
|
} |
|
if (typeof val === 'object' && val !== null && Object.keys(val).find(key => key[0] === '$')) { |
|
// The `$literal` expression can make an object a literal |
|
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/literal/#mongodb-expression-exp.-literal |
|
return val.$literal != null; |
|
} |
|
return true; |
|
}
|
|
|