UNPKG

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