assignVals.js 7.1 KB

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