cast$expr.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. 'use strict';
  2. const CastError = require('../../error/cast');
  3. const StrictModeError = require('../../error/strict');
  4. const castNumber = require('../../cast/number');
  5. const booleanComparison = new Set(['$and', '$or']);
  6. const comparisonOperator = new Set(['$cmp', '$eq', '$lt', '$lte', '$gt', '$gte']);
  7. const arithmeticOperatorArray = new Set([
  8. // avoid casting '$add' or '$subtract', because expressions can be either number or date,
  9. // and we don't have a good way of inferring which arguments should be numbers and which should
  10. // be dates.
  11. '$multiply',
  12. '$divide',
  13. '$log',
  14. '$mod',
  15. '$trunc',
  16. '$avg',
  17. '$max',
  18. '$min',
  19. '$stdDevPop',
  20. '$stdDevSamp',
  21. '$sum'
  22. ]);
  23. const arithmeticOperatorNumber = new Set([
  24. '$abs',
  25. '$exp',
  26. '$ceil',
  27. '$floor',
  28. '$ln',
  29. '$log10',
  30. '$round',
  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 !== 'object' || val === null) {
  70. throw new Error('`$expr` must be an object');
  71. }
  72. return _castExpression(val, schema, strictQuery);
  73. };
  74. function _castExpression(val, schema, strictQuery) {
  75. if (isPath(val)) {
  76. // Assume path
  77. return val;
  78. }
  79. if (val.$cond != null) {
  80. if (Array.isArray(val.$cond)) {
  81. val.$cond = val.$cond.map(expr => _castExpression(expr, schema, strictQuery));
  82. } else {
  83. val.$cond.if = _castExpression(val.$cond.if, schema, strictQuery);
  84. val.$cond.then = _castExpression(val.$cond.then, schema, strictQuery);
  85. val.$cond.else = _castExpression(val.$cond.else, schema, strictQuery);
  86. }
  87. } else if (val.$ifNull != null) {
  88. val.$ifNull.map(v => _castExpression(v, schema, strictQuery));
  89. } else if (val.$switch != null) {
  90. val.branches.map(v => _castExpression(v, schema, strictQuery));
  91. val.default = _castExpression(val.default, schema, strictQuery);
  92. }
  93. const keys = Object.keys(val);
  94. for (const key of keys) {
  95. if (booleanComparison.has(key)) {
  96. val[key] = val[key].map(v => _castExpression(v, schema, strictQuery));
  97. } else if (comparisonOperator.has(key)) {
  98. val[key] = castComparison(val[key], schema, strictQuery);
  99. } else if (arithmeticOperatorArray.has(key)) {
  100. val[key] = castArithmetic(val[key], schema, strictQuery);
  101. } else if (arithmeticOperatorNumber.has(key)) {
  102. val[key] = castNumberOperator(val[key], schema, strictQuery);
  103. } else if (expressionOperator.has(key)) {
  104. val[key] = _castExpression(val[key], schema, strictQuery);
  105. }
  106. }
  107. if (val.$in) {
  108. val.$in = castIn(val.$in, schema, strictQuery);
  109. }
  110. if (val.$size) {
  111. val.$size = castNumberOperator(val.$size, schema, strictQuery);
  112. }
  113. _omitUndefined(val);
  114. return val;
  115. }
  116. function _omitUndefined(val) {
  117. const keys = Object.keys(val);
  118. for (let i = 0, len = keys.length; i < len; ++i) {
  119. (val[keys[i]] === void 0) && delete val[keys[i]];
  120. }
  121. }
  122. // { $op: <number> }
  123. function castNumberOperator(val) {
  124. if (!isLiteral(val)) {
  125. return val;
  126. }
  127. try {
  128. return castNumber(val);
  129. } catch (err) {
  130. throw new CastError('Number', val);
  131. }
  132. }
  133. function castIn(val, schema, strictQuery) {
  134. const path = val[1];
  135. if (!isPath(path)) {
  136. return val;
  137. }
  138. const search = val[0];
  139. const schematype = schema.path(path.slice(1));
  140. if (schematype === null) {
  141. if (strictQuery === false) {
  142. return val;
  143. } else if (strictQuery === 'throw') {
  144. throw new StrictModeError('$in');
  145. }
  146. return void 0;
  147. }
  148. if (!schematype.$isMongooseArray) {
  149. throw new Error('Path must be an array for $in');
  150. }
  151. return [
  152. schematype.$isMongooseDocumentArray ? schematype.$embeddedSchemaType.cast(search) : schematype.caster.cast(search),
  153. path
  154. ];
  155. }
  156. // { $op: [<number>, <number>] }
  157. function castArithmetic(val) {
  158. if (!Array.isArray(val)) {
  159. if (!isLiteral(val)) {
  160. return val;
  161. }
  162. try {
  163. return castNumber(val);
  164. } catch (err) {
  165. throw new CastError('Number', val);
  166. }
  167. }
  168. return val.map(v => {
  169. if (!isLiteral(v)) {
  170. return v;
  171. }
  172. try {
  173. return castNumber(v);
  174. } catch (err) {
  175. throw new CastError('Number', v);
  176. }
  177. });
  178. }
  179. // { $op: [expression, expression] }
  180. function castComparison(val, schema, strictQuery) {
  181. if (!Array.isArray(val) || val.length !== 2) {
  182. throw new Error('Comparison operator must be an array of length 2');
  183. }
  184. val[0] = _castExpression(val[0], schema, strictQuery);
  185. const lhs = val[0];
  186. if (isLiteral(val[1])) {
  187. let path = null;
  188. let schematype = null;
  189. let caster = null;
  190. if (isPath(lhs)) {
  191. path = lhs.slice(1);
  192. schematype = schema.path(path);
  193. } else if (typeof lhs === 'object' && lhs != null) {
  194. for (const key of Object.keys(lhs)) {
  195. if (dateOperators.has(key) && isPath(lhs[key])) {
  196. path = lhs[key].slice(1) + '.' + key;
  197. caster = castNumber;
  198. } else if (arrayElementOperators.has(key) && isPath(lhs[key])) {
  199. path = lhs[key].slice(1) + '.' + key;
  200. schematype = schema.path(lhs[key].slice(1));
  201. if (schematype != null) {
  202. if (schematype.$isMongooseDocumentArray) {
  203. schematype = schematype.$embeddedSchemaType;
  204. } else if (schematype.$isMongooseArray) {
  205. schematype = schematype.caster;
  206. }
  207. }
  208. }
  209. }
  210. }
  211. const is$literal = typeof val[1] === 'object' && val[1] != null && val[1].$literal != null;
  212. if (schematype != null) {
  213. if (is$literal) {
  214. val[1] = { $literal: schematype.cast(val[1].$literal) };
  215. } else {
  216. val[1] = schematype.cast(val[1]);
  217. }
  218. } else if (caster != null) {
  219. if (is$literal) {
  220. try {
  221. val[1] = { $literal: caster(val[1].$literal) };
  222. } catch (err) {
  223. throw new CastError(caster.name.replace(/^cast/, ''), val[1], path + '.$literal');
  224. }
  225. } else {
  226. try {
  227. val[1] = caster(val[1]);
  228. } catch (err) {
  229. throw new CastError(caster.name.replace(/^cast/, ''), val[1], path);
  230. }
  231. }
  232. } else if (path != null && strictQuery === true) {
  233. return void 0;
  234. } else if (path != null && strictQuery === 'throw') {
  235. throw new StrictModeError(path);
  236. }
  237. } else {
  238. val[1] = _castExpression(val[1]);
  239. }
  240. return val;
  241. }
  242. function isPath(val) {
  243. return typeof val === 'string' && val[0] === '$';
  244. }
  245. function isLiteral(val) {
  246. if (typeof val === 'string' && val[0] === '$') {
  247. return false;
  248. }
  249. if (typeof val === 'object' && val !== null && Object.keys(val).find(key => key[0] === '$')) {
  250. // The `$literal` expression can make an object a literal
  251. // https://docs.mongodb.com/manual/reference/operator/aggregation/literal/#mongodb-expression-exp.-literal
  252. return val.$literal != null;
  253. }
  254. return true;
  255. }