You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							258 lines
						
					
					
						
							7.6 KiB
						
					
					
				
			
		
		
	
	
							258 lines
						
					
					
						
							7.6 KiB
						
					
					
				| 'use strict';
 | |
| 
 | |
| /*!
 | |
|  * Module dependencies.
 | |
|  */
 | |
| 
 | |
| const ValidationError = require('../error/validation');
 | |
| const cleanPositionalOperators = require('./schema/cleanPositionalOperators');
 | |
| const flatten = require('./common').flatten;
 | |
| const modifiedPaths = require('./common').modifiedPaths;
 | |
| 
 | |
| /**
 | |
|  * Applies validators and defaults to update and findOneAndUpdate operations,
 | |
|  * specifically passing a null doc as `this` to validators and defaults
 | |
|  *
 | |
|  * @param {Query} query
 | |
|  * @param {Schema} schema
 | |
|  * @param {Object} castedDoc
 | |
|  * @param {Object} options
 | |
|  * @method runValidatorsOnUpdate
 | |
|  * @api private
 | |
|  */
 | |
| 
 | |
| module.exports = function(query, schema, castedDoc, options, callback) {
 | |
|   let _keys;
 | |
|   const keys = Object.keys(castedDoc || {});
 | |
|   let updatedKeys = {};
 | |
|   let updatedValues = {};
 | |
|   const isPull = {};
 | |
|   const arrayAtomicUpdates = {};
 | |
|   const numKeys = keys.length;
 | |
|   let hasDollarUpdate = false;
 | |
|   const modified = {};
 | |
|   let currentUpdate;
 | |
|   let key;
 | |
|   let i;
 | |
| 
 | |
|   for (i = 0; i < numKeys; ++i) {
 | |
|     if (keys[i].startsWith('$')) {
 | |
|       hasDollarUpdate = true;
 | |
|       if (keys[i] === '$push' || keys[i] === '$addToSet') {
 | |
|         _keys = Object.keys(castedDoc[keys[i]]);
 | |
|         for (let ii = 0; ii < _keys.length; ++ii) {
 | |
|           currentUpdate = castedDoc[keys[i]][_keys[ii]];
 | |
|           if (currentUpdate && currentUpdate.$each) {
 | |
|             arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []).
 | |
|               concat(currentUpdate.$each);
 | |
|           } else {
 | |
|             arrayAtomicUpdates[_keys[ii]] = (arrayAtomicUpdates[_keys[ii]] || []).
 | |
|               concat([currentUpdate]);
 | |
|           }
 | |
|         }
 | |
|         continue;
 | |
|       }
 | |
|       modifiedPaths(castedDoc[keys[i]], '', modified);
 | |
|       const flat = flatten(castedDoc[keys[i]], null, null, schema);
 | |
|       const paths = Object.keys(flat);
 | |
|       const numPaths = paths.length;
 | |
|       for (let j = 0; j < numPaths; ++j) {
 | |
|         const updatedPath = cleanPositionalOperators(paths[j]);
 | |
|         key = keys[i];
 | |
|         // With `$pull` we might flatten `$in`. Skip stuff nested under `$in`
 | |
|         // for the rest of the logic, it will get handled later.
 | |
|         if (updatedPath.includes('$')) {
 | |
|           continue;
 | |
|         }
 | |
|         if (key === '$set' || key === '$setOnInsert' ||
 | |
|             key === '$pull' || key === '$pullAll') {
 | |
|           updatedValues[updatedPath] = flat[paths[j]];
 | |
|           isPull[updatedPath] = key === '$pull' || key === '$pullAll';
 | |
|         } else if (key === '$unset') {
 | |
|           updatedValues[updatedPath] = undefined;
 | |
|         }
 | |
|         updatedKeys[updatedPath] = true;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (!hasDollarUpdate) {
 | |
|     modifiedPaths(castedDoc, '', modified);
 | |
|     updatedValues = flatten(castedDoc, null, null, schema);
 | |
|     updatedKeys = Object.keys(updatedValues);
 | |
|   }
 | |
| 
 | |
|   const updates = Object.keys(updatedValues);
 | |
|   const numUpdates = updates.length;
 | |
|   const validatorsToExecute = [];
 | |
|   const validationErrors = [];
 | |
| 
 | |
|   const alreadyValidated = [];
 | |
| 
 | |
|   const context = options && options.context === 'query' ? query : null;
 | |
|   function iter(i, v) {
 | |
|     const schemaPath = schema._getSchema(updates[i]);
 | |
|     if (schemaPath == null) {
 | |
|       return;
 | |
|     }
 | |
|     if (schemaPath.instance === 'Mixed' && schemaPath.path !== updates[i]) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (v && Array.isArray(v.$in)) {
 | |
|       v.$in.forEach((v, i) => {
 | |
|         validatorsToExecute.push(function(callback) {
 | |
|           schemaPath.doValidate(
 | |
|             v,
 | |
|             function(err) {
 | |
|               if (err) {
 | |
|                 err.path = updates[i] + '.$in.' + i;
 | |
|                 validationErrors.push(err);
 | |
|               }
 | |
|               callback(null);
 | |
|             },
 | |
|             context,
 | |
|             { updateValidator: true });
 | |
|         });
 | |
|       });
 | |
|     } else {
 | |
|       if (isPull[updates[i]] &&
 | |
|           schemaPath.$isMongooseArray) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (schemaPath.$isMongooseDocumentArrayElement && v != null && v.$__ != null) {
 | |
|         alreadyValidated.push(updates[i]);
 | |
|         validatorsToExecute.push(function(callback) {
 | |
|           schemaPath.doValidate(v, function(err) {
 | |
|             if (err) {
 | |
|               err.path = updates[i];
 | |
|               validationErrors.push(err);
 | |
|               return callback(null);
 | |
|             }
 | |
| 
 | |
|             v.validate(function(err) {
 | |
|               if (err) {
 | |
|                 if (err.errors) {
 | |
|                   for (const key of Object.keys(err.errors)) {
 | |
|                     const _err = err.errors[key];
 | |
|                     _err.path = updates[i] + '.' + key;
 | |
|                     validationErrors.push(_err);
 | |
|                   }
 | |
|                 } else {
 | |
|                   err.path = updates[i];
 | |
|                   validationErrors.push(err);
 | |
|                 }
 | |
|               }
 | |
|               callback(null);
 | |
|             });
 | |
|           }, context, { updateValidator: true });
 | |
|         });
 | |
|       } else {
 | |
|         validatorsToExecute.push(function(callback) {
 | |
|           for (const path of alreadyValidated) {
 | |
|             if (updates[i].startsWith(path + '.')) {
 | |
|               return callback(null);
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           schemaPath.doValidate(v, function(err) {
 | |
|             if (schemaPath.schema != null &&
 | |
|                 schemaPath.schema.options.storeSubdocValidationError === false &&
 | |
|                 err instanceof ValidationError) {
 | |
|               return callback(null);
 | |
|             }
 | |
| 
 | |
|             if (err) {
 | |
|               err.path = updates[i];
 | |
|               validationErrors.push(err);
 | |
|             }
 | |
|             callback(null);
 | |
|           }, context, { updateValidator: true });
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   for (i = 0; i < numUpdates; ++i) {
 | |
|     iter(i, updatedValues[updates[i]]);
 | |
|   }
 | |
| 
 | |
|   const arrayUpdates = Object.keys(arrayAtomicUpdates);
 | |
|   for (const arrayUpdate of arrayUpdates) {
 | |
|     let schemaPath = schema._getSchema(arrayUpdate);
 | |
|     if (schemaPath && schemaPath.$isMongooseDocumentArray) {
 | |
|       validatorsToExecute.push(function(callback) {
 | |
|         schemaPath.doValidate(
 | |
|           arrayAtomicUpdates[arrayUpdate],
 | |
|           getValidationCallback(arrayUpdate, validationErrors, callback),
 | |
|           options && options.context === 'query' ? query : null);
 | |
|       });
 | |
|     } else {
 | |
|       schemaPath = schema._getSchema(arrayUpdate + '.0');
 | |
|       for (const atomicUpdate of arrayAtomicUpdates[arrayUpdate]) {
 | |
|         validatorsToExecute.push(function(callback) {
 | |
|           schemaPath.doValidate(
 | |
|             atomicUpdate,
 | |
|             getValidationCallback(arrayUpdate, validationErrors, callback),
 | |
|             options && options.context === 'query' ? query : null,
 | |
|             { updateValidator: true });
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (callback != null) {
 | |
|     let numValidators = validatorsToExecute.length;
 | |
|     if (numValidators === 0) {
 | |
|       return _done(callback);
 | |
|     }
 | |
|     for (const validator of validatorsToExecute) {
 | |
|       validator(function() {
 | |
|         if (--numValidators <= 0) {
 | |
|           _done(callback);
 | |
|         }
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   return function(callback) {
 | |
|     let numValidators = validatorsToExecute.length;
 | |
|     if (numValidators === 0) {
 | |
|       return _done(callback);
 | |
|     }
 | |
|     for (const validator of validatorsToExecute) {
 | |
|       validator(function() {
 | |
|         if (--numValidators <= 0) {
 | |
|           _done(callback);
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   function _done(callback) {
 | |
|     if (validationErrors.length) {
 | |
|       const err = new ValidationError(null);
 | |
| 
 | |
|       for (const validationError of validationErrors) {
 | |
|         err.addError(validationError.path, validationError);
 | |
|       }
 | |
| 
 | |
|       return callback(err);
 | |
|     }
 | |
|     callback(null);
 | |
|   }
 | |
| 
 | |
|   function getValidationCallback(arrayUpdate, validationErrors, callback) {
 | |
|     return function(err) {
 | |
|       if (err) {
 | |
|         err.path = arrayUpdate;
 | |
|         validationErrors.push(err);
 | |
|       }
 | |
|       callback(null);
 | |
|     };
 | |
|   }
 | |
| };
 | |
| 
 |