UNPKG

20.7 kBJavaScriptView Raw
1'use strict';
2
3/*!
4 * Module dependencies.
5 */
6
7const Decimal = require('./types/decimal128');
8const ObjectId = require('./types/objectid');
9const PromiseProvider = require('./promise_provider');
10const cloneRegExp = require('regexp-clone');
11const get = require('./helpers/get');
12const sliced = require('sliced');
13const mpath = require('mpath');
14const ms = require('ms');
15const Buffer = require('safe-buffer').Buffer;
16
17const emittedSymbol = Symbol.for('mongoose:emitted');
18
19let MongooseBuffer;
20let MongooseArray;
21let Document;
22
23const specialProperties = new Set(['__proto__', 'constructor', 'prototype']);
24
25exports.specialProperties = specialProperties;
26
27/*!
28 * Produces a collection name from model `name`. By default, just returns
29 * the model name
30 *
31 * @param {String} name a model name
32 * @param {Function} pluralize function that pluralizes the collection name
33 * @return {String} a collection name
34 * @api private
35 */
36
37exports.toCollectionName = function(name, pluralize) {
38 if (name === 'system.profile') {
39 return name;
40 }
41 if (name === 'system.indexes') {
42 return name;
43 }
44 if (typeof pluralize === 'function') {
45 return pluralize(name);
46 }
47 return name;
48};
49
50/*!
51 * Determines if `a` and `b` are deep equal.
52 *
53 * Modified from node/lib/assert.js
54 *
55 * @param {any} a a value to compare to `b`
56 * @param {any} b a value to compare to `a`
57 * @return {Boolean}
58 * @api private
59 */
60
61exports.deepEqual = function deepEqual(a, b) {
62 if (a === b) {
63 return true;
64 }
65
66 if (a instanceof Date && b instanceof Date) {
67 return a.getTime() === b.getTime();
68 }
69
70 if ((isBsonType(a, 'ObjectID') && isBsonType(b, 'ObjectID')) ||
71 (isBsonType(a, 'Decimal128') && isBsonType(b, 'Decimal128'))) {
72 return a.toString() === b.toString();
73 }
74
75 if (a instanceof RegExp && b instanceof RegExp) {
76 return a.source === b.source &&
77 a.ignoreCase === b.ignoreCase &&
78 a.multiline === b.multiline &&
79 a.global === b.global;
80 }
81
82 if (typeof a !== 'object' && typeof b !== 'object') {
83 return a == b;
84 }
85
86 if (a === null || b === null || a === undefined || b === undefined) {
87 return false;
88 }
89
90 if (a.prototype !== b.prototype) {
91 return false;
92 }
93
94 // Handle MongooseNumbers
95 if (a instanceof Number && b instanceof Number) {
96 return a.valueOf() === b.valueOf();
97 }
98
99 if (Buffer.isBuffer(a)) {
100 return exports.buffer.areEqual(a, b);
101 }
102
103 if (isMongooseObject(a)) {
104 a = a.toObject();
105 }
106 if (isMongooseObject(b)) {
107 b = b.toObject();
108 }
109
110 let ka;
111 let kb;
112 let key;
113 let i;
114 try {
115 ka = Object.keys(a);
116 kb = Object.keys(b);
117 } catch (e) {
118 // happens when one is a string literal and the other isn't
119 return false;
120 }
121
122 // having the same number of owned properties (keys incorporates
123 // hasOwnProperty)
124 if (ka.length !== kb.length) {
125 return false;
126 }
127
128 // the same set of keys (although not necessarily the same order),
129 ka.sort();
130 kb.sort();
131
132 // ~~~cheap key test
133 for (i = ka.length - 1; i >= 0; i--) {
134 if (ka[i] !== kb[i]) {
135 return false;
136 }
137 }
138
139 // equivalent values for every corresponding key, and
140 // ~~~possibly expensive deep test
141 for (i = ka.length - 1; i >= 0; i--) {
142 key = ka[i];
143 if (!deepEqual(a[key], b[key])) {
144 return false;
145 }
146 }
147
148 return true;
149};
150
151/*!
152 * Get the bson type, if it exists
153 */
154
155function isBsonType(obj, typename) {
156 return get(obj, '_bsontype', void 0) === typename;
157}
158
159/*!
160 * Get the last element of an array
161 */
162
163exports.last = function(arr) {
164 if (arr.length > 0) {
165 return arr[arr.length - 1];
166 }
167 return void 0;
168};
169
170/*!
171 * Object clone with Mongoose natives support.
172 *
173 * If options.minimize is true, creates a minimal data object. Empty objects and undefined values will not be cloned. This makes the data payload sent to MongoDB as small as possible.
174 *
175 * Functions are never cloned.
176 *
177 * @param {Object} obj the object to clone
178 * @param {Object} options
179 * @param {Boolean} isArrayChild true if cloning immediately underneath an array. Special case for minimize.
180 * @return {Object} the cloned object
181 * @api private
182 */
183
184exports.clone = function clone(obj, options, isArrayChild) {
185 if (obj == null) {
186 return obj;
187 }
188
189 if (Array.isArray(obj)) {
190 return cloneArray(obj, options);
191 }
192
193 if (isMongooseObject(obj)) {
194 if (options && options.json && typeof obj.toJSON === 'function') {
195 return obj.toJSON(options);
196 }
197 return obj.toObject(options);
198 }
199
200 if (obj.constructor) {
201 switch (exports.getFunctionName(obj.constructor)) {
202 case 'Object':
203 return cloneObject(obj, options, isArrayChild);
204 case 'Date':
205 return new obj.constructor(+obj);
206 case 'RegExp':
207 return cloneRegExp(obj);
208 default:
209 // ignore
210 break;
211 }
212 }
213
214 if (obj instanceof ObjectId) {
215 return new ObjectId(obj.id);
216 }
217 if (isBsonType(obj, 'Decimal128')) {
218 if (options && options.flattenDecimals) {
219 return obj.toJSON();
220 }
221 return Decimal.fromString(obj.toString());
222 }
223
224 if (!obj.constructor && exports.isObject(obj)) {
225 // object created with Object.create(null)
226 return cloneObject(obj, options, isArrayChild);
227 }
228
229 if (obj.valueOf) {
230 return obj.valueOf();
231 }
232
233 return cloneObject(obj, options, isArrayChild);
234};
235const clone = exports.clone;
236
237/*!
238 * ignore
239 */
240
241exports.promiseOrCallback = function promiseOrCallback(callback, fn, ee) {
242 if (typeof callback === 'function') {
243 return fn(function(error) {
244 if (error != null) {
245 if (ee != null && ee.listeners('error').length > 0 && !error[emittedSymbol]) {
246 error[emittedSymbol] = true;
247 ee.emit('error', error);
248 }
249 try {
250 callback(error);
251 } catch (error) {
252 return process.nextTick(() => {
253 throw error;
254 });
255 }
256 return;
257 }
258 callback.apply(this, arguments);
259 });
260 }
261
262 const Promise = PromiseProvider.get();
263
264 return new Promise((resolve, reject) => {
265 fn(function(error, res) {
266 if (error != null) {
267 if (ee != null && ee.listeners('error').length > 0 && !error[emittedSymbol]) {
268 error[emittedSymbol] = true;
269 ee.emit('error', error);
270 }
271 return reject(error);
272 }
273 if (arguments.length > 2) {
274 return resolve(Array.prototype.slice.call(arguments, 1));
275 }
276 resolve(res);
277 });
278 });
279};
280
281/*!
282 * ignore
283 */
284
285function cloneObject(obj, options, isArrayChild) {
286 const minimize = options && options.minimize;
287 const ret = {};
288 let hasKeys;
289
290 for (const k in obj) {
291 if (specialProperties.has(k)) {
292 continue;
293 }
294
295 // Don't pass `isArrayChild` down
296 const val = clone(obj[k], options);
297
298 if (!minimize || (typeof val !== 'undefined')) {
299 hasKeys || (hasKeys = true);
300 ret[k] = val;
301 }
302 }
303
304 return minimize && !isArrayChild ? hasKeys && ret : ret;
305}
306
307function cloneArray(arr, options) {
308 const ret = [];
309 for (let i = 0, l = arr.length; i < l; i++) {
310 ret.push(clone(arr[i], options, true));
311 }
312 return ret;
313}
314
315/*!
316 * Shallow copies defaults into options.
317 *
318 * @param {Object} defaults
319 * @param {Object} options
320 * @return {Object} the merged object
321 * @api private
322 */
323
324exports.options = function(defaults, options) {
325 const keys = Object.keys(defaults);
326 let i = keys.length;
327 let k;
328
329 options = options || {};
330
331 while (i--) {
332 k = keys[i];
333 if (!(k in options)) {
334 options[k] = defaults[k];
335 }
336 }
337
338 return options;
339};
340
341/*!
342 * Generates a random string
343 *
344 * @api private
345 */
346
347exports.random = function() {
348 return Math.random().toString().substr(3);
349};
350
351/*!
352 * Merges `from` into `to` without overwriting existing properties.
353 *
354 * @param {Object} to
355 * @param {Object} from
356 * @api private
357 */
358
359exports.merge = function merge(to, from, options, path) {
360 options = options || {};
361
362 const keys = Object.keys(from);
363 let i = 0;
364 const len = keys.length;
365 let key;
366
367 path = path || '';
368 const omitNested = options.omitNested || {};
369
370 while (i < len) {
371 key = keys[i++];
372 if (options.omit && options.omit[key]) {
373 continue;
374 }
375 if (omitNested[path]) {
376 continue;
377 }
378 if (specialProperties.has(key)) {
379 continue;
380 }
381 if (to[key] == null) {
382 to[key] = from[key];
383 } else if (exports.isObject(from[key])) {
384 if (!exports.isObject(to[key])) {
385 to[key] = {};
386 }
387 if (from[key] != null) {
388 if (from[key].instanceOfSchema) {
389 to[key] = from[key].clone();
390 continue;
391 } else if (from[key] instanceof ObjectId) {
392 to[key] = new ObjectId(from[key]);
393 continue;
394 }
395 }
396 merge(to[key], from[key], options, path ? path + '.' + key : key);
397 } else if (options.overwrite) {
398 to[key] = from[key];
399 }
400 }
401};
402
403/*!
404 * Applies toObject recursively.
405 *
406 * @param {Document|Array|Object} obj
407 * @return {Object}
408 * @api private
409 */
410
411exports.toObject = function toObject(obj) {
412 Document || (Document = require('./document'));
413 let ret;
414
415 if (obj == null) {
416 return obj;
417 }
418
419 if (obj instanceof Document) {
420 return obj.toObject();
421 }
422
423 if (Array.isArray(obj)) {
424 ret = [];
425
426 for (let i = 0, len = obj.length; i < len; ++i) {
427 ret.push(toObject(obj[i]));
428 }
429
430 return ret;
431 }
432
433 if ((obj.constructor && exports.getFunctionName(obj.constructor) === 'Object') ||
434 (!obj.constructor && exports.isObject(obj))) {
435 ret = {};
436
437 for (const k in obj) {
438 if (specialProperties.has(k)) {
439 continue;
440 }
441 ret[k] = toObject(obj[k]);
442 }
443
444 return ret;
445 }
446
447 return obj;
448};
449
450/*!
451 * Determines if `arg` is an object.
452 *
453 * @param {Object|Array|String|Function|RegExp|any} arg
454 * @api private
455 * @return {Boolean}
456 */
457
458exports.isObject = function(arg) {
459 if (Buffer.isBuffer(arg)) {
460 return true;
461 }
462 return Object.prototype.toString.call(arg) === '[object Object]';
463};
464
465/*!
466 * Determines if `arg` is a plain object.
467 *
468 * @param {Object|Array|String|Function|RegExp|any} arg
469 * @api private
470 * @return {Boolean}
471 */
472
473exports.isPOJO = function(arg) {
474 return arg instanceof Object && arg.constructor.name === 'Object';
475};
476
477/*!
478 * A faster Array.prototype.slice.call(arguments) alternative
479 * @api private
480 */
481
482exports.args = sliced;
483
484/*!
485 * process.nextTick helper.
486 *
487 * Wraps `callback` in a try/catch + nextTick.
488 *
489 * node-mongodb-native has a habit of state corruption when an error is immediately thrown from within a collection callback.
490 *
491 * @param {Function} callback
492 * @api private
493 */
494
495exports.tick = function tick(callback) {
496 if (typeof callback !== 'function') {
497 return;
498 }
499 return function() {
500 try {
501 callback.apply(this, arguments);
502 } catch (err) {
503 // only nextTick on err to get out of
504 // the event loop and avoid state corruption.
505 process.nextTick(function() {
506 throw err;
507 });
508 }
509 };
510};
511
512/*!
513 * Returns if `v` is a mongoose object that has a `toObject()` method we can use.
514 *
515 * This is for compatibility with libs like Date.js which do foolish things to Natives.
516 *
517 * @param {any} v
518 * @api private
519 */
520
521exports.isMongooseObject = function(v) {
522 Document || (Document = require('./document'));
523 MongooseArray || (MongooseArray = require('./types').Array);
524 MongooseBuffer || (MongooseBuffer = require('./types').Buffer);
525
526 if (v == null) {
527 return false;
528 }
529
530 return v.$__ != null || // Document
531 v.isMongooseArray || // Array or Document Array
532 v.isMongooseBuffer || // Buffer
533 v.$isMongooseMap; // Map
534};
535
536const isMongooseObject = exports.isMongooseObject;
537
538/*!
539 * Converts `expires` options of index objects to `expiresAfterSeconds` options for MongoDB.
540 *
541 * @param {Object} object
542 * @api private
543 */
544
545exports.expires = function expires(object) {
546 if (!(object && object.constructor.name === 'Object')) {
547 return;
548 }
549 if (!('expires' in object)) {
550 return;
551 }
552
553 let when;
554 if (typeof object.expires !== 'string') {
555 when = object.expires;
556 } else {
557 when = Math.round(ms(object.expires) / 1000);
558 }
559 object.expireAfterSeconds = when;
560 delete object.expires;
561};
562
563/*!
564 * Populate options constructor
565 */
566
567function PopulateOptions(obj) {
568 this.path = obj.path;
569 this.match = obj.match;
570 this.select = obj.select;
571 this.options = obj.options;
572 this.model = obj.model;
573 if (typeof obj.subPopulate === 'object') {
574 this.populate = obj.subPopulate;
575 }
576 if (obj.justOne != null) {
577 this.justOne = obj.justOne;
578 }
579 if (obj.count != null) {
580 this.count = obj.count;
581 }
582 this._docs = {};
583}
584
585// make it compatible with utils.clone
586PopulateOptions.prototype.constructor = Object;
587
588// expose
589exports.PopulateOptions = PopulateOptions;
590
591/*!
592 * populate helper
593 */
594
595exports.populate = function populate(path, select, model, match, options, subPopulate, justOne, count) {
596 // The order of select/conditions args is opposite Model.find but
597 // necessary to keep backward compatibility (select could be
598 // an array, string, or object literal).
599 function makeSingles(arr) {
600 const ret = [];
601 arr.forEach(function(obj) {
602 if (/[\s]/.test(obj.path)) {
603 const paths = obj.path.split(' ');
604 paths.forEach(function(p) {
605 const copy = Object.assign({}, obj);
606 copy.path = p;
607 ret.push(copy);
608 });
609 } else {
610 ret.push(obj);
611 }
612 });
613
614 return ret;
615 }
616
617 // might have passed an object specifying all arguments
618 if (arguments.length === 1) {
619 if (path instanceof PopulateOptions) {
620 return [path];
621 }
622
623 if (Array.isArray(path)) {
624 const singles = makeSingles(path);
625 return singles.map(function(o) {
626 if (o.populate && !(o.match || o.options)) {
627 return exports.populate(o)[0];
628 } else {
629 return exports.populate(o)[0];
630 }
631 });
632 }
633
634 if (exports.isObject(path)) {
635 match = path.match;
636 options = path.options;
637 select = path.select;
638 model = path.model;
639 subPopulate = path.populate;
640 justOne = path.justOne;
641 path = path.path;
642 count = path.count;
643 }
644 } else if (typeof model === 'object') {
645 options = match;
646 match = model;
647 model = undefined;
648 }
649
650 if (typeof path !== 'string') {
651 throw new TypeError('utils.populate: invalid path. Expected string. Got typeof `' + typeof path + '`');
652 }
653
654 if (Array.isArray(subPopulate)) {
655 const ret = [];
656 subPopulate.forEach(function(obj) {
657 if (/[\s]/.test(obj.path)) {
658 const copy = Object.assign({}, obj);
659 const paths = copy.path.split(' ');
660 paths.forEach(function(p) {
661 copy.path = p;
662 ret.push(exports.populate(copy)[0]);
663 });
664 } else {
665 ret.push(exports.populate(obj)[0]);
666 }
667 });
668 subPopulate = exports.populate(ret);
669 } else if (typeof subPopulate === 'object') {
670 subPopulate = exports.populate(subPopulate);
671 }
672
673 const ret = [];
674 const paths = path.split(' ');
675 options = exports.clone(options);
676 for (let i = 0; i < paths.length; ++i) {
677 ret.push(new PopulateOptions({
678 path: paths[i],
679 select: select,
680 match: match,
681 options: options,
682 model: model,
683 subPopulate: subPopulate,
684 justOne: justOne,
685 count: count
686 }));
687 }
688
689 return ret;
690};
691
692/*!
693 * Return the value of `obj` at the given `path`.
694 *
695 * @param {String} path
696 * @param {Object} obj
697 */
698
699exports.getValue = function(path, obj, map) {
700 return mpath.get(path, obj, '_doc', map);
701};
702
703/*!
704 * Sets the value of `obj` at the given `path`.
705 *
706 * @param {String} path
707 * @param {Anything} val
708 * @param {Object} obj
709 */
710
711exports.setValue = function(path, val, obj, map, _copying) {
712 mpath.set(path, val, obj, '_doc', map, _copying);
713};
714
715/*!
716 * Returns an array of values from object `o`.
717 *
718 * @param {Object} o
719 * @return {Array}
720 * @private
721 */
722
723exports.object = {};
724exports.object.vals = function vals(o) {
725 const keys = Object.keys(o);
726 let i = keys.length;
727 const ret = [];
728
729 while (i--) {
730 ret.push(o[keys[i]]);
731 }
732
733 return ret;
734};
735
736/*!
737 * @see exports.options
738 */
739
740exports.object.shallowCopy = exports.options;
741
742/*!
743 * Safer helper for hasOwnProperty checks
744 *
745 * @param {Object} obj
746 * @param {String} prop
747 */
748
749const hop = Object.prototype.hasOwnProperty;
750exports.object.hasOwnProperty = function(obj, prop) {
751 return hop.call(obj, prop);
752};
753
754/*!
755 * Determine if `val` is null or undefined
756 *
757 * @return {Boolean}
758 */
759
760exports.isNullOrUndefined = function(val) {
761 return val === null || val === undefined;
762};
763
764/*!
765 * ignore
766 */
767
768exports.array = {};
769
770/*!
771 * Flattens an array.
772 *
773 * [ 1, [ 2, 3, [4] ]] -> [1,2,3,4]
774 *
775 * @param {Array} arr
776 * @param {Function} [filter] If passed, will be invoked with each item in the array. If `filter` returns a falsey value, the item will not be included in the results.
777 * @return {Array}
778 * @private
779 */
780
781exports.array.flatten = function flatten(arr, filter, ret) {
782 ret || (ret = []);
783
784 arr.forEach(function(item) {
785 if (Array.isArray(item)) {
786 flatten(item, filter, ret);
787 } else {
788 if (!filter || filter(item)) {
789 ret.push(item);
790 }
791 }
792 });
793
794 return ret;
795};
796
797/*!
798 * Removes duplicate values from an array
799 *
800 * [1, 2, 3, 3, 5] => [1, 2, 3, 5]
801 * [ ObjectId("550988ba0c19d57f697dc45e"), ObjectId("550988ba0c19d57f697dc45e") ]
802 * => [ObjectId("550988ba0c19d57f697dc45e")]
803 *
804 * @param {Array} arr
805 * @return {Array}
806 * @private
807 */
808
809exports.array.unique = function(arr) {
810 const primitives = {};
811 const ids = {};
812 const ret = [];
813 const length = arr.length;
814 for (let i = 0; i < length; ++i) {
815 if (typeof arr[i] === 'number' || typeof arr[i] === 'string' || arr[i] == null) {
816 if (primitives[arr[i]]) {
817 continue;
818 }
819 ret.push(arr[i]);
820 primitives[arr[i]] = true;
821 } else if (arr[i] instanceof ObjectId) {
822 if (ids[arr[i].toString()]) {
823 continue;
824 }
825 ret.push(arr[i]);
826 ids[arr[i].toString()] = true;
827 } else {
828 ret.push(arr[i]);
829 }
830 }
831
832 return ret;
833};
834
835/*!
836 * Determines if two buffers are equal.
837 *
838 * @param {Buffer} a
839 * @param {Object} b
840 */
841
842exports.buffer = {};
843exports.buffer.areEqual = function(a, b) {
844 if (!Buffer.isBuffer(a)) {
845 return false;
846 }
847 if (!Buffer.isBuffer(b)) {
848 return false;
849 }
850 if (a.length !== b.length) {
851 return false;
852 }
853 for (let i = 0, len = a.length; i < len; ++i) {
854 if (a[i] !== b[i]) {
855 return false;
856 }
857 }
858 return true;
859};
860
861exports.getFunctionName = function(fn) {
862 if (fn.name) {
863 return fn.name;
864 }
865 return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1];
866};
867
868/*!
869 * Decorate buffers
870 */
871
872exports.decorate = function(destination, source) {
873 for (const key in source) {
874 if (specialProperties.has(key)) {
875 continue;
876 }
877 destination[key] = source[key];
878 }
879};
880
881/**
882 * merges to with a copy of from
883 *
884 * @param {Object} to
885 * @param {Object} fromObj
886 * @api private
887 */
888
889exports.mergeClone = function(to, fromObj) {
890 if (isMongooseObject(fromObj)) {
891 fromObj = fromObj.toObject({
892 transform: false,
893 virtuals: false,
894 depopulate: true,
895 getters: false,
896 flattenDecimals: false
897 });
898 }
899 const keys = Object.keys(fromObj);
900 const len = keys.length;
901 let i = 0;
902 let key;
903
904 while (i < len) {
905 key = keys[i++];
906 if (specialProperties.has(key)) {
907 continue;
908 }
909 if (typeof to[key] === 'undefined') {
910 to[key] = exports.clone(fromObj[key], {
911 transform: false,
912 virtuals: false,
913 depopulate: true,
914 getters: false,
915 flattenDecimals: false
916 });
917 } else {
918 let val = fromObj[key];
919 if (val != null && val.valueOf && !(val instanceof Date)) {
920 val = val.valueOf();
921 }
922 if (exports.isObject(val)) {
923 let obj = val;
924 if (isMongooseObject(val) && !val.isMongooseBuffer) {
925 obj = obj.toObject({
926 transform: false,
927 virtuals: false,
928 depopulate: true,
929 getters: false,
930 flattenDecimals: false
931 });
932 }
933 if (val.isMongooseBuffer) {
934 obj = Buffer.from(obj);
935 }
936 exports.mergeClone(to[key], obj);
937 } else {
938 to[key] = exports.clone(val, {
939 flattenDecimals: false
940 });
941 }
942 }
943 }
944};
945
946/**
947 * Executes a function on each element of an array (like _.each)
948 *
949 * @param {Array} arr
950 * @param {Function} fn
951 * @api private
952 */
953
954exports.each = function(arr, fn) {
955 for (let i = 0; i < arr.length; ++i) {
956 fn(arr[i]);
957 }
958};
959
960/*!
961 * ignore
962 */
963
964exports.noop = function() {};