assignVals.js 11 KB

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