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