123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- 'use strict';
- const CastError = require('../../error/cast');
- const StrictModeError = require('../../error/strict');
- const castNumber = require('../../cast/number');
- const booleanComparison = new Set(['$and', '$or', '$not']);
- 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',
- '$round',
- '$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'
- ]);
- 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) {
- if (isPath(val)) {
- // Assume path
- 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) {
- val.branches.map(v => _castExpression(v, schema, strictQuery));
- val.default = _castExpression(val.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);
- }
- }
- if (val.$in) {
- val.$in = castIn(val.$in, schema, strictQuery);
- }
- if (val.$size) {
- val.$size = castNumberOperator(val.$size, schema, strictQuery);
- }
- _omitUndefined(val);
- return val;
- }
- function _omitUndefined(val) {
- const keys = Object.keys(val);
- for (const key of keys) {
- if (val[key] === void 0) {
- delete val[key];
- }
- }
- }
- // { $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) {
- let search = val[0];
- let path = val[1];
- if (!isPath(path)) {
- return val;
- }
- path = path.slice(1);
- const schematype = schema.path(path);
- 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');
- }
- if (schematype.$isMongooseDocumentArray) {
- search = schematype.$embeddedSchemaType.cast(search);
- } else {
- search = schematype.caster.cast(search);
- }
- return [search, val[1]];
- }
- // { $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.startsWith('$');
- }
- function isLiteral(val) {
- if (typeof val === 'string' && val.startsWith('$')) {
- return false;
- }
- if (typeof val === 'object' && val != null && Object.keys(val).find(key => key.startsWith('$'))) {
- // The `$literal` expression can make an object a literal
- // https://docs.mongodb.com/manual/reference/operator/aggregation/literal/#mongodb-expression-exp.-literal
- return val.$literal != null;
- }
- return true;
- }
|