Skip to main content
Module

x/mongoose/lib/utils.js

MongoDB object modeling designed to work in an asynchronous environment.
Go to Latest
File
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010
'use strict';
/*! * Module dependencies. */
const ms = require('ms');const mpath = require('mpath');const ObjectId = require('./types/objectid');const PopulateOptions = require('./options/PopulateOptions');const clone = require('./helpers/clone');const immediate = require('./helpers/immediate');const isObject = require('./helpers/isObject');const isMongooseArray = require('./types/array/isMongooseArray');const isMongooseDocumentArray = require('./types/DocumentArray/isMongooseDocumentArray');const isBsonType = require('./helpers/isBsonType');const getFunctionName = require('./helpers/getFunctionName');const isMongooseObject = require('./helpers/isMongooseObject');const promiseOrCallback = require('./helpers/promiseOrCallback');const schemaMerge = require('./helpers/schema/merge');const specialProperties = require('./helpers/specialProperties');const { trustedSymbol } = require('./helpers/query/trusted');
let Document;
exports.specialProperties = specialProperties;
exports.isMongooseArray = isMongooseArray.isMongooseArray;exports.isMongooseDocumentArray = isMongooseDocumentArray.isMongooseDocumentArray;exports.registerMongooseArray = isMongooseArray.registerMongooseArray;exports.registerMongooseDocumentArray = isMongooseDocumentArray.registerMongooseDocumentArray;
/** * 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 (typeof a !== 'object' || typeof b !== 'object') { return a === b; }
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 && a.dotAll === b.dotAll && a.unicode === b.unicode && a.sticky === b.sticky && a.hasIndices === b.hasIndices; }
if (a == null || b == null) { return false; }
if (a.prototype !== b.prototype) { return false; }
if (a instanceof Map || b instanceof Map) { if (!(a instanceof Map) || !(b instanceof Map)) { return false; } return deepEqual(Array.from(a.keys()), Array.from(b.keys())) && deepEqual(Array.from(a.values()), Array.from(b.values())); }
// 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 (Array.isArray(a) || Array.isArray(b)) { if (!Array.isArray(a) || !Array.isArray(b)) { return false; } const len = a.length; if (len !== b.length) { return false; } for (let i = 0; i < len; ++i) { if (!deepEqual(a[i], b[i])) { return false; } } return true; }
if (a.$__ != null) { a = a._doc; } else if (isMongooseObject(a)) { a = a.toObject(); }
if (b.$__ != null) { b = b._doc; } else if (isMongooseObject(b)) { b = b.toObject(); }
const ka = Object.keys(a); const kb = Object.keys(b); const kaLength = ka.length;
// having the same number of owned properties (keys incorporates // hasOwnProperty) if (kaLength !== kb.length) { return false; }
// ~~~cheap key test for (let i = kaLength - 1; i >= 0; i--) { if (ka[i] !== kb[i]) { return false; } }
// equivalent values for every corresponding key, and // ~~~possibly expensive deep test for (const key of ka) { if (!deepEqual(a[key], b[key])) { return false; } }
return true;};
/** * Get the last element of an array * @param {Array} arr */
exports.last = function(arr) { if (arr.length > 0) { return arr[arr.length - 1]; } return void 0;};
exports.clone = clone;
/*! * ignore */
exports.promiseOrCallback = promiseOrCallback;
/*! * ignore */
exports.cloneArrays = function cloneArrays(arr) { if (!Array.isArray(arr)) { return arr; }
return arr.map(el => exports.cloneArrays(el));};
/*! * ignore */
exports.omit = function omit(obj, keys) { if (keys == null) { return Object.assign({}, obj); } if (!Array.isArray(keys)) { keys = [keys]; }
const ret = Object.assign({}, obj); for (const key of keys) { delete ret[key]; } 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;};
/** * Merges `from` into `to` without overwriting existing properties. * * @param {Object} to * @param {Object} from * @param {Object} [options] * @param {String} [path] * @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;
if (from[trustedSymbol]) { to[trustedSymbol] = from[trustedSymbol]; }
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) { // Skip merging schemas if we're creating a discriminator schema and // base schema has a given path as a single nested but discriminator schema // has the path as a document array, or vice versa (gh-9534) if (options.isDiscriminatorSchemaMerge && (from[key].$isSingleNested && to[key].$isMongooseDocumentArray) || (from[key].$isMongooseDocumentArray && to[key].$isSingleNested)) { continue; } else if (from[key].instanceOfSchema) { if (to[key].instanceOfSchema) { schemaMerge(to[key], from[key].clone(), options.isDiscriminatorSchemaMerge); } else { to[key] = from[key].clone(); } continue; } else if (isBsonType(from[key], '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 (const doc of obj) { ret.push(toObject(doc)); }
return ret; }
if (exports.isPOJO(obj)) { ret = {};
if (obj[trustedSymbol]) { ret[trustedSymbol] = obj[trustedSymbol]; }
for (const k of Object.keys(obj)) { if (specialProperties.has(k)) { continue; } ret[k] = toObject(obj[k]); }
return ret; }
return obj;};
exports.isObject = isObject;
/** * Determines if `arg` is a plain old JavaScript object (POJO). Specifically, * `arg` must be an object but not an instance of any special class, like String, * ObjectId, etc. * * `Object.getPrototypeOf()` is part of ES5: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf * * @param {Object|Array|String|Function|RegExp|any} arg * @api private * @return {Boolean} */
exports.isPOJO = function isPOJO(arg) { if (arg == null || typeof arg !== 'object') { return false; } const proto = Object.getPrototypeOf(arg); // Prototype may be null if you used `Object.create(null)` // Checking `proto`'s constructor is safe because `getPrototypeOf()` // explicitly crosses the boundary from object data to object metadata return !proto || proto.constructor.name === 'Object';};
/** * Determines if `arg` is an object that isn't an instance of a built-in value * class, like Array, Buffer, ObjectId, etc. * @param {Any} val */
exports.isNonBuiltinObject = function isNonBuiltinObject(val) { return typeof val === 'object' && !exports.isNativeObject(val) && !exports.isMongooseType(val) && val != null;};
/** * Determines if `obj` is a built-in object like an array, date, boolean, * etc. * @param {Any} arg */
exports.isNativeObject = function(arg) { return Array.isArray(arg) || arg instanceof Date || arg instanceof Boolean || arg instanceof Number || arg instanceof String;};
/** * Determines if `val` is an object that has no own keys * @param {Any} val */
exports.isEmptyObject = function(val) { return val != null && typeof val === 'object' && Object.keys(val).length === 0;};
/** * Search if `obj` or any POJOs nested underneath `obj` has a property named * `key` * @param {Object} obj * @param {String} key */
exports.hasKey = function hasKey(obj, key) { const props = Object.keys(obj); for (const prop of props) { if (prop === key) { return true; } if (exports.isPOJO(obj[prop]) && exports.hasKey(obj[prop], key)) { return true; } } return false;};
/** * 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. immediate(function() { throw err; }); } };};
/** * Returns true if `v` is an object that can be serialized as a primitive in * MongoDB * @param {Any} v */
exports.isMongooseType = function(v) { return isBsonType(v, 'ObjectID') || isBsonType(v, 'Decimal128') || v instanceof Buffer;};
exports.isMongooseObject = 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; }
object.expireAfterSeconds = (typeof object.expires !== 'string') ? object.expires : Math.round(ms(object.expires) / 1000); delete object.expires;};
/** * populate helper * @param {String} path * @param {String} select * @param {Model} model * @param {Object} match * @param {Object} options * @param {Any} subPopulate * @param {Boolean} justOne * @param {Boolean} count */
exports.populate = function populate(path, select, model, match, options, subPopulate, justOne, count) { // might have passed an object specifying all arguments let obj = null; if (arguments.length === 1) { if (path instanceof PopulateOptions) { // If reusing old populate docs, avoid reusing `_docs` because that may // lead to bugs and memory leaks. See gh-11641 path._docs = []; path._childDocs = []; return [path]; }
if (Array.isArray(path)) { const singles = makeSingles(path); return singles.map(o => exports.populate(o)[0]); }
if (exports.isObject(path)) { obj = Object.assign({}, path); } else { obj = { path: path }; } } else if (typeof model === 'object') { obj = { path: path, select: select, match: model, options: match }; } else { obj = { path: path, select: select, model: model, match: match, options: options, populate: subPopulate, justOne: justOne, count: count }; }
if (typeof obj.path !== 'string') { throw new TypeError('utils.populate: invalid path. Expected string. Got typeof `' + typeof path + '`'); }
return _populateObj(obj);
// 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; }};
function _populateObj(obj) { if (Array.isArray(obj.populate)) { const ret = []; obj.populate.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]); } }); obj.populate = exports.populate(ret); } else if (obj.populate != null && typeof obj.populate === 'object') { obj.populate = exports.populate(obj.populate); }
const ret = []; const paths = obj.path.split(' '); if (obj.options != null) { obj.options = exports.clone(obj.options); }
for (const path of paths) { ret.push(new PopulateOptions(Object.assign({}, obj, { path: path }))); }
return ret;}
/** * Return the value of `obj` at the given `path`. * * @param {String} path * @param {Object} obj * @param {Any} map */
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 * @param {Any} map * @param {Any} _copying */
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} * @api 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;
const hop = Object.prototype.hasOwnProperty;
/** * Safer helper for hasOwnProperty checks * * @param {Object} obj * @param {String} prop */
exports.object.hasOwnProperty = function(obj, prop) { return hop.call(obj, prop);};
/** * Determine if `val` is null or undefined * * @param {Any} val * @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 falsy value, the item will not be included in the results. * @param {Array} ret * @return {Array} * @api 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;};
/*! * ignore */
const _hasOwnProperty = Object.prototype.hasOwnProperty;
exports.hasUserDefinedProperty = function(obj, key) { if (obj == null) { return false; }
if (Array.isArray(key)) { for (const k of key) { if (exports.hasUserDefinedProperty(obj, k)) { return true; } } return false; }
if (_hasOwnProperty.call(obj, key)) { return true; } if (typeof obj === 'object' && key in obj) { const v = obj[key]; return v !== Object.prototype[key] && v !== Array.prototype[key]; }
return false;};
/*! * ignore */
const MAX_ARRAY_INDEX = Math.pow(2, 32) - 1;
exports.isArrayIndex = function(val) { if (typeof val === 'number') { return val >= 0 && val <= MAX_ARRAY_INDEX; } if (typeof val === 'string') { if (!/^\d+$/.test(val)) { return false; } val = +val; return val >= 0 && val <= MAX_ARRAY_INDEX; }
return false;};
/** * 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} * @api private */
exports.array.unique = function(arr) { const primitives = new Set(); const ids = new Set(); const ret = [];
for (const item of arr) { if (typeof item === 'number' || typeof item === 'string' || item == null) { if (primitives.has(item)) { continue; } ret.push(item); primitives.add(item); } else if (isBsonType(item, 'ObjectID')) { if (ids.has(item.toString())) { continue; } ret.push(item); ids.add(item.toString()); } else { ret.push(item); } }
return ret;};
exports.buffer = {};
/** * Determines if two buffers are equal. * * @param {Buffer} a * @param {Object} b */
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 = getFunctionName;
/** * Decorate buffers * @param {Object} destination * @param {Object} source */
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 (const item of arr) { fn(item); }};
/*! * ignore */
exports.getOption = function(name) { const sources = Array.prototype.slice.call(arguments, 1);
for (const source of sources) { if (source == null) { continue; } if (source[name] != null) { return source[name]; } }
return null;};
/*! * ignore */
exports.noop = function() {};
exports.errorToPOJO = function errorToPOJO(error) { const isError = error instanceof Error; if (!isError) { throw new Error('`error` must be `instanceof Error`.'); }
const ret = {}; for (const properyName of Object.getOwnPropertyNames(error)) { ret[properyName] = error[properyName]; } return ret;};
/*! * ignore */
exports.warn = function warn(message) { return process.emitWarning(message, { code: 'MONGOOSE' });};

exports.injectTimestampsOption = function injectTimestampsOption(writeOperation, timestampsOption) { if (timestampsOption == null) { return; } writeOperation.timestamps = timestampsOption;};