updateValidators.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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. /**
  9. * Applies validators and defaults to update and findOneAndUpdate operations,
  10. * specifically passing a null doc as `this` to validators and defaults
  11. *
  12. * @param {Query} query
  13. * @param {Schema} schema
  14. * @param {Object} castedDoc
  15. * @param {Object} options
  16. * @method runValidatorsOnUpdate
  17. * @api private
  18. */
  19. module.exports = async function updateValidators(query, schema, castedDoc, options) {
  20. const keys = Object.keys(castedDoc || {});
  21. let updatedKeys = {};
  22. let updatedValues = {};
  23. const isPull = {};
  24. const arrayAtomicUpdates = {};
  25. const numKeys = keys.length;
  26. let hasDollarUpdate = false;
  27. let currentUpdate;
  28. let key;
  29. for (let i = 0; i < numKeys; ++i) {
  30. if (keys[i].startsWith('$')) {
  31. hasDollarUpdate = true;
  32. if (keys[i] === '$push' || keys[i] === '$addToSet') {
  33. const _keys = Object.keys(castedDoc[keys[i]]);
  34. for (let ii = 0; ii < _keys.length; ++ii) {
  35. currentUpdate = castedDoc[keys[i]][_keys[ii]];
  36. if (currentUpdate?.$each) {
  37. arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []).
  38. concat(currentUpdate.$each);
  39. } else {
  40. arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []).
  41. concat([currentUpdate]);
  42. }
  43. }
  44. continue;
  45. }
  46. const flat = flatten(castedDoc[keys[i]], null, null, schema);
  47. const paths = Object.keys(flat);
  48. const numPaths = paths.length;
  49. for (let j = 0; j < numPaths; ++j) {
  50. const updatedPath = cleanPositionalOperators(paths[j]);
  51. key = keys[i];
  52. // With `$pull` we might flatten `$in`. Skip stuff nested under `$in`
  53. // for the rest of the logic, it will get handled later.
  54. if (updatedPath.includes('$')) {
  55. continue;
  56. }
  57. if (key === '$set' || key === '$setOnInsert' ||
  58. key === '$pull' || key === '$pullAll') {
  59. updatedValues[updatedPath] = flat[paths[j]];
  60. isPull[updatedPath] = key === '$pull' || key === '$pullAll';
  61. } else if (key === '$unset') {
  62. updatedValues[updatedPath] = undefined;
  63. }
  64. updatedKeys[updatedPath] = true;
  65. }
  66. }
  67. }
  68. if (!hasDollarUpdate) {
  69. updatedValues = flatten(castedDoc, null, null, schema);
  70. updatedKeys = Object.keys(updatedValues);
  71. }
  72. const updates = Object.keys(updatedValues);
  73. const numUpdates = updates.length;
  74. const validatorsToExecute = [];
  75. const validationErrors = [];
  76. const alreadyValidated = [];
  77. const context = query;
  78. for (let i = 0; i < numUpdates; ++i) {
  79. const v = updatedValues[updates[i]];
  80. const schemaPath = schema._getSchema(updates[i]);
  81. if (schemaPath == null) {
  82. continue;
  83. }
  84. if (schemaPath.instance === 'Mixed' && schemaPath.path !== updates[i]) {
  85. continue;
  86. }
  87. if (v && Array.isArray(v.$in)) {
  88. v.$in.forEach((v, i) => {
  89. validatorsToExecute.push(
  90. schemaPath.doValidate(v, context, { updateValidator: true }).catch(err => {
  91. err.path = updates[i] + '.$in.' + i;
  92. validationErrors.push(err);
  93. })
  94. );
  95. });
  96. } else {
  97. if (isPull[updates[i]] &&
  98. schemaPath.$isMongooseArray) {
  99. continue;
  100. }
  101. if (schemaPath.$isMongooseDocumentArrayElement && v?.$__ != null) {
  102. alreadyValidated.push(updates[i]);
  103. validatorsToExecute.push(
  104. schemaPath.doValidate(v, context, { updateValidator: true }).catch(err => {
  105. if (err.errors) {
  106. for (const key of Object.keys(err.errors)) {
  107. const _err = err.errors[key];
  108. _err.path = updates[i] + '.' + key;
  109. validationErrors.push(_err);
  110. }
  111. } else {
  112. err.path = updates[i];
  113. validationErrors.push(err);
  114. }
  115. })
  116. );
  117. } else {
  118. const isAlreadyValidated = alreadyValidated.find(path => updates[i].startsWith(path + '.'));
  119. if (isAlreadyValidated) {
  120. continue;
  121. }
  122. if (schemaPath.$isSingleNested) {
  123. alreadyValidated.push(updates[i]);
  124. }
  125. validatorsToExecute.push(
  126. schemaPath.doValidate(v, context, { updateValidator: true }).catch(err => {
  127. if (schemaPath.schema != null &&
  128. schemaPath.schema.options.storeSubdocValidationError === false &&
  129. err instanceof ValidationError) {
  130. return;
  131. }
  132. if (err) {
  133. err.path = updates[i];
  134. validationErrors.push(err);
  135. }
  136. })
  137. );
  138. }
  139. }
  140. }
  141. const arrayUpdates = Object.keys(arrayAtomicUpdates);
  142. for (const arrayUpdate of arrayUpdates) {
  143. let schemaPath = schema._getSchema(arrayUpdate);
  144. if (schemaPath && schemaPath.$isMongooseDocumentArray) {
  145. validatorsToExecute.push(
  146. schemaPath.doValidate(
  147. arrayAtomicUpdates[arrayUpdate],
  148. options?.context === 'query' ? query : null
  149. ).catch(err => {
  150. err.path = arrayUpdate;
  151. validationErrors.push(err);
  152. })
  153. );
  154. } else {
  155. schemaPath = schema._getSchema(arrayUpdate + '.0');
  156. for (const atomicUpdate of arrayAtomicUpdates[arrayUpdate]) {
  157. validatorsToExecute.push(
  158. schemaPath.doValidate(
  159. atomicUpdate,
  160. options?.context === 'query' ? query : null,
  161. { updateValidator: true }
  162. ).catch(err => {
  163. err.path = arrayUpdate;
  164. validationErrors.push(err);
  165. })
  166. );
  167. }
  168. }
  169. }
  170. await Promise.all(validatorsToExecute);
  171. if (validationErrors.length) {
  172. const err = new ValidationError(null);
  173. for (const validationError of validationErrors) {
  174. err.addError(validationError.path, validationError);
  175. }
  176. throw err;
  177. }
  178. };