cast$expr.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. 'use strict';
  2. const CastError = require('../../error/cast');
  3. const StrictModeError = require('../../error/strict');
  4. const castNumber = require('../../cast/number');
  5. const omitUndefined = require('../omitUndefined');
  6. const booleanComparison = new Set(['$and', '$or']);
  7. const comparisonOperator = new Set(['$cmp', '$eq', '$lt', '$lte', '$gt', '$gte']);
  8. const arithmeticOperatorArray = new Set([
  9. // avoid casting '$add' or '$subtract', because expressions can be either number or date,
  10. // and we don't have a good way of inferring which arguments should be numbers and which should
  11. // be dates.
  12. '$multiply',
  13. '$divide',
  14. '$log',
  15. '$mod',
  16. '$trunc',
  17. '$avg',
  18. '$max',
  19. '$min',
  20. '$stdDevPop',
  21. '$stdDevSamp',
  22. '$sum'
  23. ]);
  24. const arithmeticOperatorNumber = new Set([
  25. '$abs',
  26. '$exp',
  27. '$ceil',
  28. '$floor',
  29. '$ln',
  30. '$log10',
  31. '$sqrt',
  32. '$sin',
  33. '$cos',
  34. '$tan',
  35. '$asin',
  36. '$acos',
  37. '$atan',
  38. '$atan2',
  39. '$asinh',
  40. '$acosh',
  41. '$atanh',
  42. '$sinh',
  43. '$cosh',
  44. '$tanh',
  45. '$degreesToRadians',
  46. '$radiansToDegrees'
  47. ]);
  48. const arrayElementOperators = new Set([
  49. '$arrayElemAt',
  50. '$first',
  51. '$last'
  52. ]);
  53. const dateOperators = new Set([
  54. '$year',
  55. '$month',
  56. '$week',
  57. '$dayOfMonth',
  58. '$dayOfYear',
  59. '$hour',
  60. '$minute',
  61. '$second',
  62. '$isoDayOfWeek',
  63. '$isoWeekYear',
  64. '$isoWeek',
  65. '$millisecond'
  66. ]);
  67. const expressionOperator = new Set(['$not']);
  68. module.exports = function cast$expr(val, schema, strictQuery) {
  69. if (typeof val === 'boolean') {
  70. return val;
  71. }
  72. if (typeof val !== 'object' || val === null) {
  73. throw new Error('`$expr` must be an object or boolean literal');
  74. }
  75. return _castExpression(val, schema, strictQuery);
  76. };
  77. function _castExpression(val, schema, strictQuery) {
  78. // Preserve the value if it represents a path or if it's null
  79. if (isPath(val) || val === null) {
  80. return val;
  81. }
  82. if (val.$cond != null) {
  83. if (Array.isArray(val.$cond)) {
  84. val.$cond = val.$cond.map(expr => _castExpression(expr, schema, strictQuery));
  85. } else {
  86. val.$cond.if = _castExpression(val.$cond.if, schema, strictQuery);
  87. val.$cond.then = _castExpression(val.$cond.then, schema, strictQuery);
  88. val.$cond.else = _castExpression(val.$cond.else, schema, strictQuery);
  89. }
  90. } else if (val.$ifNull != null) {
  91. val.$ifNull.map(v => _castExpression(v, schema, strictQuery));
  92. } else if (val.$switch != null) {
  93. if (Array.isArray(val.$switch.branches)) {
  94. val.$switch.branches = val.$switch.branches.map(v => _castExpression(v, schema, strictQuery));
  95. }
  96. if ('default' in val.$switch) {
  97. val.$switch.default = _castExpression(val.$switch.default, schema, strictQuery);
  98. }
  99. }
  100. const keys = Object.keys(val);
  101. for (const key of keys) {
  102. if (booleanComparison.has(key)) {
  103. val[key] = val[key].map(v => _castExpression(v, schema, strictQuery));
  104. } else if (comparisonOperator.has(key)) {
  105. val[key] = castComparison(val[key], schema, strictQuery);
  106. } else if (arithmeticOperatorArray.has(key)) {
  107. val[key] = castArithmetic(val[key], schema, strictQuery);
  108. } else if (arithmeticOperatorNumber.has(key)) {
  109. val[key] = castNumberOperator(val[key], schema, strictQuery);
  110. } else if (expressionOperator.has(key)) {
  111. val[key] = _castExpression(val[key], schema, strictQuery);
  112. }
  113. }
  114. if (val.$in) {
  115. val.$in = castIn(val.$in, schema, strictQuery);
  116. }
  117. if (val.$size) {
  118. val.$size = castNumberOperator(val.$size, schema, strictQuery);
  119. }
  120. if (val.$round) {
  121. const $round = val.$round;
  122. if (!Array.isArray($round) || $round.length < 1 || $round.length > 2) {
  123. throw new CastError('Array', $round, '$round');
  124. }
  125. val.$round = $round.map(v => castNumberOperator(v, schema, strictQuery));
  126. }
  127. omitUndefined(val);
  128. return val;
  129. }
  130. // { $op: <number> }
  131. function castNumberOperator(val) {
  132. if (!isLiteral(val)) {
  133. return val;
  134. }
  135. try {
  136. return castNumber(val);
  137. } catch {
  138. throw new CastError('Number', val);
  139. }
  140. }
  141. function castIn(val, schema, strictQuery) {
  142. const path = val[1];
  143. if (!isPath(path)) {
  144. return val;
  145. }
  146. const search = val[0];
  147. const schematype = schema.path(path.slice(1));
  148. if (schematype === null) {
  149. if (strictQuery === false) {
  150. return val;
  151. } else if (strictQuery === 'throw') {
  152. throw new StrictModeError('$in');
  153. }
  154. return void 0;
  155. }
  156. if (!schematype.$isMongooseArray) {
  157. throw new Error('Path must be an array for $in');
  158. }
  159. return [
  160. schematype.embeddedSchemaType.cast(search),
  161. path
  162. ];
  163. }
  164. // { $op: [<number>, <number>] }
  165. function castArithmetic(val) {
  166. if (!Array.isArray(val)) {
  167. if (!isLiteral(val)) {
  168. return val;
  169. }
  170. try {
  171. return castNumber(val);
  172. } catch {
  173. throw new CastError('Number', val);
  174. }
  175. }
  176. return val.map(v => {
  177. if (!isLiteral(v)) {
  178. return v;
  179. }
  180. try {
  181. return castNumber(v);
  182. } catch {
  183. throw new CastError('Number', v);
  184. }
  185. });
  186. }
  187. // { $op: [expression, expression] }
  188. function castComparison(val, schema, strictQuery) {
  189. if (!Array.isArray(val) || val.length !== 2) {
  190. throw new Error('Comparison operator must be an array of length 2');
  191. }
  192. val[0] = _castExpression(val[0], schema, strictQuery);
  193. const lhs = val[0];
  194. if (isLiteral(val[1])) {
  195. let path = null;
  196. let schematype = null;
  197. let caster = null;
  198. if (isPath(lhs)) {
  199. path = lhs.slice(1);
  200. schematype = schema.path(path);
  201. } else if (typeof lhs === 'object' && lhs != null) {
  202. for (const key of Object.keys(lhs)) {
  203. if (dateOperators.has(key) && isPath(lhs[key])) {
  204. path = lhs[key].slice(1) + '.' + key;
  205. caster = castNumber;
  206. } else if (arrayElementOperators.has(key) && isPath(lhs[key])) {
  207. path = lhs[key].slice(1) + '.' + key;
  208. schematype = schema.path(lhs[key].slice(1));
  209. if (schematype != null) {
  210. if (schematype.$isMongooseArray) {
  211. schematype = schematype.embeddedSchemaType;
  212. }
  213. }
  214. }
  215. }
  216. }
  217. const is$literal = typeof val[1] === 'object' && val[1] != null && val[1].$literal != null;
  218. if (schematype != null) {
  219. if (is$literal) {
  220. val[1] = { $literal: schematype.cast(val[1].$literal) };
  221. } else {
  222. val[1] = schematype.cast(val[1]);
  223. }
  224. } else if (caster != null) {
  225. if (is$literal) {
  226. try {
  227. val[1] = { $literal: caster(val[1].$literal) };
  228. } catch {
  229. throw new CastError(caster.name.replace(/^cast/, ''), val[1], path + '.$literal');
  230. }
  231. } else {
  232. try {
  233. val[1] = caster(val[1]);
  234. } catch {
  235. throw new CastError(caster.name.replace(/^cast/, ''), val[1], path);
  236. }
  237. }
  238. } else if (path != null && strictQuery === true) {
  239. return void 0;
  240. } else if (path != null && strictQuery === 'throw') {
  241. throw new StrictModeError(path);
  242. }
  243. } else {
  244. val[1] = _castExpression(val[1]);
  245. }
  246. return val;
  247. }
  248. function isPath(val) {
  249. return typeof val === 'string' && val[0] === '$';
  250. }
  251. function isLiteral(val) {
  252. if (typeof val === 'string' && val[0] === '$') {
  253. return false;
  254. }
  255. if (typeof val === 'object' && val !== null && Object.keys(val).find(key => key[0] === '$')) {
  256. // The `$literal` expression can make an object a literal
  257. // https://www.mongodb.com/docs/manual/reference/operator/aggregation/literal/#mongodb-expression-exp.-literal
  258. return val.$literal != null;
  259. }
  260. return true;
  261. }