assignVals.js 10 KB

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