updateValidators.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. 'use strict';
  2. /*!
  3. * Module dependencies.
  4. */
  5. const ValidationError = require('../error/validation');
  6. const cleanPositionalOperators = require('./schema/cleanPositionalOperators');
  7. const flatten = require('./common').flatten;
  8. const modifiedPaths = require('./common').modifiedPaths;
  9. /**
  10. * Applies validators and defaults to update and findOneAndUpdate operations,
  11. * specifically passing a null doc as `this` to validators and defaults
  12. *
  13. * @param {Query} query
  14. * @param {Schema} schema
  15. * @param {Object} castedDoc
  16. * @param {Object} options
  17. * @method runValidatorsOnUpdate
  18. * @api private
  19. */
  20. module.exports = function(query, schema, castedDoc, options, callback) {
  21. let _keys;
  22. const keys = Object.keys(castedDoc || {});
  23. let updatedKeys = {};
  24. let updatedValues = {};
  25. const isPull = {};
  26. const arrayAtomicUpdates = {};
  27. const numKeys = keys.length;
  28. let hasDollarUpdate = false;
  29. const modified = {};
  30. let currentUpdate;
  31. let key;
  32. let i;
  33. for (i = 0; i < numKeys; ++i) {
  34. if (keys[i].startsWith('$')) {
  35. hasDollarUpdate = true;
  36. if (keys[i] === '$push' || keys[i] === '$addToSet') {
  37. _keys = Object.keys(castedDoc[keys[i]]);
  38. for (let ii = 0; ii < _keys.length; ++ii) {
  39. currentUpdate = castedDoc[keys[i]][_keys[ii]];
  40. if (currentUpdate && currentUpdate.$each) {
  41. arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []).
  42. concat(currentUpdate.$each);
  43. } else {
  44. arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []).
  45. concat([currentUpdate]);
  46. }
  47. }
  48. continue;
  49. }
  50. modifiedPaths(castedDoc[keys[i]], '', modified);
  51. const flat = flatten(castedDoc[keys[i]], null, null, schema);
  52. const paths = Object.keys(flat);
  53. const numPaths = paths.length;
  54. for (let j = 0; j < numPaths; ++j) {
  55. const updatedPath = cleanPositionalOperators(paths[j]);
  56. key = keys[i];
  57. // With `$pull` we might flatten `$in`. Skip stuff nested under `$in`
  58. // for the rest of the logic, it will get handled later.
  59. if (updatedPath.includes('$')) {
  60. continue;
  61. }
  62. if (key === '$set' || key === '$setOnInsert' ||
  63. key === '$pull' || key === '$pullAll') {
  64. updatedValues[updatedPath] = flat[paths[j]];
  65. isPull[updatedPath] = key === '$pull' || key === '$pullAll';
  66. } else if (key === '$unset') {
  67. updatedValues[updatedPath] = undefined;
  68. }
  69. updatedKeys[updatedPath] = true;
  70. }
  71. }
  72. }
  73. if (!hasDollarUpdate) {
  74. modifiedPaths(castedDoc, '', modified);
  75. updatedValues = flatten(castedDoc, null, null, schema);
  76. updatedKeys = Object.keys(updatedValues);
  77. }
  78. const updates = Object.keys(updatedValues);
  79. const numUpdates = updates.length;
  80. const validatorsToExecute = [];
  81. const validationErrors = [];
  82. const alreadyValidated = [];
  83. const context = options && options.context === 'query' ? query : null;
  84. function iter(i, v) {
  85. const schemaPath = schema._getSchema(updates[i]);
  86. if (schemaPath == null) {
  87. return;
  88. }
  89. if (schemaPath.instance === 'Mixed' && schemaPath.path !== updates[i]) {
  90. return;
  91. }
  92. if (v && Array.isArray(v.$in)) {
  93. v.$in.forEach((v, i) => {
  94. validatorsToExecute.push(function(callback) {
  95. schemaPath.doValidate(
  96. v,
  97. function(err) {
  98. if (err) {
  99. err.path = updates[i] + '.$in.' + i;
  100. validationErrors.push(err);
  101. }
  102. callback(null);
  103. },
  104. context,
  105. { updateValidator: true });
  106. });
  107. });
  108. } else {
  109. if (isPull[updates[i]] &&
  110. schemaPath.$isMongooseArray) {
  111. return;
  112. }
  113. if (schemaPath.$isMongooseDocumentArrayElement && v != null && v.$__ != null) {
  114. alreadyValidated.push(updates[i]);
  115. validatorsToExecute.push(function(callback) {
  116. schemaPath.doValidate(v, function(err) {
  117. if (err) {
  118. err.path = updates[i];
  119. validationErrors.push(err);
  120. return callback(null);
  121. }
  122. v.validate(function(err) {
  123. if (err) {
  124. if (err.errors) {
  125. for (const key of Object.keys(err.errors)) {
  126. const _err = err.errors[key];
  127. _err.path = updates[i] + '.' + key;
  128. validationErrors.push(_err);
  129. }
  130. } else {
  131. err.path = updates[i];
  132. validationErrors.push(err);
  133. }
  134. }
  135. callback(null);
  136. });
  137. }, context, { updateValidator: true });
  138. });
  139. } else {
  140. validatorsToExecute.push(function(callback) {
  141. for (const path of alreadyValidated) {
  142. if (updates[i].startsWith(path + '.')) {
  143. return callback(null);
  144. }
  145. }
  146. schemaPath.doValidate(v, function(err) {
  147. if (schemaPath.schema != null &&
  148. schemaPath.schema.options.storeSubdocValidationError === false &&
  149. err instanceof ValidationError) {
  150. return callback(null);
  151. }
  152. if (err) {
  153. err.path = updates[i];
  154. validationErrors.push(err);
  155. }
  156. callback(null);
  157. }, context, { updateValidator: true });
  158. });
  159. }
  160. }
  161. }
  162. for (i = 0; i < numUpdates; ++i) {
  163. iter(i, updatedValues[updates[i]]);
  164. }
  165. const arrayUpdates = Object.keys(arrayAtomicUpdates);
  166. for (const arrayUpdate of arrayUpdates) {
  167. let schemaPath = schema._getSchema(arrayUpdate);
  168. if (schemaPath && schemaPath.$isMongooseDocumentArray) {
  169. validatorsToExecute.push(function(callback) {
  170. schemaPath.doValidate(
  171. arrayAtomicUpdates[arrayUpdate],
  172. getValidationCallback(arrayUpdate, validationErrors, callback),
  173. options && options.context === 'query' ? query : null);
  174. });
  175. } else {
  176. schemaPath = schema._getSchema(arrayUpdate + '.0');
  177. for (const atomicUpdate of arrayAtomicUpdates[arrayUpdate]) {
  178. validatorsToExecute.push(function(callback) {
  179. schemaPath.doValidate(
  180. atomicUpdate,
  181. getValidationCallback(arrayUpdate, validationErrors, callback),
  182. options && options.context === 'query' ? query : null,
  183. { updateValidator: true });
  184. });
  185. }
  186. }
  187. }
  188. if (callback != null) {
  189. let numValidators = validatorsToExecute.length;
  190. if (numValidators === 0) {
  191. return _done(callback);
  192. }
  193. for (const validator of validatorsToExecute) {
  194. validator(function() {
  195. if (--numValidators <= 0) {
  196. _done(callback);
  197. }
  198. });
  199. }
  200. return;
  201. }
  202. return function(callback) {
  203. let numValidators = validatorsToExecute.length;
  204. if (numValidators === 0) {
  205. return _done(callback);
  206. }
  207. for (const validator of validatorsToExecute) {
  208. validator(function() {
  209. if (--numValidators <= 0) {
  210. _done(callback);
  211. }
  212. });
  213. }
  214. };
  215. function _done(callback) {
  216. if (validationErrors.length) {
  217. const err = new ValidationError(null);
  218. for (const validationError of validationErrors) {
  219. err.addError(validationError.path, validationError);
  220. }
  221. return callback(err);
  222. }
  223. callback(null);
  224. }
  225. function getValidationCallback(arrayUpdate, validationErrors, callback) {
  226. return function(err) {
  227. if (err) {
  228. err.path = arrayUpdate;
  229. validationErrors.push(err);
  230. }
  231. callback(null);
  232. };
  233. }
  234. };