You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					541 lines
				
				19 KiB
			
		
		
			
		
	
	
					541 lines
				
				19 KiB
			| 
											3 years ago
										 | 'use strict'; | ||
|  | 
 | ||
|  | const MongooseError = require('../../error/index'); | ||
|  | const SkipPopulateValue = require('./SkipPopulateValue'); | ||
|  | const get = require('../get'); | ||
|  | const getDiscriminatorByValue = require('../discriminator/getDiscriminatorByValue'); | ||
|  | const isPathExcluded = require('../projection/isPathExcluded'); | ||
|  | const getConstructorName = require('../getConstructorName'); | ||
|  | const getSchemaTypes = require('./getSchemaTypes'); | ||
|  | const getVirtual = require('./getVirtual'); | ||
|  | const lookupLocalFields = require('./lookupLocalFields'); | ||
|  | const mpath = require('mpath'); | ||
|  | const normalizeRefPath = require('./normalizeRefPath'); | ||
|  | const util = require('util'); | ||
|  | const utils = require('../../utils'); | ||
|  | 
 | ||
|  | const modelSymbol = require('../symbols').modelSymbol; | ||
|  | const populateModelSymbol = require('../symbols').populateModelSymbol; | ||
|  | const schemaMixedSymbol = require('../../schema/symbols').schemaMixedSymbol; | ||
|  | 
 | ||
