castUpdate.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. 'use strict';
  2. var StrictModeError = require('../../error/strict');
  3. var ValidationError = require('../../error/validation');
  4. var utils = require('../../utils');
  5. /*!
  6. * Casts an update op based on the given schema
  7. *
  8. * @param {Schema} schema
  9. * @param {Object} obj
  10. * @param {Object} options
  11. * @param {Boolean} [options.overwrite] defaults to false
  12. * @param {Boolean|String} [options.strict] defaults to true
  13. * @param {Query} context passed to setters
  14. * @return {Boolean} true iff the update is non-empty
  15. */
  16. module.exports = function castUpdate(schema, obj, options, context) {
  17. if (!obj) {
  18. return undefined;
  19. }
  20. var ops = Object.keys(obj);
  21. var i = ops.length;
  22. var ret = {};
  23. var hasKeys;
  24. var val;
  25. var hasDollarKey = false;
  26. var overwrite = options.overwrite;
  27. while (i--) {
  28. var op = ops[i];
  29. // if overwrite is set, don't do any of the special $set stuff
  30. if (op[0] !== '$' && !overwrite) {
  31. // fix up $set sugar
  32. if (!ret.$set) {
  33. if (obj.$set) {
  34. ret.$set = obj.$set;
  35. } else {
  36. ret.$set = {};
  37. }
  38. }
  39. ret.$set[op] = obj[op];
  40. ops.splice(i, 1);
  41. if (!~ops.indexOf('$set')) ops.push('$set');
  42. } else if (op === '$set') {
  43. if (!ret.$set) {
  44. ret[op] = obj[op];
  45. }
  46. } else {
  47. ret[op] = obj[op];
  48. }
  49. }
  50. // cast each value
  51. i = ops.length;
  52. // if we get passed {} for the update, we still need to respect that when it
  53. // is an overwrite scenario
  54. if (overwrite) {
  55. hasKeys = true;
  56. }
  57. while (i--) {
  58. op = ops[i];
  59. val = ret[op];
  60. hasDollarKey = hasDollarKey || op.charAt(0) === '$';
  61. if (val &&
  62. typeof val === 'object' &&
  63. (!overwrite || hasDollarKey)) {
  64. hasKeys |= walkUpdatePath(schema, val, op, options.strict, context);
  65. } else if (overwrite && ret && typeof ret === 'object') {
  66. // if we are just using overwrite, cast the query and then we will
  67. // *always* return the value, even if it is an empty object. We need to
  68. // set hasKeys above because we need to account for the case where the
  69. // user passes {} and wants to clobber the whole document
  70. // Also, _walkUpdatePath expects an operation, so give it $set since that
  71. // is basically what we're doing
  72. walkUpdatePath(schema, ret, '$set', options.strict, context);
  73. } else {
  74. var msg = 'Invalid atomic update value for ' + op + '. '
  75. + 'Expected an object, received ' + typeof val;
  76. throw new Error(msg);
  77. }
  78. }
  79. return hasKeys && ret;
  80. };
  81. /*!
  82. * Walk each path of obj and cast its values
  83. * according to its schema.
  84. *
  85. * @param {Schema} schema
  86. * @param {Object} obj - part of a query
  87. * @param {String} op - the atomic operator ($pull, $set, etc)
  88. * @param {Boolean|String} strict
  89. * @param {Query} context
  90. * @param {String} pref - path prefix (internal only)
  91. * @return {Bool} true if this path has keys to update
  92. * @api private
  93. */
  94. function walkUpdatePath(schema, obj, op, strict, context, pref) {
  95. var prefix = pref ? pref + '.' : '';
  96. var keys = Object.keys(obj);
  97. var i = keys.length;
  98. var hasKeys = false;
  99. var schematype;
  100. var key;
  101. var val;
  102. var hasError = false;
  103. var aggregatedError = new ValidationError();
  104. var useNestedStrict = schema.options.useNestedStrict;
  105. while (i--) {
  106. key = keys[i];
  107. val = obj[key];
  108. if (val && val.constructor.name === 'Object') {
  109. // watch for embedded doc schemas
  110. schematype = schema._getSchema(prefix + key);
  111. if (schematype && schematype.caster && op in castOps) {
  112. // embedded doc schema
  113. hasKeys = true;
  114. if ('$each' in val) {
  115. try {
  116. obj[key] = {
  117. $each: castUpdateVal(schematype, val.$each, op, context)
  118. };
  119. } catch (error) {
  120. hasError = true;
  121. _handleCastError(error, context, key, aggregatedError);
  122. }
  123. if (val.$slice != null) {
  124. obj[key].$slice = val.$slice | 0;
  125. }
  126. if (val.$sort) {
  127. obj[key].$sort = val.$sort;
  128. }
  129. if (!!val.$position || val.$position === 0) {
  130. obj[key].$position = val.$position;
  131. }
  132. } else {
  133. try {
  134. obj[key] = castUpdateVal(schematype, val, op, context);
  135. } catch (error) {
  136. hasError = true;
  137. _handleCastError(error, context, key, aggregatedError);
  138. }
  139. }
  140. } else if ((op === '$currentDate') || (op in castOps && schematype)) {
  141. // $currentDate can take an object
  142. try {
  143. obj[key] = castUpdateVal(schematype, val, op, context);
  144. } catch (error) {
  145. hasError = true;
  146. _handleCastError(error, context, key, aggregatedError);
  147. }
  148. hasKeys = true;
  149. } else {
  150. var pathToCheck = (prefix + key);
  151. var v = schema._getPathType(pathToCheck);
  152. var _strict = strict;
  153. if (useNestedStrict &&
  154. v &&
  155. v.schema &&
  156. 'strict' in v.schema.options) {
  157. _strict = v.schema.options.strict;
  158. }
  159. if (v.pathType === 'undefined') {
  160. if (_strict === 'throw') {
  161. throw new StrictModeError(pathToCheck);
  162. } else if (_strict) {
  163. delete obj[key];
  164. continue;
  165. }
  166. }
  167. // gh-2314
  168. // we should be able to set a schema-less field
  169. // to an empty object literal
  170. hasKeys |= walkUpdatePath(schema, val, op, strict, context, prefix + key) ||
  171. (utils.isObject(val) && Object.keys(val).length === 0);
  172. }
  173. } else {
  174. var checkPath = (key === '$each' || key === '$or' || key === '$and' || key === '$in') ?
  175. pref : prefix + key;
  176. schematype = schema._getSchema(checkPath);
  177. var pathDetails = schema._getPathType(checkPath);
  178. var isStrict = strict;
  179. if (useNestedStrict &&
  180. pathDetails &&
  181. pathDetails.schema &&
  182. 'strict' in pathDetails.schema.options) {
  183. isStrict = pathDetails.schema.options.strict;
  184. }
  185. var skip = isStrict &&
  186. !schematype &&
  187. !/real|nested/.test(pathDetails.pathType);
  188. if (skip) {
  189. if (isStrict === 'throw') {
  190. throw new StrictModeError(prefix + key);
  191. } else {
  192. delete obj[key];
  193. }
  194. } else {
  195. // gh-1845 temporary fix: ignore $rename. See gh-3027 for tracking
  196. // improving this.
  197. if (op === '$rename') {
  198. hasKeys = true;
  199. continue;
  200. }
  201. hasKeys = true;
  202. try {
  203. obj[key] = castUpdateVal(schematype, val, op, key, context);
  204. } catch (error) {
  205. hasError = true;
  206. _handleCastError(error, context, key, aggregatedError);
  207. }
  208. }
  209. }
  210. }
  211. if (hasError) {
  212. throw aggregatedError;
  213. }
  214. return hasKeys;
  215. }
  216. /*!
  217. * ignore
  218. */
  219. function _handleCastError(error, query, key, aggregatedError) {
  220. if (typeof query !== 'object' || !query.options.multipleCastError) {
  221. throw error;
  222. }
  223. aggregatedError.addError(key, error);
  224. }
  225. /*!
  226. * These operators should be cast to numbers instead
  227. * of their path schema type.
  228. */
  229. var numberOps = {
  230. $pop: 1,
  231. $unset: 1,
  232. $inc: 1
  233. };
  234. /*!
  235. * These operators require casting docs
  236. * to real Documents for Update operations.
  237. */
  238. var castOps = {
  239. $push: 1,
  240. $pushAll: 1,
  241. $addToSet: 1,
  242. $set: 1,
  243. $setOnInsert: 1
  244. };
  245. /*!
  246. * ignore
  247. */
  248. var overwriteOps = {
  249. $set: 1,
  250. $setOnInsert: 1
  251. };
  252. /*!
  253. * Casts `val` according to `schema` and atomic `op`.
  254. *
  255. * @param {SchemaType} schema
  256. * @param {Object} val
  257. * @param {String} op - the atomic operator ($pull, $set, etc)
  258. * @param {String} $conditional
  259. * @param {Query} context
  260. * @api private
  261. */
  262. function castUpdateVal(schema, val, op, $conditional, context) {
  263. if (!schema) {
  264. // non-existing schema path
  265. return op in numberOps
  266. ? Number(val)
  267. : val;
  268. }
  269. var cond = schema.caster && op in castOps &&
  270. (utils.isObject(val) || Array.isArray(val));
  271. if (cond) {
  272. // Cast values for ops that add data to MongoDB.
  273. // Ensures embedded documents get ObjectIds etc.
  274. var tmp = schema.cast(val);
  275. if (Array.isArray(val)) {
  276. val = tmp;
  277. } else if (Array.isArray(tmp)) {
  278. val = tmp[0];
  279. } else {
  280. val = tmp;
  281. }
  282. return val;
  283. }
  284. if (op in numberOps) {
  285. if (op === '$inc') {
  286. return schema.castForQueryWrapper({ val: val, context: context });
  287. }
  288. return Number(val);
  289. }
  290. if (op === '$currentDate') {
  291. if (typeof val === 'object') {
  292. return {$type: val.$type};
  293. }
  294. return Boolean(val);
  295. }
  296. if (/^\$/.test($conditional)) {
  297. return schema.castForQueryWrapper({
  298. $conditional: $conditional,
  299. val: val,
  300. context: context
  301. });
  302. }
  303. if (overwriteOps[op]) {
  304. return schema.castForQueryWrapper({
  305. val: val,
  306. context: context,
  307. $skipQueryCastForUpdate: val != null && schema.$isMongooseArray
  308. });
  309. }
  310. return schema.castForQueryWrapper({ val: val, context: context });
  311. }