cast$expr.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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', '$not']);
  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. module.exports = function cast$expr(val, schema, strictQuery) {
  68. if (typeof val !== 'object' || val == null) {
  69. throw new Error('`$expr` must be an object');
  70. }
  71. return _castExpression(val, schema, strictQuery);
  72. };
  73. function _castExpression(val, schema, strictQuery) {
  74. if (isPath(val)) {
  75. // Assume path
  76. return val;
  77. }
  78. if (val.$cond != null) {
  79. if (Array.isArray(val.$cond)) {
  80. val.$cond = val.$cond.map(expr => _castExpression(expr, schema, strictQuery));
  81. } else {
  82. val.$cond.if = _castExpression(val.$cond.if, schema, strictQuery);
  83. val.$cond.then = _castExpression(val.$cond.then, schema, strictQuery);
  84. val.$cond.else = _castExpression(val.$cond.else, schema, strictQuery);
  85. }
  86. } else if (val.$ifNull != null) {
  87. val.$ifNull.map(v => _castExpression(v, schema, strictQuery));
  88. } else if (val.$switch != null) {
  89. val.branches.map(v => _castExpression(v, schema, strictQuery));
  90. val.default = _castExpression(val.default, schema, strictQuery);
  91. }
  92. const keys = Object.keys(val);
  93. for (const key of keys) {
  94. if (booleanComparison.has(key)) {
  95. val[key] = val[key].map(v => _castExpression(v, schema, strictQuery));
  96. } else if (comparisonOperator.has(key)) {
  97. val[key] = castComparison(val[key], schema, strictQuery);
  98. } else if (arithmeticOperatorArray.has(key)) {
  99. val[key] = castArithmetic(val[key], schema, strictQuery);
  100. } else if (arithmeticOperatorNumber.has(key)) {
  101. val[key] = castNumberOperator(val[key], schema, strictQuery);
  102. }
  103. }
  104. if (val.$in) {
  105. val.$in = castIn(val.$in, schema, strictQuery);
  106. }
  107. if (val.$size) {
  108. val.$size = castNumberOperator(val.$size, schema, strictQuery);
  109. }
  110. _omitUndefined(val);
  111. return val;
  112. }
  113. function _omitUndefined(val) {
  114. const keys = Object.keys(val);
  115. for (const key of keys) {
  116. if (val[key] === void 0) {
  117. delete val[key];
  118. }
  119. }
  120. }
  121. // { $op: <number> }
  122. function castNumberOperator(val) {
  123. if (!isLiteral(val)) {
  124. return val;
  125. }
  126. try {
  127. return castNumber(val);
  128. } catch (err) {
  129. throw new CastError('Number', val);
  130. }
  131. }
  132. function castIn(val, schema, strictQuery) {
  133. let search = val[0];
  134. let path = val[1];
  135. if (!isPath(path)) {
  136. return val;
  137. }
  138. path = path.slice(1);
  139. const schematype = schema.path(path);
  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. if (schematype.$isMongooseDocumentArray) {
  152. search = schematype.$embeddedSchemaType.cast(search);
  153. } else {
  154. search = schematype.caster.cast(search);
  155. }
  156. return [search, val[1]];
  157. }
  158. // { $op: [<number>, <number>] }
  159. function castArithmetic(val) {
  160. if (!Array.isArray(val)) {
  161. if (!isLiteral(val)) {
  162. return val;
  163. }
  164. try {
  165. return castNumber(val);
  166. } catch (err) {
  167. throw new CastError('Number', val);
  168. }
  169. }
  170. return val.map(v => {
  171. if (!isLiteral(v)) {
  172. return v;
  173. }
  174. try {
  175. return castNumber(v);
  176. } catch (err) {
  177. throw new CastError('Number', v);
  178. }
  179. });
  180. }
  181. // { $op: [expression, expression] }
  182. function castComparison(val, schema, strictQuery) {
  183. if (!Array.isArray(val) || val.length !== 2) {
  184. throw new Error('Comparison operator must be an array of length 2');
  185. }
  186. val[0] = _castExpression(val[0], schema, strictQuery);
  187. const lhs = val[0];
  188. if (isLiteral(val[1])) {
  189. let path = null;
  190. let schematype = null;
  191. let caster = null;
  192. if (isPath(lhs)) {
  193. path = lhs.slice(1);
  194. schematype = schema.path(path);
  195. } else if (typeof lhs === 'object' && lhs != null) {
  196. for (const key of Object.keys(lhs)) {
  197. if (dateOperators.has(key) && isPath(lhs[key])) {
  198. path = lhs[key].slice(1) + '.' + key;
  199. caster = castNumber;
  200. } else if (arrayElementOperators.has(key) && isPath(lhs[key])) {
  201. path = lhs[key].slice(1) + '.' + key;
  202. schematype = schema.path(lhs[key].slice(1));
  203. if (schematype != null) {
  204. if (schematype.$isMongooseDocumentArray) {
  205. schematype = schematype.$embeddedSchemaType;
  206. } else if (schematype.$isMongooseArray) {
  207. schematype = schematype.caster;
  208. }
  209. }
  210. }
  211. }
  212. }
  213. const is$literal = typeof val[1] === 'object' && val[1] != null && val[1].$literal != null;
  214. if (schematype != null) {
  215. if (is$literal) {
  216. val[1] = { $literal: schematype.cast(val[1].$literal) };
  217. } else {
  218. val[1] = schematype.cast(val[1]);
  219. }
  220. } else if (caster != null) {
  221. if (is$literal) {
  222. try {
  223. val[1] = { $literal: caster(val[1].$literal) };
  224. } catch (err) {
  225. throw new CastError(caster.name.replace(/^cast/, ''), val[1], path + '.$literal');
  226. }
  227. } else {
  228. try {
  229. val[1] = caster(val[1]);
  230. } catch (err) {
  231. throw new CastError(caster.name.replace(/^cast/, ''), val[1], path);
  232. }
  233. }
  234. } else if (path != null && strictQuery === true) {
  235. return void 0;
  236. } else if (path != null && strictQuery === 'throw') {
  237. throw new StrictModeError(path);
  238. }
  239. } else {
  240. val[1] = _castExpression(val[1]);
  241. }
  242. return val;
  243. }
  244. function isPath(val) {
  245. return typeof val === 'string' && val.startsWith('$');
  246. }
  247. function isLiteral(val) {
  248. if (typeof val === 'string' && val.startsWith('$')) {
  249. return false;
  250. }
  251. if (typeof val === 'object' && val != null && Object.keys(val).find(key => key.startsWith('$'))) {
  252. // The `$literal` expression can make an object a literal
  253. // https://docs.mongodb.com/manual/reference/operator/aggregation/literal/#mongodb-expression-exp.-literal
  254. return val.$literal != null;
  255. }
  256. return true;
  257. }