assignVals.js 6.2 KB

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