'use strict'; /*! * Module dependencies. */ const Decimal = require('./types/decimal128'); const ObjectId = require('./types/objectid'); const PromiseProvider = require('./promise_provider'); const cloneRegExp = require('regexp-clone'); const get = require('./helpers/get'); const sliced = require('sliced'); const mpath = require('mpath'); const ms = require('ms'); const Buffer = require('safe-buffer').Buffer; const emittedSymbol = Symbol.for('mongoose:emitted'); let MongooseBuffer; let MongooseArray; let Document; const specialProperties = new Set(['__proto__', 'constructor', 'prototype']); exports.specialProperties = specialProperties; /*! * Produces a collection name from model `name`. By default, just returns * the model name * * @param {String} name a model name * @param {Function} pluralize function that pluralizes the collection name * @return {String} a collection name * @api private */ exports.toCollectionName = function(name, pluralize) { if (name === 'system.profile') { return name; } if (name === 'system.indexes') { return name; } if (typeof pluralize === 'function') { return pluralize(name); } return name; }; /*! * Determines if `a` and `b` are deep equal. * * Modified from node/lib/assert.js * * @param {any} a a value to compare to `b` * @param {any} b a value to compare to `a` * @return {Boolean} * @api private */ exports.deepEqual = function deepEqual(a, b) { if (a === b) { return true; } if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime(); } if ((isBsonType(a, 'ObjectID') && isBsonType(b, 'ObjectID')) || (isBsonType(a, 'Decimal128') && isBsonType(b, 'Decimal128'))) { return a.toString() === b.toString(); } if (a instanceof RegExp && b instanceof RegExp) { return a.source === b.source && a.ignoreCase === b.ignoreCase && a.multiline === b.multiline && a.global === b.global; } if (typeof a !== 'object' && typeof b !== 'object') { return a == b; } if (a === null || b === null || a === undefined || b === undefined) { return false; } if (a.prototype !== b.prototype) { return false; } // Handle MongooseNumbers if (a instanceof Number && b instanceof Number) { return a.valueOf() === b.valueOf(); } if (Buffer.isBuffer(a)) { return exports.buffer.areEqual(a, b); } if (isMongooseObject(a)) { a = a.toObject(); } if (isMongooseObject(b)) { b = b.toObject(); } let ka; let kb; let key; let i; try { ka = Object.keys(a); kb = Object.keys(b); } catch (e) { // happens when one is a string literal and the other isn't return false; } // having the same number of owned properties (keys incorporates // hasOwnProperty) if (ka.length !== kb.length) { return false; } // the same set of keys (although not necessarily the same order), ka.sort(); kb.sort(); // ~~~cheap key test for (i = ka.length - 1; i >= 0; i--) { if (ka[i] !== kb[i]) { return false; } } // equivalent values for every corresponding key, and // ~~~possibly expensive deep test for (i = ka.length - 1; i >= 0; i--) { key = ka[i]; if (!deepEqual(a[key], b[key])) { return false; } } return true; }; /*! * Get the bson type, if it exists */ function isBsonType(obj, typename) { return get(obj, '_bsontype', void 0) === typename; } /*! * Get the last element of an array */ exports.last = function(arr) { if (arr.length > 0) { return arr[arr.length - 1]; } return void 0; }; /*! * Object clone with Mongoose natives support. * * 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. * * Functions are never cloned. * * @param {Object} obj the object to clone * @param {Object} options * @param {Boolean} isArrayChild true if cloning immediately underneath an array. Special case for minimize. * @return {Object} the cloned object * @api private */ exports.clone = function clone(obj, options, isArrayChild) { if (obj == null) { return obj; } if (Array.isArray(obj)) { return cloneArray(obj, options); } if (isMongooseObject(obj)) { if (options && options.json && typeof obj.toJSON === 'function') { return obj.toJSON(options); } return obj.toObject(options); } if (obj.constructor) { switch (exports.getFunctionName(obj.constructor)) { case 'Object': return cloneObject(obj, options, isArrayChild); case 'Date': return new obj.constructor(+obj); case 'RegExp': return cloneRegExp(obj); default: // ignore break; } } if (obj instanceof ObjectId) { return new ObjectId(obj.id); } if (isBsonType(obj, 'Decimal128')) { if (options && options.flattenDecimals) { return obj.toJSON(); } return Decimal.fromString(obj.toString()); } if (!obj.constructor && exports.isObject(obj)) { // object created with Object.create(null) return cloneObject(obj, options, isArrayChild); } if (obj.valueOf) { return obj.valueOf(); } return cloneObject(obj, options, isArrayChild); }; const clone = exports.clone; /*! * ignore */ exports.promiseOrCallback = function promiseOrCallback(callback, fn, ee) { if (typeof callback === 'function') { return fn(function(error) { if (error != null) { if (ee != null && ee.listeners('error').length > 0 && !error[emittedSymbol]) { error[emittedSymbol] = true; ee.emit('error', error); } try { callback(error); } catch (error) { return process.nextTick(() => { throw error; }); } return; } callback.apply(this, arguments); }); } const Promise = PromiseProvider.get(); return new Promise((resolve, reject) => { fn(function(error, res) { if (error != null) { if (ee != null && ee.listeners('error').length > 0 && !error[emittedSymbol]) { error[emittedSymbol] = true; ee.emit('error', error); } return reject(error); } if (arguments.length > 2) { return resolve(Array.prototype.slice.call(arguments, 1)); } resolve(res); }); }); }; /*! * ignore */ function cloneObject(obj, options, isArrayChild) { const minimize = options && options.minimize; const ret = {}; let hasKeys; for (const k in obj) { if (specialProperties.has(k)) { continue; } // Don't pass `isArrayChild` down const val = clone(obj[k], options); if (!minimize || (typeof val !== 'undefined')) { hasKeys || (hasKeys = true); ret[k] = val; } } return minimize && !isArrayChild ? hasKeys && ret : ret; } function cloneArray(arr, options) { const ret = []; for (let i = 0, l = arr.length; i < l; i++) { ret.push(clone(arr[i], options, true)); } return ret; } /*! * Shallow copies defaults into options. * * @param {Object} defaults * @param {Object} options * @return {Object} the merged object * @api private */ exports.options = function(defaults, options) { const keys = Object.keys(defaults); let i = keys.length; let k; options = options || {}; while (i--) { k = keys[i]; if (!(k in options)) { options[k] = defaults[k]; } } return options; }; /*! * Generates a random string * * @api private */ exports.random = function() { return Math.random().toString().substr(3); }; /*! * Merges `from` into `to` without overwriting existing properties. * * @param {Object} to * @param {Object} from * @api private */ exports.merge = function merge(to, from, options, path) { options = options || {}; const keys = Object.keys(from); let i = 0; const len = keys.length; let key; path = path || ''; const omitNested = options.omitNested || {}; while (i < len) { key = keys[i++]; if (options.omit && options.omit[key]) { continue; } if (omitNested[path]) { continue; } if (specialProperties.has(key)) { continue; } if (to[key] == null) { to[key] = from[key]; } else if (exports.isObject(from[key])) { if (!exports.isObject(to[key])) { to[key] = {}; } if (from[key] != null) { if (from[key].instanceOfSchema) { to[key] = from[key].clone(); continue; } else if (from[key] instanceof ObjectId) { to[key] = new ObjectId(from[key]); continue; } } merge(to[key], from[key], options, path ? path + '.' + key : key); } else if (options.overwrite) { to[key] = from[key]; } } }; /*! * Applies toObject recursively. * * @param {Document|Array|Object} obj * @return {Object} * @api private */ exports.toObject = function toObject(obj) { Document || (Document = require('./document')); let ret; if (obj == null) { return obj; } if (obj instanceof Document) { return obj.toObject(); } if (Array.isArray(obj)) { ret = []; for (let i = 0, len = obj.length; i < len; ++i) { ret.push(toObject(obj[i])); } return ret; } if ((obj.constructor && exports.getFunctionName(obj.constructor) === 'Object') || (!obj.constructor && exports.isObject(obj))) { ret = {}; for (const k in obj) { if (specialProperties.has(k)) { continue; } ret[k] = toObject(obj[k]); } return ret; } return obj; }; /*! * Determines if `arg` is an object. * * @param {Object|Array|String|Function|RegExp|any} arg * @api private * @return {Boolean} */ exports.isObject = function(arg) { if (Buffer.isBuffer(arg)) { return true; } return Object.prototype.toString.call(arg) === '[object Object]'; }; /*! * Determines if `arg` is a plain object. * * @param {Object|Array|String|Function|RegExp|any} arg * @api private * @return {Boolean} */ exports.isPOJO = function(arg) { return arg instanceof Object && arg.constructor.name === 'Object'; }; /*! * A faster Array.prototype.slice.call(arguments) alternative * @api private */ exports.args = sliced; /*! * process.nextTick helper. * * Wraps `callback` in a try/catch + nextTick. * * node-mongodb-native has a habit of state corruption when an error is immediately thrown from within a collection callback. * * @param {Function} callback * @api private */ exports.tick = function tick(callback) { if (typeof callback !== 'function') { return; } return function() { try { callback.apply(this, arguments); } catch (err) { // only nextTick on err to get out of // the event loop and avoid state corruption. process.nextTick(function() { throw err; }); } }; }; /*! * Returns if `v` is a mongoose object that has a `toObject()` method we can use. * * This is for compatibility with libs like Date.js which do foolish things to Natives. * * @param {any} v * @api private */ exports.isMongooseObject = function(v) { Document || (Document = require('./document')); MongooseArray || (MongooseArray = require('./types').Array); MongooseBuffer || (MongooseBuffer = require('./types').Buffer); if (v == null) { return false; } return v.$__ != null || // Document v.isMongooseArray || // Array or Document Array v.isMongooseBuffer || // Buffer v.$isMongooseMap; // Map }; const isMongooseObject = exports.isMongooseObject; /*! * Converts `expires` options of index objects to `expiresAfterSeconds` options for MongoDB. * * @param {Object} object * @api private */ exports.expires = function expires(object) { if (!(object && object.constructor.name === 'Object')) { return; } if (!('expires' in object)) { return; } let when; if (typeof object.expires !== 'string') { when = object.expires; } else { when = Math.round(ms(object.expires) / 1000); } object.expireAfterSeconds = when; delete object.expires; }; /*! * Populate options constructor */ function PopulateOptions(obj) { this.path = obj.path; this.match = obj.match; this.select = obj.select; this.options = obj.options; this.model = obj.model; if (typeof obj.subPopulate === 'object') { this.populate = obj.subPopulate; } if (obj.justOne != null) { this.justOne = obj.justOne; } if (obj.count != null) { this.count = obj.count; } this._docs = {}; } // make it compatible with utils.clone PopulateOptions.prototype.constructor = Object; // expose exports.PopulateOptions = PopulateOptions; /*! * populate helper */ exports.populate = function populate(path, select, model, match, options, subPopulate, justOne, count) { // The order of select/conditions args is opposite Model.find but // necessary to keep backward compatibility (select could be // an array, string, or object literal). function makeSingles(arr) { const ret = []; arr.forEach(function(obj) { if (/[\s]/.test(obj.path)) { const paths = obj.path.split(' '); paths.forEach(function(p) { const copy = Object.assign({}, obj); copy.path = p; ret.push(copy); }); } else { ret.push(obj); } }); return ret; } // might have passed an object specifying all arguments if (arguments.length === 1) { if (path instanceof PopulateOptions) { return [path]; } if (Array.isArray(path)) { const singles = makeSingles(path); return singles.map(function(o) { if (o.populate && !(o.match || o.options)) { return exports.populate(o)[0]; } else { return exports.populate(o)[0]; } }); } if (exports.isObject(path)) { match = path.match; options = path.options; select = path.select; model = path.model; subPopulate = path.populate; justOne = path.justOne; path = path.path; count = path.count; } } else if (typeof model === 'object') { options = match; match = model; model = undefined; } if (typeof path !== 'string') { throw new TypeError('utils.populate: invalid path. Expected string. Got typeof `' + typeof path + '`'); } if (Array.isArray(subPopulate)) { const ret = []; subPopulate.forEach(function(obj) { if (/[\s]/.test(obj.path)) { const copy = Object.assign({}, obj); const paths = copy.path.split(' '); paths.forEach(function(p) { copy.path = p; ret.push(exports.populate(copy)[0]); }); } else { ret.push(exports.populate(obj)[0]); } }); subPopulate = exports.populate(ret); } else if (typeof subPopulate === 'object') { subPopulate = exports.populate(subPopulate); } const ret = []; const paths = path.split(' '); options = exports.clone(options); for (let i = 0; i < paths.length; ++i) { ret.push(new PopulateOptions({ path: paths[i], select: select, match: match, options: options, model: model, subPopulate: subPopulate, justOne: justOne, count: count })); } return ret; }; /*! * Return the value of `obj` at the given `path`. * * @param {String} path * @param {Object} obj */ exports.getValue = function(path, obj, map) { return mpath.get(path, obj, '_doc', map); }; /*! * Sets the value of `obj` at the given `path`. * * @param {String} path * @param {Anything} val * @param {Object} obj */ exports.setValue = function(path, val, obj, map, _copying) { mpath.set(path, val, obj, '_doc', map, _copying); }; /*! * Returns an array of values from object `o`. * * @param {Object} o * @return {Array} * @private */ exports.object = {}; exports.object.vals = function vals(o) { const keys = Object.keys(o); let i = keys.length; const ret = []; while (i--) { ret.push(o[keys[i]]); } return ret; }; /*! * @see exports.options */ exports.object.shallowCopy = exports.options; /*! * Safer helper for hasOwnProperty checks * * @param {Object} obj * @param {String} prop */ const hop = Object.prototype.hasOwnProperty; exports.object.hasOwnProperty = function(obj, prop) { return hop.call(obj, prop); }; /*! * Determine if `val` is null or undefined * * @return {Boolean} */ exports.isNullOrUndefined = function(val) { return val === null || val === undefined; }; /*! * ignore */ exports.array = {}; /*! * Flattens an array. * * [ 1, [ 2, 3, [4] ]] -> [1,2,3,4] * * @param {Array} arr * @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. * @return {Array} * @private */ exports.array.flatten = function flatten(arr, filter, ret) { ret || (ret = []); arr.forEach(function(item) { if (Array.isArray(item)) { flatten(item, filter, ret); } else { if (!filter || filter(item)) { ret.push(item); } } }); return ret; }; /*! * Removes duplicate values from an array * * [1, 2, 3, 3, 5] => [1, 2, 3, 5] * [ ObjectId("550988ba0c19d57f697dc45e"), ObjectId("550988ba0c19d57f697dc45e") ] * => [ObjectId("550988ba0c19d57f697dc45e")] * * @param {Array} arr * @return {Array} * @private */ exports.array.unique = function(arr) { const primitives = {}; const ids = {}; const ret = []; const length = arr.length; for (let i = 0; i < length; ++i) { if (typeof arr[i] === 'number' || typeof arr[i] === 'string' || arr[i] == null) { if (primitives[arr[i]]) { continue; } ret.push(arr[i]); primitives[arr[i]] = true; } else if (arr[i] instanceof ObjectId) { if (ids[arr[i].toString()]) { continue; } ret.push(arr[i]); ids[arr[i].toString()] = true; } else { ret.push(arr[i]); } } return ret; }; /*! * Determines if two buffers are equal. * * @param {Buffer} a * @param {Object} b */ exports.buffer = {}; exports.buffer.areEqual = function(a, b) { if (!Buffer.isBuffer(a)) { return false; } if (!Buffer.isBuffer(b)) { return false; } if (a.length !== b.length) { return false; } for (let i = 0, len = a.length; i < len; ++i) { if (a[i] !== b[i]) { return false; } } return true; }; exports.getFunctionName = function(fn) { if (fn.name) { return fn.name; } return (fn.toString().trim().match(/^function\s*([^\s(]+)/) || [])[1]; }; /*! * Decorate buffers */ exports.decorate = function(destination, source) { for (const key in source) { if (specialProperties.has(key)) { continue; } destination[key] = source[key]; } }; /** * merges to with a copy of from * * @param {Object} to * @param {Object} fromObj * @api private */ exports.mergeClone = function(to, fromObj) { if (isMongooseObject(fromObj)) { fromObj = fromObj.toObject({ transform: false, virtuals: false, depopulate: true, getters: false, flattenDecimals: false }); } const keys = Object.keys(fromObj); const len = keys.length; let i = 0; let key; while (i < len) { key = keys[i++]; if (specialProperties.has(key)) { continue; } if (typeof to[key] === 'undefined') { to[key] = exports.clone(fromObj[key], { transform: false, virtuals: false, depopulate: true, getters: false, flattenDecimals: false }); } else { let val = fromObj[key]; if (val != null && val.valueOf && !(val instanceof Date)) { val = val.valueOf(); } if (exports.isObject(val)) { let obj = val; if (isMongooseObject(val) && !val.isMongooseBuffer) { obj = obj.toObject({ transform: false, virtuals: false, depopulate: true, getters: false, flattenDecimals: false }); } if (val.isMongooseBuffer) { obj = Buffer.from(obj); } exports.mergeClone(to[key], obj); } else { to[key] = exports.clone(val, { flattenDecimals: false }); } } } }; /** * Executes a function on each element of an array (like _.each) * * @param {Array} arr * @param {Function} fn * @api private */ exports.each = function(arr, fn) { for (let i = 0; i < arr.length; ++i) { fn(arr[i]); } }; /*! * ignore */ exports.noop = function() {};