assignVals.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. 'use strict';
  2. const SkipPopulateValue = require('./SkipPopulateValue');
  3. const assignRawDocsToIdStructure = require('./assignRawDocsToIdStructure');
  4. const get = require('../get');
  5. const getVirtual = require('./getVirtual');
  6. const leanPopulateMap = require('./leanPopulateMap');
  7. const lookupLocalFields = require('./lookupLocalFields');
  8. const mpath = require('mpath');
  9. const sift = require('sift').default;
  10. const utils = require('../../utils');
  11. module.exports = function assignVals(o) {
  12. // Options that aren't explicitly listed in `populateOptions`
  13. const userOptions = Object.assign({}, get(o, 'allOptions.options.options'), get(o, 'allOptions.options'));
  14. // `o.options` contains options explicitly listed in `populateOptions`, like
  15. // `match` and `limit`.
  16. const populateOptions = Object.assign({}, o.options, userOptions, {
  17. justOne: o.justOne
  18. });
  19. populateOptions.$nullIfNotFound = o.isVirtual;
  20. const populatedModel = o.populatedModel;
  21. const originalIds = [].concat(o.rawIds);
  22. // replace the original ids in our intermediate _ids structure
  23. // with the documents found by query
  24. o.allIds = [].concat(o.allIds);
  25. assignRawDocsToIdStructure(o.rawIds, o.rawDocs, o.rawOrder, populateOptions);
  26. // now update the original documents being populated using the
  27. // result structure that contains real documents.
  28. const docs = o.docs;
  29. const rawIds = o.rawIds;
  30. const options = o.options;
  31. const count = o.count && o.isVirtual;
  32. let i;
  33. function setValue(val) {
  34. if (count) {
  35. return val;
  36. }
  37. if (val instanceof SkipPopulateValue) {
  38. return val.val;
  39. }
  40. const _allIds = o.allIds[i];
  41. if (o.justOne === true && Array.isArray(val)) {
  42. // Might be an embedded discriminator (re: gh-9244) with multiple models, so make sure to pick the right
  43. // model before assigning.
  44. const ret = [];
  45. for (const doc of val) {
  46. const _docPopulatedModel = leanPopulateMap.get(doc);
  47. if (_docPopulatedModel == null || _docPopulatedModel === populatedModel) {
  48. ret.push(doc);
  49. }
  50. }
  51. // Since we don't want to have to create a new mongoosearray, make sure to
  52. // modify the array in place
  53. while (val.length > ret.length) {
  54. Array.prototype.pop.apply(val, []);
  55. }
  56. for (let i = 0; i < ret.length; ++i) {
  57. val[i] = ret[i];
  58. }
  59. return valueFilter(val[0], options, populateOptions, _allIds);
  60. } else if (o.justOne === false && !Array.isArray(val)) {
  61. return valueFilter([val], options, populateOptions, _allIds);
  62. }
  63. return valueFilter(val, options, populateOptions, _allIds);
  64. }
  65. for (i = 0; i < docs.length; ++i) {
  66. const _path = o.path.endsWith('.$*') ? o.path.slice(0, -3) : o.path;
  67. const existingVal = mpath.get(_path, docs[i], lookupLocalFields);
  68. if (existingVal == null && !getVirtual(o.originalModel.schema, _path)) {
  69. continue;
  70. }
  71. let valueToSet;
  72. if (count) {
  73. valueToSet = numDocs(rawIds[i]);
  74. } else if (Array.isArray(o.match)) {
  75. valueToSet = Array.isArray(rawIds[i]) ?
  76. rawIds[i].filter(sift(o.match[i])) :
  77. [rawIds[i]].filter(sift(o.match[i]))[0];
  78. } else {
  79. valueToSet = rawIds[i];
  80. }
  81. // If we're populating a map, the existing value will be an object, so
  82. // we need to transform again
  83. const originalSchema = o.originalModel.schema;
  84. const isDoc = get(docs[i], '$__', null) != null;
  85. let isMap = isDoc ?
  86. existingVal instanceof Map :
  87. utils.isPOJO(existingVal);
  88. // If we pass the first check, also make sure the local field's schematype
  89. // is map (re: gh-6460)
  90. isMap = isMap && get(originalSchema._getSchema(_path), '$isSchemaMap');
  91. if (!o.isVirtual && isMap) {
  92. const _keys = existingVal instanceof Map ?
  93. Array.from(existingVal.keys()) :
  94. Object.keys(existingVal);
  95. valueToSet = valueToSet.reduce((cur, v, i) => {
  96. cur.set(_keys[i], v);
  97. return cur;
  98. }, new Map());
  99. }
  100. if (isDoc && Array.isArray(valueToSet)) {
  101. for (const val of valueToSet) {
  102. if (val != null && val.$__ != null) {
  103. val.$__.parent = docs[i];
  104. }
  105. }
  106. } else if (isDoc && valueToSet != null && valueToSet.$__ != null) {
  107. valueToSet.$__.parent = docs[i];
  108. }
  109. if (o.isVirtual && isDoc) {
  110. docs[i].populated(_path, o.justOne ? originalIds[0] : originalIds, o.allOptions);
  111. // If virtual populate and doc is already init-ed, need to walk through
  112. // the actual doc to set rather than setting `_doc` directly
  113. mpath.set(_path, valueToSet, docs[i], setValue);
  114. continue;
  115. }
  116. const parts = _path.split('.');
  117. let cur = docs[i];
  118. const curPath = parts[0];
  119. for (let j = 0; j < parts.length - 1; ++j) {
  120. // If we get to an array with a dotted path, like `arr.foo`, don't set
  121. // `foo` on the array.
  122. if (Array.isArray(cur) && !utils.isArrayIndex(parts[j])) {
  123. break;
  124. }
  125. if (parts[j] === '$*') {
  126. break;
  127. }
  128. if (cur[parts[j]] == null) {
  129. // If nothing to set, avoid creating an unnecessary array. Otherwise
  130. // we'll end up with a single doc in the array with only defaults.
  131. // See gh-8342, gh-8455
  132. const schematype = originalSchema._getSchema(curPath);
  133. if (valueToSet == null && schematype != null && schematype.$isMongooseArray) {
  134. break;
  135. }
  136. cur[parts[j]] = {};
  137. }
  138. cur = cur[parts[j]];
  139. // If the property in MongoDB is a primitive, we won't be able to populate
  140. // the nested path, so skip it. See gh-7545
  141. if (typeof cur !== 'object') {
  142. break;
  143. }
  144. }
  145. if (docs[i].$__) {
  146. docs[i].populated(_path, o.allIds[i], o.allOptions);
  147. }
  148. // If lean, need to check that each individual virtual respects
  149. // `justOne`, because you may have a populated virtual with `justOne`
  150. // underneath an array. See gh-6867
  151. mpath.set(_path, valueToSet, docs[i], lookupLocalFields, setValue, false);
  152. }
  153. };
  154. function numDocs(v) {
  155. if (Array.isArray(v)) {
  156. // If setting underneath an array of populated subdocs, we may have an
  157. // array of arrays. See gh-7573
  158. if (v.some(el => Array.isArray(el))) {
  159. return v.map(el => numDocs(el));
  160. }
  161. return v.length;
  162. }
  163. return v == null ? 0 : 1;
  164. }
  165. /*!
  166. * 1) Apply backwards compatible find/findOne behavior to sub documents
  167. *
  168. * find logic:
  169. * a) filter out non-documents
  170. * b) remove _id from sub docs when user specified
  171. *
  172. * findOne
  173. * a) if no doc found, set to null
  174. * b) remove _id from sub docs when user specified
  175. *
  176. * 2) Remove _ids when specified by users query.
  177. *
  178. * background:
  179. * _ids are left in the query even when user excludes them so
  180. * that population mapping can occur.
  181. */
  182. function valueFilter(val, assignmentOpts, populateOptions, allIds) {
  183. const userSpecifiedTransform = typeof populateOptions.transform === 'function';
  184. const transform = userSpecifiedTransform ? populateOptions.transform : noop;
  185. if (Array.isArray(val)) {
  186. // find logic
  187. const ret = [];
  188. const numValues = val.length;
  189. for (let i = 0; i < numValues; ++i) {
  190. let subdoc = val[i];
  191. const _allIds = Array.isArray(allIds) ? allIds[i] : allIds;
  192. if (!isPopulatedObject(subdoc) && (!populateOptions.retainNullValues || subdoc != null) && !userSpecifiedTransform) {
  193. continue;
  194. } else if (userSpecifiedTransform) {
  195. subdoc = transform(isPopulatedObject(subdoc) ? subdoc : null, _allIds);
  196. }
  197. maybeRemoveId(subdoc, assignmentOpts);
  198. ret.push(subdoc);
  199. if (assignmentOpts.originalLimit &&
  200. ret.length >= assignmentOpts.originalLimit) {
  201. break;
  202. }
  203. }
  204. // Since we don't want to have to create a new mongoosearray, make sure to
  205. // modify the array in place
  206. while (val.length > ret.length) {
  207. Array.prototype.pop.apply(val, []);
  208. }
  209. for (let i = 0; i < ret.length; ++i) {
  210. val[i] = ret[i];
  211. }
  212. return val;
  213. }
  214. // findOne
  215. if (isPopulatedObject(val) || utils.isPOJO(val)) {
  216. maybeRemoveId(val, assignmentOpts);
  217. return transform(val, allIds);
  218. }
  219. if (val instanceof Map) {
  220. return val;
  221. }
  222. if (populateOptions.justOne === false) {
  223. return [];
  224. }
  225. return val == null ? transform(val, allIds) : transform(null, allIds);
  226. }
  227. /*!
  228. * Remove _id from `subdoc` if user specified "lean" query option
  229. */
  230. function maybeRemoveId(subdoc, assignmentOpts) {
  231. if (subdoc != null && assignmentOpts.excludeId) {
  232. if (typeof subdoc.$__setValue === 'function') {
  233. delete subdoc._doc._id;
  234. } else {
  235. delete subdoc._id;
  236. }
  237. }
  238. }
  239. /*!
  240. * Determine if `obj` is something we can set a populated path to. Can be a
  241. * document, a lean document, or an array/map that contains docs.
  242. */
  243. function isPopulatedObject(obj) {
  244. if (obj == null) {
  245. return false;
  246. }
  247. return Array.isArray(obj) ||
  248. obj.$isMongooseMap ||
  249. obj.$__ != null ||
  250. leanPopulateMap.has(obj);
  251. }
  252. function noop(v) {
  253. return v;
  254. }