castUpdate.js 19 KB

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