getModelsMapForPopulate.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. 'use strict';
  2. const MongooseError = require('../../error/index');
  3. const SkipPopulateValue = require('./SkipPopulateValue');
  4. const get = require('../get');
  5. const getDiscriminatorByValue = require('../discriminator/getDiscriminatorByValue');
  6. const isPathExcluded = require('../projection/isPathExcluded');
  7. const getSchemaTypes = require('./getSchemaTypes');
  8. const getVirtual = require('./getVirtual');
  9. const normalizeRefPath = require('./normalizeRefPath');
  10. const util = require('util');
  11. const utils = require('../../utils');
  12. const modelSymbol = require('../symbols').modelSymbol;
  13. const populateModelSymbol = require('../symbols').populateModelSymbol;
  14. const schemaMixedSymbol = require('../../schema/symbols').schemaMixedSymbol;
  15. module.exports = function getModelsMapForPopulate(model, docs, options) {
  16. let i;
  17. let doc;
  18. const len = docs.length;
  19. const available = {};
  20. const map = [];
  21. const modelNameFromQuery = options.model && options.model.modelName || options.model;
  22. let schema;
  23. let refPath;
  24. let Model;
  25. let currentOptions;
  26. let modelNames;
  27. let modelName;
  28. let modelForFindSchema;
  29. const originalModel = options.model;
  30. let isVirtual = false;
  31. const modelSchema = model.schema;
  32. let allSchemaTypes = getSchemaTypes(modelSchema, null, options.path);
  33. allSchemaTypes = Array.isArray(allSchemaTypes) ? allSchemaTypes : [allSchemaTypes].filter(v => v != null);
  34. const _firstWithRefPath = allSchemaTypes.find(schematype => get(schematype, 'options.refPath', null) != null);
  35. for (i = 0; i < len; i++) {
  36. doc = docs[i];
  37. schema = getSchemaTypes(modelSchema, doc, options.path);
  38. // Special case: populating a path that's a DocumentArray unless
  39. // there's an explicit `ref` or `refPath` re: gh-8946
  40. if (schema != null &&
  41. schema.$isMongooseDocumentArray &&
  42. schema.options.ref == null &&
  43. schema.options.refPath == null) {
  44. continue;
  45. }
  46. const isUnderneathDocArray = schema && schema.$isUnderneathDocArray;
  47. if (isUnderneathDocArray && get(options, 'options.sort') != null) {
  48. return new MongooseError('Cannot populate with `sort` on path ' + options.path +
  49. ' because it is a subproperty of a document array');
  50. }
  51. modelNames = null;
  52. let isRefPath = !!_firstWithRefPath;
  53. let normalizedRefPath = _firstWithRefPath ? get(_firstWithRefPath, 'options.refPath', null) : null;
  54. if (Array.isArray(schema)) {
  55. const schemasArray = schema;
  56. for (const _schema of schemasArray) {
  57. let _modelNames;
  58. let res;
  59. try {
  60. res = _getModelNames(doc, _schema);
  61. _modelNames = res.modelNames;
  62. isRefPath = isRefPath || res.isRefPath;
  63. normalizedRefPath = normalizeRefPath(normalizedRefPath, doc, options.path) ||
  64. res.refPath;
  65. } catch (error) {
  66. return error;
  67. }
  68. if (isRefPath && !res.isRefPath) {
  69. continue;
  70. }
  71. if (!_modelNames) {
  72. continue;
  73. }
  74. modelNames = modelNames || [];
  75. for (const modelName of _modelNames) {
  76. if (modelNames.indexOf(modelName) === -1) {
  77. modelNames.push(modelName);
  78. }
  79. }
  80. }
  81. } else {
  82. try {
  83. const res = _getModelNames(doc, schema);
  84. modelNames = res.modelNames;
  85. isRefPath = res.isRefPath;
  86. normalizedRefPath = res.refPath;
  87. } catch (error) {
  88. return error;
  89. }
  90. if (!modelNames) {
  91. continue;
  92. }
  93. }
  94. const _virtualRes = getVirtual(model.schema, options.path);
  95. const virtual = _virtualRes == null ? null : _virtualRes.virtual;
  96. let localField;
  97. let count = false;
  98. if (virtual && virtual.options) {
  99. const virtualPrefix = _virtualRes.nestedSchemaPath ?
  100. _virtualRes.nestedSchemaPath + '.' : '';
  101. if (typeof virtual.options.localField === 'function') {
  102. localField = virtualPrefix + virtual.options.localField.call(doc, doc);
  103. } else {
  104. localField = virtualPrefix + virtual.options.localField;
  105. }
  106. count = virtual.options.count;
  107. if (virtual.options.skip != null && !options.hasOwnProperty('skip')) {
  108. options.skip = virtual.options.skip;
  109. }
  110. if (virtual.options.limit != null && !options.hasOwnProperty('limit')) {
  111. options.limit = virtual.options.limit;
  112. }
  113. if (virtual.options.perDocumentLimit != null && !options.hasOwnProperty('perDocumentLimit')) {
  114. options.perDocumentLimit = virtual.options.perDocumentLimit;
  115. }
  116. } else {
  117. localField = options.path;
  118. }
  119. let foreignField = virtual && virtual.options ?
  120. virtual.options.foreignField :
  121. '_id';
  122. // `justOne = null` means we don't know from the schema whether the end
  123. // result should be an array or a single doc. This can result from
  124. // populating a POJO using `Model.populate()`
  125. let justOne = null;
  126. if ('justOne' in options && options.justOne !== void 0) {
  127. justOne = options.justOne;
  128. } else if (virtual && virtual.options && virtual.options.refPath) {
  129. const normalizedRefPath =
  130. normalizeRefPath(virtual.options.refPath, doc, options.path);
  131. justOne = !!virtual.options.justOne;
  132. isVirtual = true;
  133. const refValue = utils.getValue(normalizedRefPath, doc);
  134. modelNames = Array.isArray(refValue) ? refValue : [refValue];
  135. } else if (virtual && virtual.options && virtual.options.ref) {
  136. let normalizedRef;
  137. if (typeof virtual.options.ref === 'function') {
  138. normalizedRef = virtual.options.ref.call(doc, doc);
  139. } else {
  140. normalizedRef = virtual.options.ref;
  141. }
  142. justOne = !!virtual.options.justOne;
  143. isVirtual = true;
  144. if (!modelNames) {
  145. modelNames = [].concat(normalizedRef);
  146. }
  147. } else if (schema && !schema[schemaMixedSymbol]) {
  148. // Skip Mixed types because we explicitly don't do casting on those.
  149. justOne = !schema.$isMongooseArray;
  150. }
  151. if (!modelNames) {
  152. continue;
  153. }
  154. if (virtual && (!localField || !foreignField)) {
  155. return new MongooseError('If you are populating a virtual, you must set the ' +
  156. 'localField and foreignField options');
  157. }
  158. options.isVirtual = isVirtual;
  159. options.virtual = virtual;
  160. if (typeof localField === 'function') {
  161. localField = localField.call(doc, doc);
  162. }
  163. if (typeof foreignField === 'function') {
  164. foreignField = foreignField.call(doc);
  165. }
  166. const localFieldPathType = modelSchema._getPathType(localField);
  167. const localFieldPath = localFieldPathType === 'real' ? modelSchema.path(localField) : localFieldPathType.schema;
  168. const localFieldGetters = localFieldPath && localFieldPath.getters ? localFieldPath.getters : [];
  169. let ret;
  170. const _populateOptions = get(options, 'options', {});
  171. const getters = 'getters' in _populateOptions ?
  172. _populateOptions.getters :
  173. options.isVirtual && get(virtual, 'options.getters', false);
  174. if (localFieldGetters.length > 0 && getters) {
  175. const hydratedDoc = (doc.$__ != null) ? doc : model.hydrate(doc);
  176. const localFieldValue = utils.getValue(localField, doc);
  177. if (Array.isArray(localFieldValue)) {
  178. const localFieldHydratedValue = utils.getValue(localField.split('.').slice(0, -1), hydratedDoc);
  179. ret = localFieldValue.map((localFieldArrVal, localFieldArrIndex) =>
  180. localFieldPath.applyGetters(localFieldArrVal, localFieldHydratedValue[localFieldArrIndex]));
  181. } else {
  182. ret = localFieldPath.applyGetters(localFieldValue, hydratedDoc);
  183. }
  184. } else {
  185. ret = convertTo_id(utils.getValue(localField, doc), schema);
  186. }
  187. const id = String(utils.getValue(foreignField, doc));
  188. options._docs[id] = Array.isArray(ret) ? ret.slice() : ret;
  189. let match = get(options, 'match', null) ||
  190. get(currentOptions, 'match', null) ||
  191. get(options, 'virtual.options.match', null) ||
  192. get(options, 'virtual.options.options.match', null);
  193. const hasMatchFunction = typeof match === 'function';
  194. if (hasMatchFunction) {
  195. match = match.call(doc, doc);
  196. }
  197. // Re: gh-8452. Embedded discriminators may not have `refPath`, so clear
  198. // out embedded discriminator docs that don't have a `refPath` on the
  199. // populated path.
  200. if (isRefPath && normalizedRefPath != null) {
  201. const pieces = normalizedRefPath.split('.');
  202. let cur = '';
  203. for (const piece of pieces) {
  204. cur = cur + (cur.length === 0 ? '' : '.') + piece;
  205. const schematype = modelSchema.path(cur);
  206. if (schematype != null &&
  207. schematype.$isMongooseArray &&
  208. schematype.caster.discriminators != null &&
  209. Object.keys(schematype.caster.discriminators).length > 0) {
  210. const subdocs = utils.getValue(cur, doc);
  211. const remnant = options.path.substr(cur.length + 1);
  212. const discriminatorKey = schematype.caster.schema.options.discriminatorKey;
  213. modelNames = [];
  214. for (const subdoc of subdocs) {
  215. const discriminatorName = utils.getValue(discriminatorKey, subdoc);
  216. const discriminator = schematype.caster.discriminators[discriminatorName];
  217. const discriminatorSchema = discriminator && discriminator.schema;
  218. if (discriminatorSchema == null) {
  219. continue;
  220. }
  221. const _path = discriminatorSchema.path(remnant);
  222. if (_path == null || _path.options.refPath == null) {
  223. const docValue = utils.getValue(localField.substr(cur.length + 1), subdoc);
  224. ret = ret.map(v => v === docValue ? SkipPopulateValue(v) : v);
  225. continue;
  226. }
  227. const modelName = utils.getValue(pieces.slice(i + 1).join('.'), subdoc);
  228. modelNames.push(modelName);
  229. }
  230. }
  231. }
  232. }
  233. let k = modelNames.length;
  234. while (k--) {
  235. modelName = modelNames[k];
  236. if (modelName == null) {
  237. continue;
  238. }
  239. // `PopulateOptions#connection`: if the model is passed as a string, the
  240. // connection matters because different connections have different models.
  241. const connection = options.connection != null ? options.connection : model.db;
  242. try {
  243. Model = originalModel && originalModel[modelSymbol] ?
  244. originalModel :
  245. modelName[modelSymbol] ? modelName : connection.model(modelName);
  246. } catch (error) {
  247. return error;
  248. }
  249. let ids = ret;
  250. const flat = Array.isArray(ret) ? utils.array.flatten(ret) : [];
  251. if (isRefPath && Array.isArray(ret) && flat.length === modelNames.length) {
  252. ids = flat.filter((val, i) => modelNames[i] === modelName);
  253. }
  254. if (!available[modelName] || currentOptions.perDocumentLimit != null) {
  255. currentOptions = {
  256. model: Model
  257. };
  258. if (isVirtual && get(virtual, 'options.options')) {
  259. currentOptions.options = utils.clone(virtual.options.options);
  260. }
  261. utils.merge(currentOptions, options);
  262. // Used internally for checking what model was used to populate this
  263. // path.
  264. options[populateModelSymbol] = Model;
  265. available[modelName] = {
  266. model: Model,
  267. options: currentOptions,
  268. match: hasMatchFunction ? [match] : match,
  269. docs: [doc],
  270. ids: [ids],
  271. allIds: [ret],
  272. localField: new Set([localField]),
  273. foreignField: new Set([foreignField]),
  274. justOne: justOne,
  275. isVirtual: isVirtual,
  276. virtual: virtual,
  277. count: count,
  278. [populateModelSymbol]: Model
  279. };
  280. map.push(available[modelName]);
  281. } else {
  282. available[modelName].localField.add(localField);
  283. available[modelName].foreignField.add(foreignField);
  284. available[modelName].docs.push(doc);
  285. available[modelName].ids.push(ids);
  286. available[modelName].allIds.push(ret);
  287. if (hasMatchFunction) {
  288. available[modelName].match.push(match);
  289. }
  290. }
  291. }
  292. }
  293. return map;
  294. function _getModelNames(doc, schema) {
  295. let modelNames;
  296. let discriminatorKey;
  297. let isRefPath = false;
  298. if (schema && schema.caster) {
  299. schema = schema.caster;
  300. }
  301. if (schema && schema.$isSchemaMap) {
  302. schema = schema.$__schemaType;
  303. }
  304. if (!schema && model.discriminators) {
  305. discriminatorKey = model.schema.discriminatorMapping.key;
  306. }
  307. refPath = schema && schema.options && schema.options.refPath;
  308. const normalizedRefPath = normalizeRefPath(refPath, doc, options.path);
  309. if (modelNameFromQuery) {
  310. modelNames = [modelNameFromQuery]; // query options
  311. } else if (normalizedRefPath) {
  312. if (options._queryProjection != null && isPathExcluded(options._queryProjection, normalizedRefPath)) {
  313. throw new MongooseError('refPath `' + normalizedRefPath +
  314. '` must not be excluded in projection, got ' +
  315. util.inspect(options._queryProjection));
  316. }
  317. if (modelSchema.virtuals.hasOwnProperty(normalizedRefPath) && doc.$__ == null) {
  318. modelNames = [modelSchema.virtuals[normalizedRefPath].applyGetters(void 0, doc)];
  319. } else {
  320. modelNames = utils.getValue(normalizedRefPath, doc);
  321. }
  322. if (Array.isArray(modelNames)) {
  323. modelNames = utils.array.flatten(modelNames);
  324. }
  325. isRefPath = true;
  326. } else {
  327. let modelForCurrentDoc = model;
  328. let schemaForCurrentDoc;
  329. if (!schema && discriminatorKey) {
  330. modelForFindSchema = utils.getValue(discriminatorKey, doc);
  331. if (modelForFindSchema) {
  332. // `modelForFindSchema` is the discriminator value, so we might need
  333. // find the discriminated model name
  334. const discriminatorModel = getDiscriminatorByValue(model, modelForFindSchema);
  335. if (discriminatorModel != null) {
  336. modelForCurrentDoc = discriminatorModel;
  337. } else {
  338. try {
  339. modelForCurrentDoc = model.db.model(modelForFindSchema);
  340. } catch (error) {
  341. return error;
  342. }
  343. }
  344. schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path);
  345. if (schemaForCurrentDoc && schemaForCurrentDoc.caster) {
  346. schemaForCurrentDoc = schemaForCurrentDoc.caster;
  347. }
  348. }
  349. } else {
  350. schemaForCurrentDoc = schema;
  351. }
  352. const _virtualRes = getVirtual(modelForCurrentDoc.schema, options.path);
  353. const virtual = _virtualRes == null ? null : _virtualRes.virtual;
  354. let ref;
  355. let refPath;
  356. if ((ref = get(schemaForCurrentDoc, 'options.ref')) != null) {
  357. ref = handleRefFunction(ref, doc);
  358. modelNames = [ref];
  359. } else if ((ref = get(virtual, 'options.ref')) != null) {
  360. ref = handleRefFunction(ref, doc);
  361. // When referencing nested arrays, the ref should be an Array
  362. // of modelNames.
  363. if (Array.isArray(ref)) {
  364. modelNames = ref;
  365. } else {
  366. modelNames = [ref];
  367. }
  368. isVirtual = true;
  369. } else if ((refPath = get(schemaForCurrentDoc, 'options.refPath')) != null) {
  370. isRefPath = true;
  371. refPath = normalizeRefPath(refPath, doc, options.path);
  372. modelNames = utils.getValue(refPath, doc);
  373. if (Array.isArray(modelNames)) {
  374. modelNames = utils.array.flatten(modelNames);
  375. }
  376. } else {
  377. // We may have a discriminator, in which case we don't want to
  378. // populate using the base model by default
  379. modelNames = discriminatorKey ? null : [model.modelName];
  380. }
  381. }
  382. if (!modelNames) {
  383. return { modelNames: modelNames, isRefPath: isRefPath, refPath: normalizedRefPath };
  384. }
  385. if (!Array.isArray(modelNames)) {
  386. modelNames = [modelNames];
  387. }
  388. return { modelNames: modelNames, isRefPath: isRefPath, refPath: normalizedRefPath };
  389. }
  390. };
  391. /*!
  392. * ignore
  393. */
  394. function handleRefFunction(ref, doc) {
  395. if (typeof ref === 'function' && !ref[modelSymbol]) {
  396. return ref.call(doc, doc);
  397. }
  398. return ref;
  399. }
  400. /*!
  401. * Retrieve the _id of `val` if a Document or Array of Documents.
  402. *
  403. * @param {Array|Document|Any} val
  404. * @return {Array|Document|Any}
  405. */
  406. function convertTo_id(val, schema) {
  407. if (val != null && val.$__ != null) return val._id;
  408. if (Array.isArray(val)) {
  409. for (let i = 0; i < val.length; ++i) {
  410. if (val[i] != null && val[i].$__ != null) {
  411. val[i] = val[i]._id;
  412. }
  413. }
  414. if (val.isMongooseArray && val.$schema()) {
  415. return val.$schema().cast(val, val.$parent());
  416. }
  417. return [].concat(val);
  418. }
  419. // `populate('map')` may be an object if populating on a doc that hasn't
  420. // been hydrated yet
  421. if (val != null &&
  422. val.constructor.name === 'Object' &&
  423. // The intent here is we should only flatten the object if we expect
  424. // to get a Map in the end. Avoid doing this for mixed types.
  425. (schema == null || schema[schemaMixedSymbol] == null)) {
  426. const ret = [];
  427. for (const key of Object.keys(val)) {
  428. ret.push(val[key]);
  429. }
  430. return ret;
  431. }
  432. // If doc has already been hydrated, e.g. `doc.populate('map').execPopulate()`
  433. // then `val` will already be a map
  434. if (val instanceof Map) {
  435. return Array.from(val.values());
  436. }
  437. return val;
  438. }