UNPKG

14.6 kBJavaScriptView Raw
1'use strict';
2
3const CastError = require('../../error/cast');
4const MongooseError = require('../../error/mongooseError');
5const StrictModeError = require('../../error/strict');
6const ValidationError = require('../../error/validation');
7const castNumber = require('../../cast/number');
8const cast = require('../../cast');
9const getEmbeddedDiscriminatorPath = require('./getEmbeddedDiscriminatorPath');
10const handleImmutable = require('./handleImmutable');
11const moveImmutableProperties = require('../update/moveImmutableProperties');
12const schemaMixedSymbol = require('../../schema/symbols').schemaMixedSymbol;
13const utils = require('../../utils');
14
15/*!
16 * Casts an update op based on the given schema
17 *
18 * @param {Schema} schema
19 * @param {Object} obj
20 * @param {Object} options
21 * @param {Boolean} [options.overwrite] defaults to false
22 * @param {Boolean|String} [options.strict] defaults to true
23 * @param {Query} context passed to setters
24 * @return {Boolean} true iff the update is non-empty
25 */
26
27module.exports = function castUpdate(schema, obj, options, context, filter) {
28 if (obj == null) {
29 return undefined;
30 }
31 options = options || {};
32
33 // Update pipeline
34 if (Array.isArray(obj)) {
35 const len = obj.length;
36 for (let i = 0; i < len; ++i) {
37 const ops = Object.keys(obj[i]);
38 for (const op of ops) {
39 obj[i][op] = castPipelineOperator(op, obj[i][op]);
40 }
41 }
42 return obj;
43 }
44
45 if (options.upsert) {
46 moveImmutableProperties(schema, obj, context);
47 }
48
49 const ops = Object.keys(obj);
50 let i = ops.length;
51 const ret = {};
52 let val;
53 let hasDollarKey = false;
54 const overwrite = options.overwrite;
55
56 filter = filter || {};
57
58 while (i--) {
59 const op = ops[i];
60 // if overwrite is set, don't do any of the special $set stuff
61 if (op[0] !== '$' && !overwrite) {
62 // fix up $set sugar
63 if (!ret.$set) {
64 if (obj.$set) {
65 ret.$set = obj.$set;
66 } else {
67 ret.$set = {};
68 }
69 }
70 ret.$set[op] = obj[op];
71 ops.splice(i, 1);
72 if (!~ops.indexOf('$set')) ops.push('$set');
73 } else if (op === '$set') {
74 if (!ret.$set) {
75 ret[op] = obj[op];
76 }
77 } else {
78 ret[op] = obj[op];
79 }
80 }
81
82 // cast each value
83 i = ops.length;
84
85 while (i--) {
86 const op = ops[i];
87 val = ret[op];
88 hasDollarKey = hasDollarKey || op.startsWith('$');
89
90 if (val &&
91 typeof val === 'object' &&
92 !Buffer.isBuffer(val) &&
93 (!overwrite || hasDollarKey)) {
94 walkUpdatePath(schema, val, op, options, context, filter);
95 } else if (overwrite && ret && typeof ret === 'object') {
96 walkUpdatePath(schema, ret, '$set', options, context, filter);
97 } else {
98 const msg = 'Invalid atomic update value for ' + op + '. '
99 + 'Expected an object, received ' + typeof val;
100 throw new Error(msg);
101 }
102
103 if (op.startsWith('$') && utils.isEmptyObject(val)) {
104 delete ret[op];
105 }
106 }
107
108 return ret;
109};
110
111/*!
112 * ignore
113 */
114
115function castPipelineOperator(op, val) {
116 if (op === '$unset') {
117 if (!Array.isArray(val) || val.find(v => typeof v !== 'string')) {
118 throw new MongooseError('Invalid $unset in pipeline, must be ' +
119 'an array of strings');
120 }
121 return val;
122 }
123 if (op === '$project') {
124 if (val == null || typeof val !== 'object') {
125 throw new MongooseError('Invalid $project in pipeline, must be an object');
126 }
127 return val;
128 }
129 if (op === '$addFields' || op === '$set') {
130 if (val == null || typeof val !== 'object') {
131 throw new MongooseError('Invalid ' + op + ' in pipeline, must be an object');
132 }
133 return val;
134 } else if (op === '$replaceRoot' || op === '$replaceWith') {
135 if (val == null || typeof val !== 'object') {
136 throw new MongooseError('Invalid ' + op + ' in pipeline, must be an object');
137 }
138 return val;
139 }
140
141 throw new MongooseError('Invalid update pipeline operator: "' + op + '"');
142}
143
144/*!
145 * Walk each path of obj and cast its values
146 * according to its schema.
147 *
148 * @param {Schema} schema
149 * @param {Object} obj - part of a query
150 * @param {String} op - the atomic operator ($pull, $set, etc)
151 * @param {Object} options
152 * @param {Boolean|String} [options.strict]
153 * @param {Boolean} [options.omitUndefined]
154 * @param {Query} context
155 * @param {String} pref - path prefix (internal only)
156 * @return {Bool} true if this path has keys to update
157 * @api private
158 */
159
160function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
161 const strict = options.strict;
162 const prefix = pref ? pref + '.' : '';
163 const keys = Object.keys(obj);
164 let i = keys.length;
165 let hasKeys = false;
166 let schematype;
167 let key;
168 let val;
169
170 let aggregatedError = null;
171
172 let useNestedStrict;
173 if (options.useNestedStrict === undefined) {
174 useNestedStrict = schema.options.useNestedStrict;
175 } else {
176 useNestedStrict = options.useNestedStrict;
177 }
178
179 while (i--) {
180 key = keys[i];
181 val = obj[key];
182
183 // `$pull` is special because we need to cast the RHS as a query, not as
184 // an update.
185 if (op === '$pull') {
186 schematype = schema._getSchema(prefix + key);
187 if (schematype != null && schematype.schema != null) {
188 obj[key] = cast(schematype.schema, obj[key], options, context);
189 hasKeys = true;
190 continue;
191 }
192 }
193
194 if (val && val.constructor.name === 'Object') {
195 // watch for embedded doc schemas
196 schematype = schema._getSchema(prefix + key);
197
198 if (handleImmutable(schematype, strict, obj, key, prefix + key, context)) {
199 continue;
200 }
201
202 if (schematype && schematype.caster && op in castOps) {
203 // embedded doc schema
204 if ('$each' in val) {
205 hasKeys = true;
206 try {
207 obj[key] = {
208 $each: castUpdateVal(schematype, val.$each, op, key, context, prefix + key)
209 };
210 } catch (error) {
211 aggregatedError = _handleCastError(error, context, key, aggregatedError);
212 }
213
214 if (val.$slice != null) {
215 obj[key].$slice = val.$slice | 0;
216 }
217
218 if (val.$sort) {
219 obj[key].$sort = val.$sort;
220 }
221
222 if (val.$position != null) {
223 obj[key].$position = castNumber(val.$position);
224 }
225 } else {
226 if (schematype != null && schematype.$isSingleNested) {
227 // Special case to make sure `strict` bubbles down correctly to
228 // single nested re: gh-8735
229 let _strict = strict;
230 if (useNestedStrict !== false && schematype.schema.options.hasOwnProperty('strict')) {
231 _strict = schematype.schema.options.strict;
232 } else if (useNestedStrict === false) {
233 _strict = schema.options.strict;
234 }
235 try {
236 obj[key] = schematype.castForQuery(val, context, { strict: _strict });
237 } catch (error) {
238 aggregatedError = _handleCastError(error, context, key, aggregatedError);
239 }
240 } else {
241 try {
242 obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
243 } catch (error) {
244 aggregatedError = _handleCastError(error, context, key, aggregatedError);
245 }
246 }
247
248 if (options.omitUndefined && obj[key] === void 0) {
249 delete obj[key];
250 continue;
251 }
252
253 hasKeys = true;
254 }
255 } else if ((op === '$currentDate') || (op in castOps && schematype)) {
256 // $currentDate can take an object
257 try {
258 obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
259 } catch (error) {
260 aggregatedError = _handleCastError(error, context, key, aggregatedError);
261 }
262
263 if (options.omitUndefined && obj[key] === void 0) {
264 delete obj[key];
265 continue;
266 }
267
268 hasKeys = true;
269 } else {
270 const pathToCheck = (prefix + key);
271 const v = schema._getPathType(pathToCheck);
272 let _strict = strict;
273 if (useNestedStrict &&
274 v &&
275 v.schema &&
276 'strict' in v.schema.options) {
277 _strict = v.schema.options.strict;
278 }
279
280 if (v.pathType === 'undefined') {
281 if (_strict === 'throw') {
282 throw new StrictModeError(pathToCheck);
283 } else if (_strict) {
284 delete obj[key];
285 continue;
286 }
287 }
288
289 // gh-2314
290 // we should be able to set a schema-less field
291 // to an empty object literal
292 hasKeys |= walkUpdatePath(schema, val, op, options, context, filter, prefix + key) ||
293 (utils.isObject(val) && Object.keys(val).length === 0);
294 }
295 } else {
296 const checkPath = (key === '$each' || key === '$or' || key === '$and' || key === '$in') ?
297 pref : prefix + key;
298 schematype = schema._getSchema(checkPath);
299
300 // You can use `$setOnInsert` with immutable keys
301 if (op !== '$setOnInsert' &&
302 handleImmutable(schematype, strict, obj, key, prefix + key, context)) {
303 continue;
304 }
305
306 let pathDetails = schema._getPathType(checkPath);
307
308 // If no schema type, check for embedded discriminators because the
309 // filter or update may imply an embedded discriminator type. See #8378
310 if (schematype == null) {
311 const _res = getEmbeddedDiscriminatorPath(schema, obj, filter, checkPath);
312 if (_res.schematype != null) {
313 schematype = _res.schematype;
314 pathDetails = _res.type;
315 }
316 }
317
318 let isStrict = strict;
319 if (useNestedStrict &&
320 pathDetails &&
321 pathDetails.schema &&
322 'strict' in pathDetails.schema.options) {
323 isStrict = pathDetails.schema.options.strict;
324 }
325
326 const skip = isStrict &&
327 !schematype &&
328 !/real|nested/.test(pathDetails.pathType);
329
330 if (skip) {
331 // Even if strict is `throw`, avoid throwing an error because of
332 // virtuals because of #6731
333 if (isStrict === 'throw' && schema.virtuals[checkPath] == null) {
334 throw new StrictModeError(prefix + key);
335 } else {
336 delete obj[key];
337 }
338 } else {
339 // gh-1845 temporary fix: ignore $rename. See gh-3027 for tracking
340 // improving this.
341 if (op === '$rename') {
342 hasKeys = true;
343 continue;
344 }
345
346 try {
347 obj[key] = castUpdateVal(schematype, val, op, key, context, prefix + key);
348 } catch (error) {
349 aggregatedError = _handleCastError(error, context, key, aggregatedError);
350 }
351
352 if (Array.isArray(obj[key]) && (op === '$addToSet' || op === '$push') && key !== '$each') {
353 if (schematype && schematype.caster && !schematype.caster.$isMongooseArray) {
354 obj[key] = { $each: obj[key] };
355 }
356 }
357
358 if (options.omitUndefined && obj[key] === void 0) {
359 delete obj[key];
360 continue;
361 }
362
363 hasKeys = true;
364 }
365 }
366 }
367
368 if (aggregatedError != null) {
369 throw aggregatedError;
370 }
371
372 return hasKeys;
373}
374
375/*!
376 * ignore
377 */
378
379function _handleCastError(error, query, key, aggregatedError) {
380 if (typeof query !== 'object' || !query.options.multipleCastError) {
381 throw error;
382 }
383 aggregatedError = aggregatedError || new ValidationError();
384 aggregatedError.addError(key, error);
385 return aggregatedError;
386}
387
388/*!
389 * These operators should be cast to numbers instead
390 * of their path schema type.
391 */
392
393const numberOps = {
394 $pop: 1,
395 $inc: 1
396};
397
398/*!
399 * These ops require no casting because the RHS doesn't do anything.
400 */
401
402const noCastOps = {
403 $unset: 1
404};
405
406/*!
407 * These operators require casting docs
408 * to real Documents for Update operations.
409 */
410
411const castOps = {
412 $push: 1,
413 $addToSet: 1,
414 $set: 1,
415 $setOnInsert: 1
416};
417
418/*!
419 * ignore
420 */
421
422const overwriteOps = {
423 $set: 1,
424 $setOnInsert: 1
425};
426
427/*!
428 * Casts `val` according to `schema` and atomic `op`.
429 *
430 * @param {SchemaType} schema
431 * @param {Object} val
432 * @param {String} op - the atomic operator ($pull, $set, etc)
433 * @param {String} $conditional
434 * @param {Query} context
435 * @api private
436 */
437
438function castUpdateVal(schema, val, op, $conditional, context, path) {
439 if (!schema) {
440 // non-existing schema path
441 if (op in numberOps) {
442 try {
443 return castNumber(val);
444 } catch (err) {
445 throw new CastError('number', val, path);
446 }
447 }
448 return val;
449 }
450
451 const cond = schema.caster && op in castOps &&
452 (utils.isObject(val) || Array.isArray(val));
453 if (cond && !overwriteOps[op]) {
454 // Cast values for ops that add data to MongoDB.
455 // Ensures embedded documents get ObjectIds etc.
456 let schemaArrayDepth = 0;
457 let cur = schema;
458 while (cur.$isMongooseArray) {
459 ++schemaArrayDepth;
460 cur = cur.caster;
461 }
462 let arrayDepth = 0;
463 let _val = val;
464 while (Array.isArray(_val)) {
465 ++arrayDepth;
466 _val = _val[0];
467 }
468
469 const additionalNesting = schemaArrayDepth - arrayDepth;
470 while (arrayDepth < schemaArrayDepth) {
471 val = [val];
472 ++arrayDepth;
473 }
474
475 let tmp = schema.applySetters(Array.isArray(val) ? val : [val], context);
476
477 for (let i = 0; i < additionalNesting; ++i) {
478 tmp = tmp[0];
479 }
480 return tmp;
481 }
482
483 if (op in noCastOps) {
484 return val;
485 }
486 if (op in numberOps) {
487 // Null and undefined not allowed for $pop, $inc
488 if (val == null) {
489 throw new CastError('number', val, schema.path);
490 }
491 if (op === '$inc') {
492 // Support `$inc` with long, int32, etc. (gh-4283)
493 return schema.castForQueryWrapper({
494 val: val,
495 context: context
496 });
497 }
498 try {
499 return castNumber(val);
500 } catch (error) {
501 throw new CastError('number', val, schema.path);
502 }
503 }
504 if (op === '$currentDate') {
505 if (typeof val === 'object') {
506 return { $type: val.$type };
507 }
508 return Boolean(val);
509 }
510
511 if (/^\$/.test($conditional)) {
512 return schema.castForQueryWrapper({
513 $conditional: $conditional,
514 val: val,
515 context: context
516 });
517 }
518
519 if (overwriteOps[op]) {
520 return schema.castForQueryWrapper({
521 val: val,
522 context: context,
523 $skipQueryCastForUpdate: val != null && schema.$isMongooseArray && schema.$fullPath != null && !schema.$fullPath.match(/\d+$/),
524 $applySetters: schema[schemaMixedSymbol] != null
525 });
526 }
527
528 return schema.castForQueryWrapper({ val: val, context: context });
529}