'use strict'; const Decimal = require('../types/decimal128'); const ObjectId = require('../types/objectid'); const specialProperties = require('./specialProperties'); const isMongooseObject = require('./isMongooseObject'); const getFunctionName = require('./getFunctionName'); const isBsonType = require('./isBsonType'); const isObject = require('./isObject'); const symbols = require('./symbols'); const trustedSymbol = require('./query/trusted').trustedSymbol; const utils = require('../utils'); /*! * 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 */ function clone(obj, options, isArrayChild) { if (obj == null) { return obj; } if (Array.isArray(obj)) { return cloneArray(utils.isMongooseArray(obj) ? obj.__array : obj, options); } if (isMongooseObject(obj)) { // Single nested subdocs should apply getters later in `applyGetters()` // when calling `toObject()`. See gh-7442, gh-8295 if (options && options._skipSingleNestedGetters && obj.$isSingleNested) { options = Object.assign({}, options, { getters: false }); } const isSingleNested = obj.$isSingleNested; if (utils.isPOJO(obj) && obj.$__ != null && obj._doc != null) { return obj._doc; } let ret; if (options && options.json && typeof obj.toJSON === 'function') { ret = obj.toJSON(options); } else { ret = obj.toObject(options); } if (options && options.minimize && isSingleNested && Object.keys(ret).length === 0) { return undefined; } return ret; } const objConstructor = obj.constructor; if (objConstructor) { switch (getFunctionName(objConstructor)) { case 'Object': return cloneObject(obj, options, isArrayChild); case 'Date': return new objConstructor(+obj); case 'RegExp': return cloneRegExp(obj); default: // ignore break; } } if (isBsonType(obj, 'ObjectID')) { return new ObjectId(obj.id); } if (isBsonType(obj, 'Decimal128')) { if (options && options.flattenDecimals) { return obj.toJSON(); } return Decimal.fromString(obj.toString()); } // object created with Object.create(null) if (!objConstructor && isObject(obj)) { return cloneObject(obj, options, isArrayChild); } if (typeof obj === 'object' && obj[symbols.schemaTypeSymbol]) { return obj.clone(); } // If we're cloning this object to go into a MongoDB command, // and there's a `toBSON()` function, assume this object will be // stored as a primitive in MongoDB and doesn't need to be cloned. if (options && options.bson && typeof obj.toBSON === 'function') { return obj; } if (typeof obj.valueOf === 'function') { return obj.valueOf(); } return cloneObject(obj, options, isArrayChild); } module.exports = clone; /*! * ignore */ function cloneObject(obj, options, isArrayChild) { const minimize = options && options.minimize; const omitUndefined = options && options.omitUndefined; const seen = options && options._seen; const ret = {}; let hasKeys; if (seen && seen.has(obj)) { return seen.get(obj); } else if (seen) { seen.set(obj, ret); } if (trustedSymbol in obj) { ret[trustedSymbol] = obj[trustedSymbol]; } let i = 0; let key = ''; const keys = Object.keys(obj); const len = keys.length; for (i = 0; i < len; ++i) { if (specialProperties.has(key = keys[i])) { continue; } // Don't pass `isArrayChild` down const val = clone(obj[key], options, false); if ((minimize === false || omitUndefined) && typeof val === 'undefined') { delete ret[key]; } else if (minimize !== true || (typeof val !== 'undefined')) { hasKeys || (hasKeys = true); ret[key] = val; } } return minimize && !isArrayChild ? hasKeys && ret : ret; } function cloneArray(arr, options) { let i = 0; const len = arr.length; const ret = new Array(len); for (i = 0; i < len; ++i) { ret[i] = clone(arr[i], options, true); } return ret; } function cloneRegExp(regexp) { const ret = new RegExp(regexp.source, regexp.flags); if (ret.lastIndex !== regexp.lastIndex) { ret.lastIndex = regexp.lastIndex; } return ret; }