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.
		
		
		
		
		
			
		
			
				
					
					
						
							288 lines
						
					
					
						
							8.9 KiB
						
					
					
				
			
		
		
	
	
							288 lines
						
					
					
						
							8.9 KiB
						
					
					
				| 'use strict';
 | |
| 
 | |
| const SkipPopulateValue = require('./SkipPopulateValue');
 | |
| const assignRawDocsToIdStructure = require('./assignRawDocsToIdStructure');
 | |
| const get = require('../get');
 | |
| const getVirtual = require('./getVirtual');
 | |
| const leanPopulateMap = require('./leanPopulateMap');
 | |
| const lookupLocalFields = require('./lookupLocalFields');
 | |
| const mpath = require('mpath');
 | |
| const sift = require('sift').default;
 | |
| const utils = require('../../utils');
 | |
| 
 | |
| module.exports = function assignVals(o) {
 | |
|   // Options that aren't explicitly listed in `populateOptions`
 | |
|   const userOptions = Object.assign({}, get(o, 'allOptions.options.options'), get(o, 'allOptions.options'));
 | |
|   // `o.options` contains options explicitly listed in `populateOptions`, like
 | |
|   // `match` and `limit`.
 | |
|   const populateOptions = Object.assign({}, o.options, userOptions, {
 | |
|     justOne: o.justOne
 | |
|   });
 | |
|   populateOptions.$nullIfNotFound = o.isVirtual;
 | |
|   const populatedModel = o.populatedModel;
 | |
| 
 | |
|   const originalIds = [].concat(o.rawIds);
 | |
| 
 | |
|   // replace the original ids in our intermediate _ids structure
 | |
|   // with the documents found by query
 | |
|   o.allIds = [].concat(o.allIds);
 | |
|   assignRawDocsToIdStructure(o.rawIds, o.rawDocs, o.rawOrder, populateOptions);
 | |
| 
 | |
|   // now update the original documents being populated using the
 | |
|   // result structure that contains real documents.
 | |
|   const docs = o.docs;
 | |
|   const rawIds = o.rawIds;
 | |
|   const options = o.options;
 | |
|   const count = o.count && o.isVirtual;
 | |
|   let i;
 | |
| 
 | |
|   function setValue(val) {
 | |
|     if (count) {
 | |
|       return val;
 | |
|     }
 | |
|     if (val instanceof SkipPopulateValue) {
 | |
|       return val.val;
 | |
|     }
 | |
|     if (val === void 0) {
 | |
|       return val;
 | |
|     }
 | |
| 
 | |
|     const _allIds = o.allIds[i];
 | |
| 
 | |
|     if (o.justOne === true && Array.isArray(val)) {
 | |
|       // Might be an embedded discriminator (re: gh-9244) with multiple models, so make sure to pick the right
 | |
|       // model before assigning.
 | |
|       const ret = [];
 | |
|       for (const doc of val) {
 | |
|         const _docPopulatedModel = leanPopulateMap.get(doc);
 | |
|         if (_docPopulatedModel == null || _docPopulatedModel === populatedModel) {
 | |
|           ret.push(doc);
 | |
|         }
 | |
|       }
 | |
|       // Since we don't want to have to create a new mongoosearray, make sure to
 | |
|       // modify the array in place
 | |
|       while (val.length > ret.length) {
 | |
|         Array.prototype.pop.apply(val, []);
 | |
|       }
 | |
|       for (let i = 0; i < ret.length; ++i) {
 | |
|         val[i] = ret[i];
 | |
|       }
 | |
| 
 | |
|       return valueFilter(val[0], options, populateOptions, _allIds);
 | |
|     } else if (o.justOne === false && !Array.isArray(val)) {
 | |
|       return valueFilter([val], options, populateOptions, _allIds);
 | |
|     }
 | |
|     return valueFilter(val, options, populateOptions, _allIds);
 | |
|   }
 | |
| 
 | |
|   for (i = 0; i < docs.length; ++i) {
 | |
|     const _path = o.path.endsWith('.$*') ? o.path.slice(0, -3) : o.path;
 | |
|     const existingVal = mpath.get(_path, docs[i], lookupLocalFields);
 | |
|     if (existingVal == null && !getVirtual(o.originalModel.schema, _path)) {
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     let valueToSet;
 | |
|     if (count) {
 | |
|       valueToSet = numDocs(rawIds[i]);
 | |
|     } else if (Array.isArray(o.match)) {
 | |
|       valueToSet = Array.isArray(rawIds[i]) ?
 | |
|         rawIds[i].filter(sift(o.match[i])) :
 | |
|         [rawIds[i]].filter(sift(o.match[i]))[0];
 | |
|     } else {
 | |
|       valueToSet = rawIds[i];
 | |
|     }
 | |
| 
 | |
|     // If we're populating a map, the existing value will be an object, so
 | |
|     // we need to transform again
 | |
|     const originalSchema = o.originalModel.schema;
 | |
|     const isDoc = get(docs[i], '$__', null) != null;
 | |
|     let isMap = isDoc ?
 | |
|       existingVal instanceof Map :
 | |
|       utils.isPOJO(existingVal);
 | |
|     // If we pass the first check, also make sure the local field's schematype
 | |
|     // is map (re: gh-6460)
 | |
|     isMap = isMap && get(originalSchema._getSchema(_path), '$isSchemaMap');
 | |
|     if (!o.isVirtual && isMap) {
 | |
|       const _keys = existingVal instanceof Map ?
 | |
|         Array.from(existingVal.keys()) :
 | |
|         Object.keys(existingVal);
 | |
|       valueToSet = valueToSet.reduce((cur, v, i) => {
 | |
|         cur.set(_keys[i], v);
 | |
|         return cur;
 | |
|       }, new Map());
 | |
|     }
 | |
| 
 | |
|     if (isDoc && Array.isArray(valueToSet)) {
 | |
|       for (const val of valueToSet) {
 | |
|         if (val != null && val.$__ != null) {
 | |
|           val.$__.parent = docs[i];
 | |
|         }
 | |
|       }
 | |
|     } else if (isDoc && valueToSet != null && valueToSet.$__ != null) {
 | |
|       valueToSet.$__.parent = docs[i];
 | |
|     }
 | |
| 
 | |
|     if (o.isVirtual && isDoc) {
 | |
|       docs[i].populated(_path, o.justOne ? originalIds[0] : originalIds, o.allOptions);
 | |
|       // If virtual populate and doc is already init-ed, need to walk through
 | |
|       // the actual doc to set rather than setting `_doc` directly
 | |
|       mpath.set(_path, valueToSet, docs[i], setValue);
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     const parts = _path.split('.');
 | |
|     let cur = docs[i];
 | |
|     const curPath = parts[0];
 | |
|     for (let j = 0; j < parts.length - 1; ++j) {
 | |
|       // If we get to an array with a dotted path, like `arr.foo`, don't set
 | |
|       // `foo` on the array.
 | |
|       if (Array.isArray(cur) && !utils.isArrayIndex(parts[j])) {
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       if (parts[j] === '$*') {
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       if (cur[parts[j]] == null) {
 | |
|         // If nothing to set, avoid creating an unnecessary array. Otherwise
 | |
|         // we'll end up with a single doc in the array with only defaults.
 | |
|         // See gh-8342, gh-8455
 | |
|         const schematype = originalSchema._getSchema(curPath);
 | |
|         if (valueToSet == null && schematype != null && schematype.$isMongooseArray) {
 | |
|           break;
 | |
|         }
 | |
|         cur[parts[j]] = {};
 | |
|       }
 | |
|       cur = cur[parts[j]];
 | |
|       // If the property in MongoDB is a primitive, we won't be able to populate
 | |
|       // the nested path, so skip it. See gh-7545
 | |
|       if (typeof cur !== 'object') {
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
|     if (docs[i].$__) {
 | |
|       docs[i].populated(_path, o.allIds[i], o.allOptions);
 | |
|     }
 | |
| 
 | |
|     // If lean, need to check that each individual virtual respects
 | |
|     // `justOne`, because you may have a populated virtual with `justOne`
 | |
|     // underneath an array. See gh-6867
 | |
|     mpath.set(_path, valueToSet, docs[i], lookupLocalFields, setValue, false);
 | |
|   }
 | |
| };
 | |
| 
 | |
| function numDocs(v) {
 | |
|   if (Array.isArray(v)) {
 | |
|     // If setting underneath an array of populated subdocs, we may have an
 | |
|     // array of arrays. See gh-7573
 | |
|     if (v.some(el => Array.isArray(el))) {
 | |
|       return v.map(el => numDocs(el));
 | |
|     }
 | |
|     return v.length;
 | |
|   }
 | |
|   return v == null ? 0 : 1;
 | |
| }
 | |
| 
 | |
| /*!
 | |
|  * 1) Apply backwards compatible find/findOne behavior to sub documents
 | |
|  *
 | |
|  *    find logic:
 | |
|  *      a) filter out non-documents
 | |
|  *      b) remove _id from sub docs when user specified
 | |
|  *
 | |
|  *    findOne
 | |
|  *      a) if no doc found, set to null
 | |
|  *      b) remove _id from sub docs when user specified
 | |
|  *
 | |
|  * 2) Remove _ids when specified by users query.
 | |
|  *
 | |
|  * background:
 | |
|  * _ids are left in the query even when user excludes them so
 | |
|  * that population mapping can occur.
 | |
|  */
 | |
| 
 | |
| function valueFilter(val, assignmentOpts, populateOptions, allIds) {
 | |
|   const userSpecifiedTransform = typeof populateOptions.transform === 'function';
 | |
|   const transform = userSpecifiedTransform ? populateOptions.transform : noop;
 | |
|   if (Array.isArray(val)) {
 | |
|     // find logic
 | |
|     const ret = [];
 | |
|     const numValues = val.length;
 | |
|     for (let i = 0; i < numValues; ++i) {
 | |
|       let subdoc = val[i];
 | |
|       const _allIds = Array.isArray(allIds) ? allIds[i] : allIds;
 | |
|       if (!isPopulatedObject(subdoc) && (!populateOptions.retainNullValues || subdoc != null) && !userSpecifiedTransform) {
 | |
|         continue;
 | |
|       } else if (userSpecifiedTransform) {
 | |
|         subdoc = transform(isPopulatedObject(subdoc) ? subdoc : null, _allIds);
 | |
|       }
 | |
|       maybeRemoveId(subdoc, assignmentOpts);
 | |
|       ret.push(subdoc);
 | |
|       if (assignmentOpts.originalLimit &&
 | |
|           ret.length >= assignmentOpts.originalLimit) {
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Since we don't want to have to create a new mongoosearray, make sure to
 | |
|     // modify the array in place
 | |
|     while (val.length > ret.length) {
 | |
|       Array.prototype.pop.apply(val, []);
 | |
|     }
 | |
|     for (let i = 0; i < ret.length; ++i) {
 | |
|       val[i] = ret[i];
 | |
|     }
 | |
|     return val;
 | |
|   }
 | |
| 
 | |
|   // findOne
 | |
|   if (isPopulatedObject(val) || utils.isPOJO(val)) {
 | |
|     maybeRemoveId(val, assignmentOpts);
 | |
|     return transform(val, allIds);
 | |
|   }
 | |
|   if (val instanceof Map) {
 | |
|     return val;
 | |
|   }
 | |
| 
 | |
|   if (populateOptions.justOne === false) {
 | |
|     return [];
 | |
|   }
 | |
| 
 | |
|   return val == null ? transform(val, allIds) : transform(null, allIds);
 | |
| }
 | |
| 
 | |
| /*!
 | |
|  * Remove _id from `subdoc` if user specified "lean" query option
 | |
|  */
 | |
| 
 | |
| function maybeRemoveId(subdoc, assignmentOpts) {
 | |
|   if (subdoc != null && assignmentOpts.excludeId) {
 | |
|     if (typeof subdoc.$__setValue === 'function') {
 | |
|       delete subdoc._doc._id;
 | |
|     } else {
 | |
|       delete subdoc._id;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /*!
 | |
|  * Determine if `obj` is something we can set a populated path to. Can be a
 | |
|  * document, a lean document, or an array/map that contains docs.
 | |
|  */
 | |
| 
 | |
| function isPopulatedObject(obj) {
 | |
|   if (obj == null) {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   return Array.isArray(obj) ||
 | |
|     obj.$isMongooseMap ||
 | |
|     obj.$__ != null ||
 | |
|     leanPopulateMap.has(obj);
 | |
| }
 | |
| 
 | |
| function noop(v) {
 | |
|   return v;
 | |
| } |