'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; }