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.
251 lines
7.4 KiB
251 lines
7.4 KiB
5 years ago
|
'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 (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);
|
||
|
};
|
||
|
}
|
||
|
};
|
||
|
|