1 | 'use strict';
|
2 |
|
3 | const CastError = require('../../error/cast');
|
4 | const MongooseError = require('../../error/mongooseError');
|
5 | const StrictModeError = require('../../error/strict');
|
6 | const ValidationError = require('../../error/validation');
|
7 | const castNumber = require('../../cast/number');
|
8 | const cast = require('../../cast');
|
9 | const getEmbeddedDiscriminatorPath = require('./getEmbeddedDiscriminatorPath');
|
10 | const handleImmutable = require('./handleImmutable');
|
11 | const moveImmutableProperties = require('../update/moveImmutableProperties');
|
12 | const schemaMixedSymbol = require('../../schema/symbols').schemaMixedSymbol;
|
13 | const utils = require('../../utils');
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 | module.exports = function castUpdate(schema, obj, options, context, filter) {
|
28 | if (obj == null) {
|
29 | return undefined;
|
30 | }
|
31 | options = options || {};
|
32 |
|
33 |
|
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 |
|
61 | if (op[0] !== '$' && !overwrite) {
|
62 |
|
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 |
|
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 |
|
113 |
|
114 |
|
115 | function 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 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 | function 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 |
|
184 |
|
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 |
|
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 |
|
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 |
|
228 |
|
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 |
|
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 |
|
290 |
|
291 |
|
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 |
|
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 |
|
309 |
|
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 |
|
332 |
|
333 | if (isStrict === 'throw' && schema.virtuals[checkPath] == null) {
|
334 | throw new StrictModeError(prefix + key);
|
335 | } else {
|
336 | delete obj[key];
|
337 | }
|
338 | } else {
|
339 |
|
340 |
|
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 |
|
377 |
|
378 |
|
379 | function _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 |
|
390 |
|
391 |
|
392 |
|
393 | const numberOps = {
|
394 | $pop: 1,
|
395 | $inc: 1
|
396 | };
|
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 | const noCastOps = {
|
403 | $unset: 1
|
404 | };
|
405 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 |
|
411 | const castOps = {
|
412 | $push: 1,
|
413 | $addToSet: 1,
|
414 | $set: 1,
|
415 | $setOnInsert: 1
|
416 | };
|
417 |
|
418 | /*!
|
419 | * ignore
|
420 | */
|
421 |
|
422 | const 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 |
|
438 | function castUpdateVal(schema, val, op, $conditional, context, path) {
|
439 | if (!schema) {
|
440 |
|
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 |
|
455 |
|
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 |
|
488 | if (val == null) {
|
489 | throw new CastError('number', val, schema.path);
|
490 | }
|
491 | if (op === '$inc') {
|
492 |
|
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 | }
|