|  | module.exports = function getModelsMapForPopulate(model, docs, options) { | ||
|  |   let i; | ||
|  |   let doc; | ||
|  |   const len = docs.length; | ||
|  |   const available = {}; | ||
|  |   const map = []; | ||
|  |   const modelNameFromQuery = options.model && options.model.modelName || options.model; | ||
|  |   let schema; | ||
|  |   let refPath; | ||
|  |   let Model; | ||
|  |   let currentOptions; | ||
|  |   let modelNames; | ||
|  |   let modelName; | ||
|  | 
 | ||
|  |   const originalModel = options.model; | ||
|  |   let isVirtual = false; | ||
|  |   const modelSchema = model.schema; | ||
|  | 
 | ||
|  |   let allSchemaTypes = getSchemaTypes(modelSchema, null, options.path); | ||
|  |   allSchemaTypes = Array.isArray(allSchemaTypes) ? allSchemaTypes : [allSchemaTypes].filter(v => v != null); | ||
|  |   const _firstWithRefPath = allSchemaTypes.find(schematype => get(schematype, 'options.refPath', null) != null); | ||
|  | 
 | ||
|  |   for (i = 0; i < len; i++) { | ||
|  |     doc = docs[i]; | ||
|  |     let justOne = null; | ||
|  |     schema = getSchemaTypes(modelSchema, doc, options.path); | ||
|  |     // Special case: populating a path that's a DocumentArray unless
 | ||
|  |     // there's an explicit `ref` or `refPath` re: gh-8946
 | ||
|  |     if (schema != null && | ||
|  |         schema.$isMongooseDocumentArray && | ||
|  |         schema.options.ref == null && | ||
|  |         schema.options.refPath == null) { | ||
|  |       continue; | ||
|  |     } | ||
|  |     // Populating a nested path should always be a no-op re: #9073.
 | ||
|  |     // People shouldn't do this, but apparently they do.
 | ||
|  |     if (options._localModel != null && options._localModel.schema.nested[options.path]) { | ||
|  |       continue; | ||
|  |     } | ||
|  |     const isUnderneathDocArray = schema && schema.$isUnderneathDocArray; | ||
|  |     if (isUnderneathDocArray && get(options, 'options.sort') != null) { | ||
|  |       return new MongooseError('Cannot populate with `sort` on path ' + options.path + | ||
|  |         ' because it is a subproperty of a document array'); | ||
|  |     } | ||
|  | 
 | ||
|  |     modelNames = null; | ||
|  |     let isRefPath = !!_firstWithRefPath; | ||
|  |     let normalizedRefPath = _firstWithRefPath ? get(_firstWithRefPath, 'options.refPath', null) : null; | ||
|  |     let schemaOptions = null; | ||
|  | 
 | ||
|  |     if (Array.isArray(schema)) { | ||
|  |       const schemasArray = schema; | ||
|  |       for (const _schema of schemasArray) { | ||
|  |         let _modelNames; | ||
|  |         let res; | ||
|  |         try { | ||
|  |           res = _getModelNames(doc, _schema); | ||
|  |           _modelNames = res.modelNames; | ||
|  |           isRefPath = isRefPath || res.isRefPath; | ||
|  |           normalizedRefPath = normalizeRefPath(normalizedRefPath, doc, options.path) || | ||
|  |             res.refPath; | ||
|  |           justOne = res.justOne; | ||
|  |         } catch (error) { | ||
|  |           return error; | ||
|  |         } | ||
|  | 
 | ||
|  |         if (isRefPath && !res.isRefPath) { | ||
|  |           continue; | ||
|  |         } | ||
|  |         if (!_modelNames) { | ||
|  |           continue; | ||
|  |         } | ||
|  |         modelNames = modelNames || []; | ||
|  |         for (const modelName of _modelNames) { | ||
|  |           if (modelNames.indexOf(modelName) === -1) { | ||
|  |             modelNames.push(modelName); | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } else { | ||
|  |       try { | ||
|  |         const res = _getModelNames(doc, schema); | ||
|  |         modelNames = res.modelNames; | ||
|  |         isRefPath = res.isRefPath; | ||
|  |         normalizedRefPath = res.refPath; | ||
|  |         justOne = res.justOne; | ||
|  |         schemaOptions = get(schema, 'options.populate', null); | ||
|  |       } catch (error) { | ||
|  |         return error; | ||
|  |       } | ||
|  | 
 | ||
|  |       if (!modelNames) { | ||
|  |         continue; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     const _virtualRes = getVirtual(model.schema, options.path); | ||
|  |     const virtual = _virtualRes == null ? null : _virtualRes.virtual; | ||
|  | 
 | ||
|  |     let localField; | ||
|  |     let count = false; | ||
|  |     if (virtual && virtual.options) { | ||
|  |       const virtualPrefix = _virtualRes.nestedSchemaPath ? | ||
|  |         _virtualRes.nestedSchemaPath + '.' : ''; | ||
|  |       if (typeof virtual.options.localField === 'function') { | ||
|  |         localField = virtualPrefix + virtual.options.localField.call(doc, doc); | ||
|  |       } else if (Array.isArray(virtual.options.localField)) { | ||
|  |         localField = virtual.options.localField.map(field => virtualPrefix + field); | ||
|  |       } else { | ||
|  |         localField = virtualPrefix + virtual.options.localField; | ||
|  |       } | ||
|  |       count = virtual.options.count; | ||
|  | 
 | ||
|  |       if (virtual.options.skip != null && !options.hasOwnProperty('skip')) { | ||
|  |         options.skip = virtual.options.skip; | ||
|  |       } | ||
|  |       if (virtual.options.limit != null && !options.hasOwnProperty('limit')) { | ||
|  |         options.limit = virtual.options.limit; | ||
|  |       } | ||
|  |       if (virtual.options.perDocumentLimit != null && !options.hasOwnProperty('perDocumentLimit')) { | ||
|  |         options.perDocumentLimit = virtual.options.perDocumentLimit; | ||
|  |       } | ||
|  |     } else { | ||
|  |       localField = options.path; | ||
|  |     } | ||
|  |     let foreignField = virtual && virtual.options ? | ||
|  |       virtual.options.foreignField : | ||
|  |       '_id'; | ||
|  | 
 | ||
|  |     // `justOne = null` means we don't know from the schema whether the end
 | ||
|  |     // result should be an array or a single doc. This can result from
 | ||
|  |     // populating a POJO using `Model.populate()`
 | ||
|  |     if ('justOne' in options && options.justOne !== void 0) { | ||
|  |       justOne = options.justOne; | ||
|  |     } else if (virtual && virtual.options && virtual.options.refPath) { | ||
|  |       const normalizedRefPath = | ||
|  |         normalizeRefPath(virtual.options.refPath, doc, options.path); | ||
|  |       justOne = !!virtual.options.justOne; | ||
|  |       isVirtual = true; | ||
|  |       const refValue = utils.getValue(normalizedRefPath, doc); | ||
|  |       modelNames = Array.isArray(refValue) ? refValue : [refValue]; | ||
|  |     } else if (virtual && virtual.options && virtual.options.ref) { | ||
|  |       let normalizedRef; | ||
|  |       if (typeof virtual.options.ref === 'function') { | ||
|  |         normalizedRef = virtual.options.ref.call(doc, doc); | ||
|  |       } else { | ||
|  |         normalizedRef = virtual.options.ref; | ||
|  |       } | ||
|  |       justOne = !!virtual.options.justOne; | ||
|  |       isVirtual = true; | ||
|  |       if (!modelNames) { | ||
|  |         modelNames = [].concat(normalizedRef); | ||
|  |       } | ||
|  |     } else if (schema && !schema[schemaMixedSymbol]) { | ||
|  |       // Skip Mixed types because we explicitly don't do casting on those.
 | ||
|  |       if (options.path.endsWith('.' + schema.path)) { | ||
|  |         justOne = Array.isArray(schema) ? | ||
|  |           schema.every(schema => !schema.$isMongooseArray) : | ||
|  |           !schema.$isMongooseArray; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!modelNames) { | ||
|  |       continue; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (virtual && (!localField || !foreignField)) { | ||
|  |       return new MongooseError('If you are populating a virtual, you must set the ' + | ||
|  |         'localField and foreignField options'); | ||
|  |     } | ||
|  | 
 | ||
|  |     options.isVirtual = isVirtual; | ||
|  |     options.virtual = virtual; | ||
|  |     if (typeof localField === 'function') { | ||
|  |       localField = localField.call(doc, doc); | ||
|  |     } | ||
|  |     if (typeof foreignField === 'function') { | ||
|  |       foreignField = foreignField.call(doc); | ||
|  |     } | ||
|  | 
 | ||
|  |     let match = get(options, 'match', null) || | ||
|  |       get(currentOptions, 'match', null) || | ||
|  |       get(options, 'virtual.options.match', null) || | ||
|  |       get(options, 'virtual.options.options.match', null); | ||
|  | 
 | ||
|  |     let hasMatchFunction = typeof match === 'function'; | ||
|  |     if (hasMatchFunction) { | ||
|  |       match = match.call(doc, doc); | ||
|  |     } | ||
|  | 
 | ||
|  |     if (Array.isArray(localField) && Array.isArray(foreignField) && localField.length === foreignField.length) { | ||
|  |       match = Object.assign({}, match); | ||
|  |       for (let i = 1; i < localField.length; ++i) { | ||
|  |         match[foreignField[i]] = convertTo_id(mpath.get(localField[i], doc, lookupLocalFields), schema); | ||
|  |         hasMatchFunction = true; | ||
|  |       } | ||
|  | 
 | ||
|  |       localField = localField[0]; | ||
|  |       foreignField = foreignField[0]; | ||
|  |     } | ||
|  | 
 | ||
|  |     const localFieldPathType = modelSchema._getPathType(localField); | ||
|  |     const localFieldPath = localFieldPathType === 'real' ? modelSchema.path(localField) : localFieldPathType.schema; | ||
|  |     const localFieldGetters = localFieldPath && localFieldPath.getters ? localFieldPath.getters : []; | ||
|  |     let ret; | ||
|  | 
 | ||
|  |     const _populateOptions = get(options, 'options', {}); | ||
|  | 
 | ||
|  |     const getters = 'getters' in _populateOptions ? | ||
|  |       _populateOptions.getters : | ||
|  |       options.isVirtual && get(virtual, 'options.getters', false); | ||
|  |     if (localFieldGetters.length > 0 && getters) { | ||
|  |       const hydratedDoc = (doc.$__ != null) ? doc : model.hydrate(doc); | ||
|  |       const localFieldValue = mpath.get(localField, doc, lookupLocalFields); | ||
|  |       if (Array.isArray(localFieldValue)) { | ||
|  |         const localFieldHydratedValue = mpath.get(localField.split('.').slice(0, -1), hydratedDoc, lookupLocalFields); | ||
|  |         ret = localFieldValue.map((localFieldArrVal, localFieldArrIndex) => | ||
|  |           localFieldPath.applyGetters(localFieldArrVal, localFieldHydratedValue[localFieldArrIndex])); | ||
|  |       } else { | ||
|  |         ret = localFieldPath.applyGetters(localFieldValue, hydratedDoc); | ||
|  |       } | ||
|  |     } else { | ||
|  |       ret = convertTo_id(mpath.get(localField, doc, lookupLocalFields), schema); | ||
|  |     } | ||
|  | 
 | ||
|  |     const id = String(utils.getValue(foreignField, doc)); | ||
|  |     options._docs[id] = Array.isArray(ret) ? ret.slice() : ret; | ||
|  | 
 | ||
|  |     // Re: gh-8452. Embedded discriminators may not have `refPath`, so clear
 | ||
|  |     // out embedded discriminator docs that don't have a `refPath` on the
 | ||
|  |     // populated path.
 | ||
|  |     if (isRefPath && normalizedRefPath != null) { | ||
|  |       const pieces = normalizedRefPath.split('.'); | ||
|  |       let cur = ''; | ||
|  |       for (let j = 0; j < pieces.length; ++j) { | ||
|  |         const piece = pieces[j]; | ||
|  |         cur = cur + (cur.length === 0 ? '' : '.') + piece; | ||
|  |         const schematype = modelSchema.path(cur); | ||
|  |         if (schematype != null && | ||
|  |             schematype.$isMongooseArray && | ||
|  |             schematype.caster.discriminators != null && | ||
|  |             Object.keys(schematype.caster.discriminators).length > 0) { | ||
|  |           const subdocs = utils.getValue(cur, doc); | ||
|  |           const remnant = options.path.substr(cur.length + 1); | ||
|  |           const discriminatorKey = schematype.caster.schema.options.discriminatorKey; | ||
|  |           modelNames = []; | ||
|  |           for (const subdoc of subdocs) { | ||
|  |             const discriminatorName = utils.getValue(discriminatorKey, subdoc); | ||
|  |             const discriminator = schematype.caster.discriminators[discriminatorName]; | ||
|  |             const discriminatorSchema = discriminator && discriminator.schema; | ||
|  |             if (discriminatorSchema == null) { | ||
|  |               continue; | ||
|  |             } | ||
|  |             const _path = discriminatorSchema.path(remnant); | ||
|  |             if (_path == null || _path.options.refPath == null) { | ||
|  |               const docValue = utils.getValue(localField.substr(cur.length + 1), subdoc); | ||
|  |               ret = ret.map(v => v === docValue ? SkipPopulateValue(v) : v); | ||
|  |               continue; | ||
|  |             } | ||
|  |             const modelName = utils.getValue(pieces.slice(j + 1).join('.'), subdoc); | ||
|  |             modelNames.push(modelName); | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     let k = modelNames.length; | ||
|  |     while (k--) { | ||
|  |       modelName = modelNames[k]; | ||
|  |       if (modelName == null) { | ||
|  |         continue; | ||
|  |       } | ||
|  | 
 | ||
|  |       // `PopulateOptions#connection`: if the model is passed as a string, the
 | ||
|  |       // connection matters because different connections have different models.
 | ||
|  |       const connection = options.connection != null ? options.connection : model.db; | ||
|  | 
 | ||
|  |       try { | ||
|  |         Model = originalModel && originalModel[modelSymbol] ? | ||
|  |           originalModel : | ||
|  |           modelName[modelSymbol] ? modelName : connection.model(modelName); | ||
|  |       } catch (error) { | ||
|  |         // If `ret` is undefined, we'll add an empty entry to modelsMap. We shouldn't
 | ||
|  |         // execute a query, but it is necessary to make sure `justOne` gets handled
 | ||
|  |         // correctly for setting an empty array (see gh-8455)
 | ||
|  |         if (ret !== undefined) { | ||
|  |           return error; | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       let ids = ret; | ||
|  |       const flat = Array.isArray(ret) ? utils.array.flatten(ret) : []; | ||
|  | 
 | ||
|  |       if (isRefPath && Array.isArray(ret) && flat.length === modelNames.length) { | ||
|  |         ids = flat.filter((val, i) => modelNames[i] === modelName); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (!available[modelName] || currentOptions.perDocumentLimit != null || get(currentOptions, 'options.perDocumentLimit') != null) { | ||
|  |         currentOptions = { | ||
|  |           model: Model | ||
|  |         }; | ||
|  | 
 | ||
|  |         if (isVirtual && get(virtual, 'options.options')) { | ||
|  |           currentOptions.options = utils.clone(virtual.options.options); | ||
|  |         } else if (schemaOptions != null) { | ||
|  |           currentOptions.options = Object.assign({}, schemaOptions); | ||
|  |         } | ||
|  |         utils.merge(currentOptions, options); | ||
|  | 
 | ||
|  |         // Used internally for checking what model was used to populate this
 | ||
|  |         // path.
 | ||
|  |         options[populateModelSymbol] = Model; | ||
|  | 
 | ||
|  |         available[modelName] = { | ||
|  |           model: Model, | ||
|  |           options: currentOptions, | ||
|  |           match: hasMatchFunction ? [match] : match, | ||
|  |           docs: [doc], | ||
|  |           ids: [ids], | ||
|  |           allIds: [ret], | ||
|  |           localField: new Set([localField]), | ||
|  |           foreignField: new Set([foreignField]), | ||
|  |           justOne: justOne, | ||
|  |           isVirtual: isVirtual, | ||
|  |           virtual: virtual, | ||
|  |           count: count, | ||
|  |           [populateModelSymbol]: Model | ||
|  |         }; | ||
|  |         map.push(available[modelName]); | ||
|  |       } else { | ||
|  |         available[modelName].localField.add(localField); | ||
|  |         available[modelName].foreignField.add(foreignField); | ||
|  |         available[modelName].docs.push(doc); | ||
|  |         available[modelName].ids.push(ids); | ||
|  |         available[modelName].allIds.push(ret); | ||
|  |         if (hasMatchFunction) { | ||
|  |           available[modelName].match.push(match); | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  |   return map; | ||
|  | 
 | ||
|  |   function _getModelNames(doc, schema) { | ||
|  |     let modelNames; | ||
|  |     let discriminatorKey; | ||
|  |     let isRefPath = false; | ||
|  |     let justOne = null; | ||
|  | 
 | ||
|  |     if (schema && schema.caster) { | ||
|  |       schema = schema.caster; | ||
|  |     } | ||
|  |     if (schema && schema.$isSchemaMap) { | ||
|  |       schema = schema.$__schemaType; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!schema && model.discriminators) { | ||
|  |       discriminatorKey = model.schema.discriminatorMapping.key; | ||
|  |     } | ||
|  | 
 | ||
|  |     refPath = schema && schema.options && schema.options.refPath; | ||
|  | 
 | ||
|  |     const normalizedRefPath = normalizeRefPath(refPath, doc, options.path); | ||
|  | 
 | ||
|  |     if (modelNameFromQuery) { | ||
|  |       modelNames = [modelNameFromQuery]; // query options
 | ||
|  |     } else if (normalizedRefPath) { | ||
|  |       if (options._queryProjection != null && isPathExcluded(options._queryProjection, normalizedRefPath)) { | ||
|  |         throw new MongooseError('refPath `' + normalizedRefPath + | ||
|  |           '` must not be excluded in projection, got ' + | ||
|  |           util.inspect(options._queryProjection)); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (modelSchema.virtuals.hasOwnProperty(normalizedRefPath) && doc.$__ == null) { | ||
|  |         modelNames = [modelSchema.virtuals[normalizedRefPath].applyGetters(void 0, doc)]; | ||
|  |       } else { | ||
|  |         modelNames = utils.getValue(normalizedRefPath, doc); | ||
|  |       } | ||
|  | 
 | ||
|  |       if (Array.isArray(modelNames)) { | ||
|  |         modelNames = utils.array.flatten(modelNames); | ||
|  |       } | ||
|  | 
 | ||
|  |       isRefPath = true; | ||
|  |     } else { | ||
|  |       let modelForCurrentDoc = model; | ||
|  |       let schemaForCurrentDoc; | ||
|  |       let discriminatorValue; | ||
|  | 
 | ||
|  |       if (!schema && discriminatorKey && (discriminatorValue = utils.getValue(discriminatorKey, doc))) { | ||
|  |         // `modelNameForFind` is the discriminator value, so we might need
 | ||
|  |         // find the discriminated model name
 | ||
|  |         const discriminatorModel = getDiscriminatorByValue(model.discriminators, discriminatorValue) || model; | ||
|  |         if (discriminatorModel != null) { | ||
|  |           modelForCurrentDoc = discriminatorModel; | ||
|  |         } else { | ||
|  |           try { | ||
|  |             modelForCurrentDoc = model.db.model(discriminatorValue); | ||
|  |           } catch (error) { | ||
|  |             return error; | ||
|  |           } | ||
|  |         } | ||
|  | 
 | ||
|  |         schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path); | ||
|  | 
 | ||
|  |         if (schemaForCurrentDoc && schemaForCurrentDoc.caster) { | ||
|  |           schemaForCurrentDoc = schemaForCurrentDoc.caster; | ||
|  |         } | ||
|  |       } else { | ||
|  |         schemaForCurrentDoc = schema; | ||
|  |       } | ||
|  |       const _virtualRes = getVirtual(modelForCurrentDoc.schema, options.path); | ||
|  |       const virtual = _virtualRes == null ? null : _virtualRes.virtual; | ||
|  | 
 | ||
|  |       if (schemaForCurrentDoc != null) { | ||
|  |         justOne = !schemaForCurrentDoc.$isMongooseArray && !schemaForCurrentDoc._arrayPath; | ||
|  |       } | ||
|  | 
 | ||
|  |       let ref; | ||
|  |       let refPath; | ||
|  | 
 | ||
|  |       if ((ref = get(schemaForCurrentDoc, 'options.ref')) != null) { | ||
|  |         ref = handleRefFunction(ref, doc); | ||
|  |         modelNames = [ref]; | ||
|  |       } else if ((ref = get(virtual, 'options.ref')) != null) { | ||
|  |         ref = handleRefFunction(ref, doc); | ||
|  | 
 | ||
|  |         // When referencing nested arrays, the ref should be an Array
 | ||
|  |         // of modelNames.
 | ||
|  |         if (Array.isArray(ref)) { | ||
|  |           modelNames = ref; | ||
|  |         } else { | ||
|  |           modelNames = [ref]; | ||
|  |         } | ||
|  | 
 | ||
|  |         isVirtual = true; | ||
|  |       } else if ((refPath = get(schemaForCurrentDoc, 'options.refPath')) != null) { | ||
|  |         isRefPath = true; | ||
|  |         refPath = normalizeRefPath(refPath, doc, options.path); | ||
|  |         modelNames = utils.getValue(refPath, doc); | ||
|  |         if (Array.isArray(modelNames)) { | ||
|  |           modelNames = utils.array.flatten(modelNames); | ||
|  |         } | ||
|  |       } else { | ||
|  |         // We may have a discriminator, in which case we don't want to
 | ||
|  |         // populate using the base model by default
 | ||
|  |         modelNames = discriminatorKey ? null : [model.modelName]; | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!modelNames) { | ||
|  |       return { modelNames: modelNames, isRefPath: isRefPath, refPath: normalizedRefPath, justOne: justOne }; | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!Array.isArray(modelNames)) { | ||
|  |       modelNames = [modelNames]; | ||
|  |     } | ||
|  | 
 | ||
|  |     return { modelNames: modelNames, isRefPath: isRefPath, refPath: normalizedRefPath, justOne: justOne }; | ||
|  |   } | ||
|  | }; | ||
|  | 
 | ||
|  | /*! | ||
|  |  * ignore | ||
|  |  */ | ||
|  | 
 | ||
|  | function handleRefFunction(ref, doc) { | ||
|  |   if (typeof ref === 'function' && !ref[modelSymbol]) { | ||
|  |     return ref.call(doc, doc); | ||
|  |   } | ||
|  |   return ref; | ||
|  | } | ||
|  | 
 | ||
|  | /*! | ||
|  |  * Retrieve the _id of `val` if a Document or Array of Documents. | ||
|  |  * | ||
|  |  * @param {Array|Document|Any} val | ||
|  |  * @return {Array|Document|Any} | ||
|  |  */ | ||
|  | 
 | ||
|  | function convertTo_id(val, schema) { | ||
|  |   if (val != null && val.$__ != null) { | ||
|  |     return val._id; | ||
|  |   } | ||
|  |   if (val != null && val._id != null && (schema == null || !schema.$isSchemaMap)) { | ||
|  |     return val._id; | ||
|  |   } | ||
|  | 
 | ||
|  |   if (Array.isArray(val)) { | ||
|  |     for (let i = 0; i < val.length; ++i) { | ||
|  |       if (val[i] != null && val[i].$__ != null) { | ||
|  |         val[i] = val[i]._id; | ||
|  |       } | ||
|  |     } | ||
|  |     if (val.isMongooseArray && val.$schema()) { | ||
|  |       return val.$schema()._castForPopulate(val, val.$parent()); | ||
|  |     } | ||
|  | 
 | ||
|  |     return [].concat(val); | ||
|  |   } | ||
|  | 
 | ||
|  |   // `populate('map')` may be an object if populating on a doc that hasn't
 | ||
|  |   // been hydrated yet
 | ||
|  |   if (getConstructorName(val) === 'Object' && | ||
|  |       // The intent here is we should only flatten the object if we expect
 | ||
|  |       // to get a Map in the end. Avoid doing this for mixed types.
 | ||
|  |       (schema == null || schema[schemaMixedSymbol] == null)) { | ||
|  |     const ret = []; | ||
|  |     for (const key of Object.keys(val)) { | ||
|  |       ret.push(val[key]); | ||
|  |     } | ||
|  |     return ret; | ||
|  |   } | ||
|  |   // If doc has already been hydrated, e.g. `doc.populate('map').execPopulate()`
 | ||
|  |   // then `val` will already be a map
 | ||
|  |   if (val instanceof Map) { | ||
|  |     return Array.from(val.values()); | ||
|  |   } | ||
|  | 
 | ||
|  |   return val; | ||
|  | } |