index.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  1. 'use strict';
  2. const Document = require('../../../document');
  3. const ArraySubdocument = require('../../ArraySubdocument');
  4. const MongooseError = require('../../../error/mongooseError');
  5. const cleanModifiedSubpaths = require('../../../helpers/document/cleanModifiedSubpaths');
  6. const internalToObjectOptions = require('../../../options').internalToObjectOptions;
  7. const utils = require('../../../utils');
  8. const isBsonType = require('../../../helpers/isBsonType');
  9. const arrayAtomicsSymbol = require('../../../helpers/symbols').arrayAtomicsSymbol;
  10. const arrayParentSymbol = require('../../../helpers/symbols').arrayParentSymbol;
  11. const arrayPathSymbol = require('../../../helpers/symbols').arrayPathSymbol;
  12. const arraySchemaSymbol = require('../../../helpers/symbols').arraySchemaSymbol;
  13. const populateModelSymbol = require('../../../helpers/symbols').populateModelSymbol;
  14. const slicedSymbol = Symbol('mongoose#Array#sliced');
  15. const _basePush = Array.prototype.push;
  16. /*!
  17. * ignore
  18. */
  19. const methods = {
  20. /**
  21. * Depopulates stored atomic operation values as necessary for direct insertion to MongoDB.
  22. *
  23. * If no atomics exist, we return all array values after conversion.
  24. *
  25. * @return {Array}
  26. * @method $__getAtomics
  27. * @memberOf MongooseArray
  28. * @instance
  29. * @api private
  30. */
  31. $__getAtomics() {
  32. const ret = [];
  33. const keys = Object.keys(this[arrayAtomicsSymbol] || {});
  34. let i = keys.length;
  35. const opts = Object.assign({}, internalToObjectOptions, { _isNested: true });
  36. if (i === 0) {
  37. ret[0] = ['$set', this.toObject(opts)];
  38. return ret;
  39. }
  40. while (i--) {
  41. const op = keys[i];
  42. let val = this[arrayAtomicsSymbol][op];
  43. // the atomic values which are arrays are not MongooseArrays. we
  44. // need to convert their elements as if they were MongooseArrays
  45. // to handle populated arrays versus DocumentArrays properly.
  46. if (utils.isMongooseObject(val)) {
  47. val = val.toObject(opts);
  48. } else if (Array.isArray(val)) {
  49. val = this.toObject.call(val, opts);
  50. } else if (val != null && Array.isArray(val.$each)) {
  51. val.$each = this.toObject.call(val.$each, opts);
  52. } else if (val != null && typeof val.valueOf === 'function') {
  53. val = val.valueOf();
  54. }
  55. if (op === '$addToSet') {
  56. val = { $each: val };
  57. }
  58. ret.push([op, val]);
  59. }
  60. return ret;
  61. },
  62. /*!
  63. * ignore
  64. */
  65. $atomics() {
  66. return this[arrayAtomicsSymbol];
  67. },
  68. /*!
  69. * ignore
  70. */
  71. $parent() {
  72. return this[arrayParentSymbol];
  73. },
  74. /*!
  75. * ignore
  76. */
  77. $path() {
  78. return this[arrayPathSymbol];
  79. },
  80. /**
  81. * Atomically shifts the array at most one time per document `save()`.
  82. *
  83. * #### Note:
  84. *
  85. * _Calling this multiple times on an array before saving sends the same command as calling it once._
  86. * _This update is implemented using the MongoDB [$pop](https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop) method which enforces this restriction._
  87. *
  88. * doc.array = [1,2,3];
  89. *
  90. * const shifted = doc.array.$shift();
  91. * console.log(shifted); // 1
  92. * console.log(doc.array); // [2,3]
  93. *
  94. * // no affect
  95. * shifted = doc.array.$shift();
  96. * console.log(doc.array); // [2,3]
  97. *
  98. * doc.save(function (err) {
  99. * if (err) return handleError(err);
  100. *
  101. * // we saved, now $shift works again
  102. * shifted = doc.array.$shift();
  103. * console.log(shifted ); // 2
  104. * console.log(doc.array); // [3]
  105. * })
  106. *
  107. * @api public
  108. * @memberOf MongooseArray
  109. * @instance
  110. * @method $shift
  111. * @see mongodb https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop
  112. */
  113. $shift() {
  114. this._registerAtomic('$pop', -1);
  115. this._markModified();
  116. // only allow shifting once
  117. if (this._shifted) {
  118. return;
  119. }
  120. this._shifted = true;
  121. return [].shift.call(this);
  122. },
  123. /**
  124. * Pops the array atomically at most one time per document `save()`.
  125. *
  126. * #### NOTE:
  127. *
  128. * _Calling this multiple times on an array before saving sends the same command as calling it once._
  129. * _This update is implemented using the MongoDB [$pop](https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop) method which enforces this restriction._
  130. *
  131. * doc.array = [1,2,3];
  132. *
  133. * const popped = doc.array.$pop();
  134. * console.log(popped); // 3
  135. * console.log(doc.array); // [1,2]
  136. *
  137. * // no affect
  138. * popped = doc.array.$pop();
  139. * console.log(doc.array); // [1,2]
  140. *
  141. * doc.save(function (err) {
  142. * if (err) return handleError(err);
  143. *
  144. * // we saved, now $pop works again
  145. * popped = doc.array.$pop();
  146. * console.log(popped); // 2
  147. * console.log(doc.array); // [1]
  148. * })
  149. *
  150. * @api public
  151. * @method $pop
  152. * @memberOf MongooseArray
  153. * @instance
  154. * @see mongodb https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop
  155. * @method $pop
  156. * @memberOf MongooseArray
  157. */
  158. $pop() {
  159. this._registerAtomic('$pop', 1);
  160. this._markModified();
  161. // only allow popping once
  162. if (this._popped) {
  163. return;
  164. }
  165. this._popped = true;
  166. return [].pop.call(this);
  167. },
  168. /*!
  169. * ignore
  170. */
  171. $schema() {
  172. return this[arraySchemaSymbol];
  173. },
  174. /**
  175. * Casts a member based on this arrays schema.
  176. *
  177. * @param {any} value
  178. * @return value the casted value
  179. * @method _cast
  180. * @api private
  181. * @memberOf MongooseArray
  182. */
  183. _cast(value) {
  184. let populated = false;
  185. let Model;
  186. const parent = this[arrayParentSymbol];
  187. if (parent) {
  188. populated = parent.$populated(this[arrayPathSymbol], true);
  189. }
  190. if (populated && value !== null && value !== undefined) {
  191. // cast to the populated Models schema
  192. Model = populated.options[populateModelSymbol];
  193. // only objects are permitted so we can safely assume that
  194. // non-objects are to be interpreted as _id
  195. if (Buffer.isBuffer(value) ||
  196. isBsonType(value, 'ObjectID') || !utils.isObject(value)) {
  197. value = { _id: value };
  198. }
  199. // gh-2399
  200. // we should cast model only when it's not a discriminator
  201. const isDisc = value.schema && value.schema.discriminatorMapping &&
  202. value.schema.discriminatorMapping.key !== undefined;
  203. if (!isDisc) {
  204. value = new Model(value);
  205. }
  206. return this[arraySchemaSymbol].caster.applySetters(value, parent, true);
  207. }
  208. return this[arraySchemaSymbol].caster.applySetters(value, parent, false);
  209. },
  210. /**
  211. * Internal helper for .map()
  212. *
  213. * @api private
  214. * @return {Number}
  215. * @method _mapCast
  216. * @memberOf MongooseArray
  217. */
  218. _mapCast(val, index) {
  219. return this._cast(val, this.length + index);
  220. },
  221. /**
  222. * Marks this array as modified.
  223. *
  224. * If it bubbles up from an embedded document change, then it takes the following arguments (otherwise, takes 0 arguments)
  225. *
  226. * @param {ArraySubdocument} subdoc the embedded doc that invoked this method on the Array
  227. * @param {String} embeddedPath the path which changed in the subdoc
  228. * @method _markModified
  229. * @api private
  230. * @memberOf MongooseArray
  231. */
  232. _markModified(elem) {
  233. const parent = this[arrayParentSymbol];
  234. let dirtyPath;
  235. if (parent) {
  236. dirtyPath = this[arrayPathSymbol];
  237. if (arguments.length) {
  238. dirtyPath = dirtyPath + '.' + elem;
  239. }
  240. if (dirtyPath != null && dirtyPath.endsWith('.$')) {
  241. return this;
  242. }
  243. parent.markModified(dirtyPath, arguments.length !== 0 ? elem : parent);
  244. }
  245. return this;
  246. },
  247. /**
  248. * Register an atomic operation with the parent.
  249. *
  250. * @param {Array} op operation
  251. * @param {any} val
  252. * @method _registerAtomic
  253. * @api private
  254. * @memberOf MongooseArray
  255. */
  256. _registerAtomic(op, val) {
  257. if (this[slicedSymbol]) {
  258. return;
  259. }
  260. if (op === '$set') {
  261. // $set takes precedence over all other ops.
  262. // mark entire array modified.
  263. this[arrayAtomicsSymbol] = { $set: val };
  264. cleanModifiedSubpaths(this[arrayParentSymbol], this[arrayPathSymbol]);
  265. this._markModified();
  266. return this;
  267. }
  268. const atomics = this[arrayAtomicsSymbol];
  269. // reset pop/shift after save
  270. if (op === '$pop' && !('$pop' in atomics)) {
  271. const _this = this;
  272. this[arrayParentSymbol].once('save', function() {
  273. _this._popped = _this._shifted = null;
  274. });
  275. }
  276. // check for impossible $atomic combos (Mongo denies more than one
  277. // $atomic op on a single path
  278. if (atomics.$set || Object.keys(atomics).length && !(op in atomics)) {
  279. // a different op was previously registered.
  280. // save the entire thing.
  281. this[arrayAtomicsSymbol] = { $set: this };
  282. return this;
  283. }
  284. let selector;
  285. if (op === '$pullAll' || op === '$addToSet') {
  286. atomics[op] || (atomics[op] = []);
  287. atomics[op] = atomics[op].concat(val);
  288. } else if (op === '$pullDocs') {
  289. const pullOp = atomics['$pull'] || (atomics['$pull'] = {});
  290. if (val[0] instanceof ArraySubdocument) {
  291. selector = pullOp['$or'] || (pullOp['$or'] = []);
  292. Array.prototype.push.apply(selector, val.map(function(v) {
  293. return v.toObject({ transform: false, virtuals: false });
  294. }));
  295. } else {
  296. selector = pullOp['_id'] || (pullOp['_id'] = { $in: [] });
  297. selector['$in'] = selector['$in'].concat(val);
  298. }
  299. } else if (op === '$push') {
  300. atomics.$push = atomics.$push || { $each: [] };
  301. if (val != null && utils.hasUserDefinedProperty(val, '$each')) {
  302. atomics.$push = val;
  303. } else {
  304. atomics.$push.$each = atomics.$push.$each.concat(val);
  305. }
  306. } else {
  307. atomics[op] = val;
  308. }
  309. return this;
  310. },
  311. /**
  312. * Adds values to the array if not already present.
  313. *
  314. * #### Example:
  315. *
  316. * console.log(doc.array) // [2,3,4]
  317. * const added = doc.array.addToSet(4,5);
  318. * console.log(doc.array) // [2,3,4,5]
  319. * console.log(added) // [5]
  320. *
  321. * @param {any} [args...]
  322. * @return {Array} the values that were added
  323. * @memberOf MongooseArray
  324. * @api public
  325. * @method addToSet
  326. */
  327. addToSet() {
  328. _checkManualPopulation(this, arguments);
  329. let values = [].map.call(arguments, this._mapCast, this);
  330. values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol]);
  331. const added = [];
  332. let type = '';
  333. if (values[0] instanceof ArraySubdocument) {
  334. type = 'doc';
  335. } else if (values[0] instanceof Date) {
  336. type = 'date';
  337. }
  338. const rawValues = utils.isMongooseArray(values) ? values.__array : this;
  339. const rawArray = utils.isMongooseArray(this) ? this.__array : this;
  340. rawValues.forEach(function(v) {
  341. let found;
  342. const val = +v;
  343. switch (type) {
  344. case 'doc':
  345. found = this.some(function(doc) {
  346. return doc.equals(v);
  347. });
  348. break;
  349. case 'date':
  350. found = this.some(function(d) {
  351. return +d === val;
  352. });
  353. break;
  354. default:
  355. found = ~this.indexOf(v);
  356. }
  357. if (!found) {
  358. this._markModified();
  359. rawArray.push(v);
  360. this._registerAtomic('$addToSet', v);
  361. [].push.call(added, v);
  362. }
  363. }, this);
  364. return added;
  365. },
  366. /**
  367. * Returns the number of pending atomic operations to send to the db for this array.
  368. *
  369. * @api private
  370. * @return {Number}
  371. * @method hasAtomics
  372. * @memberOf MongooseArray
  373. */
  374. hasAtomics() {
  375. if (!utils.isPOJO(this[arrayAtomicsSymbol])) {
  376. return 0;
  377. }
  378. return Object.keys(this[arrayAtomicsSymbol]).length;
  379. },
  380. /**
  381. * Return whether or not the `obj` is included in the array.
  382. *
  383. * @param {Object} obj the item to check
  384. * @param {Number} fromIndex
  385. * @return {Boolean}
  386. * @api public
  387. * @method includes
  388. * @memberOf MongooseArray
  389. */
  390. includes(obj, fromIndex) {
  391. const ret = this.indexOf(obj, fromIndex);
  392. return ret !== -1;
  393. },
  394. /**
  395. * Return the index of `obj` or `-1` if not found.
  396. *
  397. * @param {Object} obj the item to look for
  398. * @param {Number} fromIndex
  399. * @return {Number}
  400. * @api public
  401. * @method indexOf
  402. * @memberOf MongooseArray
  403. */
  404. indexOf(obj, fromIndex) {
  405. if (isBsonType(obj, 'ObjectID')) {
  406. obj = obj.toString();
  407. }
  408. fromIndex = fromIndex == null ? 0 : fromIndex;
  409. const len = this.length;
  410. for (let i = fromIndex; i < len; ++i) {
  411. if (obj == this[i]) {
  412. return i;
  413. }
  414. }
  415. return -1;
  416. },
  417. /**
  418. * Helper for console.log
  419. *
  420. * @api public
  421. * @method inspect
  422. * @memberOf MongooseArray
  423. */
  424. inspect() {
  425. return JSON.stringify(this);
  426. },
  427. /**
  428. * Pushes items to the array non-atomically.
  429. *
  430. * #### Note:
  431. *
  432. * _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
  433. *
  434. * @param {any} [args...]
  435. * @api public
  436. * @method nonAtomicPush
  437. * @memberOf MongooseArray
  438. */
  439. nonAtomicPush() {
  440. const values = [].map.call(arguments, this._mapCast, this);
  441. this._markModified();
  442. const ret = [].push.apply(this, values);
  443. this._registerAtomic('$set', this);
  444. return ret;
  445. },
  446. /**
  447. * Wraps [`Array#pop`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/pop) with proper change tracking.
  448. *
  449. * #### Note:
  450. *
  451. * _marks the entire array as modified which will pass the entire thing to $set potentially overwriting any changes that happen between when you retrieved the object and when you save it._
  452. *
  453. * @see MongooseArray#$pop #types_array_MongooseArray-%24pop
  454. * @api public
  455. * @method pop
  456. * @memberOf MongooseArray
  457. */
  458. pop() {
  459. this._markModified();
  460. const ret = [].pop.call(this);
  461. this._registerAtomic('$set', this);
  462. return ret;
  463. },
  464. /**
  465. * Pulls items from the array atomically. Equality is determined by casting
  466. * the provided value to an embedded document and comparing using
  467. * [the `Document.equals()` function.](/docs/api.html#document_Document-equals)
  468. *
  469. * #### Examples:
  470. *
  471. * doc.array.pull(ObjectId)
  472. * doc.array.pull({ _id: 'someId' })
  473. * doc.array.pull(36)
  474. * doc.array.pull('tag 1', 'tag 2')
  475. *
  476. * To remove a document from a subdocument array we may pass an object with a matching `_id`.
  477. *
  478. * doc.subdocs.push({ _id: 4815162342 })
  479. * doc.subdocs.pull({ _id: 4815162342 }) // removed
  480. *
  481. * Or we may passing the _id directly and let mongoose take care of it.
  482. *
  483. * doc.subdocs.push({ _id: 4815162342 })
  484. * doc.subdocs.pull(4815162342); // works
  485. *
  486. * The first pull call will result in a atomic operation on the database, if pull is called repeatedly without saving the document, a $set operation is used on the complete array instead, overwriting possible changes that happened on the database in the meantime.
  487. *
  488. * @param {any} [args...]
  489. * @see mongodb https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull
  490. * @api public
  491. * @method pull
  492. * @memberOf MongooseArray
  493. */
  494. pull() {
  495. const values = [].map.call(arguments, this._cast, this);
  496. const cur = this[arrayParentSymbol].get(this[arrayPathSymbol]);
  497. let i = cur.length;
  498. let mem;
  499. this._markModified();
  500. while (i--) {
  501. mem = cur[i];
  502. if (mem instanceof Document) {
  503. const some = values.some(function(v) {
  504. return mem.equals(v);
  505. });
  506. if (some) {
  507. [].splice.call(cur, i, 1);
  508. }
  509. } else if (~cur.indexOf.call(values, mem)) {
  510. [].splice.call(cur, i, 1);
  511. }
  512. }
  513. if (values[0] instanceof ArraySubdocument) {
  514. this._registerAtomic('$pullDocs', values.map(function(v) {
  515. return v.$__getValue('_id') || v;
  516. }));
  517. } else {
  518. this._registerAtomic('$pullAll', values);
  519. }
  520. // Might have modified child paths and then pulled, like
  521. // `doc.children[1].name = 'test';` followed by
  522. // `doc.children.remove(doc.children[0]);`. In this case we fall back
  523. // to a `$set` on the whole array. See #3511
  524. if (cleanModifiedSubpaths(this[arrayParentSymbol], this[arrayPathSymbol]) > 0) {
  525. this._registerAtomic('$set', this);
  526. }
  527. return this;
  528. },
  529. /**
  530. * Wraps [`Array#push`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/push) with proper change tracking.
  531. *
  532. * #### Example:
  533. *
  534. * const schema = Schema({ nums: [Number] });
  535. * const Model = mongoose.model('Test', schema);
  536. *
  537. * const doc = await Model.create({ nums: [3, 4] });
  538. * doc.nums.push(5); // Add 5 to the end of the array
  539. * await doc.save();
  540. *
  541. * // You can also pass an object with `$each` as the
  542. * // first parameter to use MongoDB's `$position`
  543. * doc.nums.push({
  544. * $each: [1, 2],
  545. * $position: 0
  546. * });
  547. * doc.nums; // [1, 2, 3, 4, 5]
  548. *
  549. * @param {Object} [args...]
  550. * @api public
  551. * @method push
  552. * @memberOf MongooseArray
  553. */
  554. push() {
  555. let values = arguments;
  556. let atomic = values;
  557. const isOverwrite = values[0] != null &&
  558. utils.hasUserDefinedProperty(values[0], '$each');
  559. const arr = utils.isMongooseArray(this) ? this.__array : this;
  560. if (isOverwrite) {
  561. atomic = values[0];
  562. values = values[0].$each;
  563. }
  564. if (this[arraySchemaSymbol] == null) {
  565. return _basePush.apply(this, values);
  566. }
  567. _checkManualPopulation(this, values);
  568. const parent = this[arrayParentSymbol];
  569. values = [].map.call(values, this._mapCast, this);
  570. values = this[arraySchemaSymbol].applySetters(values, parent, undefined,
  571. undefined, { skipDocumentArrayCast: true });
  572. let ret;
  573. const atomics = this[arrayAtomicsSymbol];
  574. this._markModified();
  575. if (isOverwrite) {
  576. atomic.$each = values;
  577. if ((atomics.$push && atomics.$push.$each && atomics.$push.$each.length || 0) !== 0 &&
  578. atomics.$push.$position != atomic.$position) {
  579. throw new MongooseError('Cannot call `Array#push()` multiple times ' +
  580. 'with different `$position`');
  581. }
  582. if (atomic.$position != null) {
  583. [].splice.apply(arr, [atomic.$position, 0].concat(values));
  584. ret = this.length;
  585. } else {
  586. ret = [].push.apply(arr, values);
  587. }
  588. } else {
  589. if ((atomics.$push && atomics.$push.$each && atomics.$push.$each.length || 0) !== 0 &&
  590. atomics.$push.$position != null) {
  591. throw new MongooseError('Cannot call `Array#push()` multiple times ' +
  592. 'with different `$position`');
  593. }
  594. atomic = values;
  595. ret = [].push.apply(arr, values);
  596. }
  597. this._registerAtomic('$push', atomic);
  598. return ret;
  599. },
  600. /**
  601. * Alias of [pull](#mongoosearray_MongooseArray-pull)
  602. *
  603. * @see MongooseArray#pull #types_array_MongooseArray-pull
  604. * @see mongodb https://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull
  605. * @api public
  606. * @memberOf MongooseArray
  607. * @instance
  608. * @method remove
  609. */
  610. remove() {
  611. return this.pull.apply(this, arguments);
  612. },
  613. /**
  614. * Sets the casted `val` at index `i` and marks the array modified.
  615. *
  616. * #### Example:
  617. *
  618. * // given documents based on the following
  619. * const Doc = mongoose.model('Doc', new Schema({ array: [Number] }));
  620. *
  621. * const doc = new Doc({ array: [2,3,4] })
  622. *
  623. * console.log(doc.array) // [2,3,4]
  624. *
  625. * doc.array.set(1,"5");
  626. * console.log(doc.array); // [2,5,4] // properly cast to number
  627. * doc.save() // the change is saved
  628. *
  629. * // VS not using array#set
  630. * doc.array[1] = "5";
  631. * console.log(doc.array); // [2,"5",4] // no casting
  632. * doc.save() // change is not saved
  633. *
  634. * @return {Array} this
  635. * @api public
  636. * @method set
  637. * @memberOf MongooseArray
  638. */
  639. set(i, val, skipModified) {
  640. const arr = this.__array;
  641. if (skipModified) {
  642. arr[i] = val;
  643. return this;
  644. }
  645. const value = methods._cast.call(this, val, i);
  646. methods._markModified.call(this, i);
  647. arr[i] = value;
  648. return this;
  649. },
  650. /**
  651. * Wraps [`Array#shift`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/unshift) with proper change tracking.
  652. *
  653. * #### Example:
  654. *
  655. * doc.array = [2,3];
  656. * const res = doc.array.shift();
  657. * console.log(res) // 2
  658. * console.log(doc.array) // [3]
  659. *
  660. * #### Note:
  661. *
  662. * _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
  663. *
  664. * @api public
  665. * @method shift
  666. * @memberOf MongooseArray
  667. */
  668. shift() {
  669. const arr = utils.isMongooseArray(this) ? this.__array : this;
  670. this._markModified();
  671. const ret = [].shift.call(arr);
  672. this._registerAtomic('$set', this);
  673. return ret;
  674. },
  675. /**
  676. * Wraps [`Array#sort`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/sort) with proper change tracking.
  677. *
  678. * #### Note:
  679. *
  680. * _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
  681. *
  682. * @api public
  683. * @method sort
  684. * @memberOf MongooseArray
  685. * @see https://masteringjs.io/tutorials/fundamentals/array-sort
  686. */
  687. sort() {
  688. const arr = utils.isMongooseArray(this) ? this.__array : this;
  689. const ret = [].sort.apply(arr, arguments);
  690. this._registerAtomic('$set', this);
  691. return ret;
  692. },
  693. /**
  694. * Wraps [`Array#splice`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/splice) with proper change tracking and casting.
  695. *
  696. * #### Note:
  697. *
  698. * _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
  699. *
  700. * @api public
  701. * @method splice
  702. * @memberOf MongooseArray
  703. * @see https://masteringjs.io/tutorials/fundamentals/array-splice
  704. */
  705. splice() {
  706. let ret;
  707. const arr = utils.isMongooseArray(this) ? this.__array : this;
  708. _checkManualPopulation(this, Array.prototype.slice.call(arguments, 2));
  709. if (arguments.length) {
  710. let vals;
  711. if (this[arraySchemaSymbol] == null) {
  712. vals = arguments;
  713. } else {
  714. vals = [];
  715. for (let i = 0; i < arguments.length; ++i) {
  716. vals[i] = i < 2 ?
  717. arguments[i] :
  718. this._cast(arguments[i], arguments[0] + (i - 2));
  719. }
  720. }
  721. ret = [].splice.apply(arr, vals);
  722. this._registerAtomic('$set', this);
  723. }
  724. return ret;
  725. },
  726. /*!
  727. * ignore
  728. */
  729. toBSON() {
  730. return this.toObject(internalToObjectOptions);
  731. },
  732. /**
  733. * Returns a native js Array.
  734. *
  735. * @param {Object} options
  736. * @return {Array}
  737. * @api public
  738. * @method toObject
  739. * @memberOf MongooseArray
  740. */
  741. toObject(options) {
  742. const arr = utils.isMongooseArray(this) ? this.__array : this;
  743. if (options && options.depopulate) {
  744. options = utils.clone(options);
  745. options._isNested = true;
  746. // Ensure return value is a vanilla array, because in Node.js 6+ `map()`
  747. // is smart enough to use the inherited array's constructor.
  748. return [].concat(arr).map(function(doc) {
  749. return doc instanceof Document
  750. ? doc.toObject(options)
  751. : doc;
  752. });
  753. }
  754. return [].concat(arr);
  755. },
  756. $toObject() {
  757. return this.constructor.prototype.toObject.apply(this, arguments);
  758. },
  759. /**
  760. * Wraps [`Array#unshift`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/unshift) with proper change tracking.
  761. *
  762. * #### Note:
  763. *
  764. * _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwriting any changes that happen between when you retrieved the object and when you save it._
  765. *
  766. * @api public
  767. * @method unshift
  768. * @memberOf MongooseArray
  769. */
  770. unshift() {
  771. _checkManualPopulation(this, arguments);
  772. let values;
  773. if (this[arraySchemaSymbol] == null) {
  774. values = arguments;
  775. } else {
  776. values = [].map.call(arguments, this._cast, this);
  777. values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol]);
  778. }
  779. const arr = utils.isMongooseArray(this) ? this.__array : this;
  780. this._markModified();
  781. [].unshift.apply(arr, values);
  782. this._registerAtomic('$set', this);
  783. return this.length;
  784. }
  785. };
  786. /*!
  787. * ignore
  788. */
  789. function _isAllSubdocs(docs, ref) {
  790. if (!ref) {
  791. return false;
  792. }
  793. for (const arg of docs) {
  794. if (arg == null) {
  795. return false;
  796. }
  797. const model = arg.constructor;
  798. if (!(arg instanceof Document) ||
  799. (model.modelName !== ref && model.baseModelName !== ref)) {
  800. return false;
  801. }
  802. }
  803. return true;
  804. }
  805. /*!
  806. * ignore
  807. */
  808. function _checkManualPopulation(arr, docs) {
  809. const ref = arr == null ?
  810. null :
  811. arr[arraySchemaSymbol] && arr[arraySchemaSymbol].caster && arr[arraySchemaSymbol].caster.options && arr[arraySchemaSymbol].caster.options.ref || null;
  812. if (arr.length === 0 &&
  813. docs.length !== 0) {
  814. if (_isAllSubdocs(docs, ref)) {
  815. arr[arrayParentSymbol].$populated(arr[arrayPathSymbol], [], {
  816. [populateModelSymbol]: docs[0].constructor
  817. });
  818. }
  819. }
  820. }
  821. const returnVanillaArrayMethods = [
  822. 'filter',
  823. 'flat',
  824. 'flatMap',
  825. 'map',
  826. 'slice'
  827. ];
  828. for (const method of returnVanillaArrayMethods) {
  829. if (Array.prototype[method] == null) {
  830. continue;
  831. }
  832. methods[method] = function() {
  833. const _arr = utils.isMongooseArray(this) ? this.__array : this;
  834. const arr = [].concat(_arr);
  835. return arr[method].apply(arr, arguments);
  836. };
  837. }
  838. module.exports = methods;