getModelsMapForPopulate.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  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 getConstructorName = require('../getConstructorName');
  7. const getSchemaTypes = require('./getSchemaTypes');
  8. const getVirtual = require('./getVirtual');
  9. const lookupLocalFields = require('./lookupLocalFields');
  10. const mpath = require('mpath');
  11. const modelNamesFromRefPath = require('./modelNamesFromRefPath');
  12. const utils = require('../../utils');
  13. const modelSymbol = require('../symbols').modelSymbol;
  14. const populateModelSymbol = require('../symbols').populateModelSymbol;
  15. const schemaMixedSymbol = require('../../schema/symbols').schemaMixedSymbol;
  16. module.exports = function getModelsMapForPopulate(model, docs, options) {
  17. let doc;
  18. const len = docs.length;
  19. const map = [];
  20. const modelNameFromQuery = options.model && options.model.modelName || options.model;
  21. let schema;
  22. let refPath;
  23. let modelNames;
  24. const available = {};
  25. const modelSchema = model.schema;
  26. // Populating a nested path should always be a no-op re: #9073.
  27. // People shouldn't do this, but apparently they do.
  28. if (options._localModel != null && options._localModel.schema.nested[options.path]) {
  29. return [];
  30. }
  31. const _virtualRes = getVirtual(model.schema, options.path);
  32. const virtual = _virtualRes == null ? null : _virtualRes.virtual;
  33. if (virtual != null) {
  34. return _virtualPopulate(model, docs, options, _virtualRes);
  35. }
  36. let allSchemaTypes = getSchemaTypes(model, modelSchema, null, options.path);
  37. allSchemaTypes = Array.isArray(allSchemaTypes) ? allSchemaTypes : [allSchemaTypes].filter(v => v != null);
  38. if (allSchemaTypes.length === 0 && options.strictPopulate !== false && options._localModel != null) {
  39. return new MongooseError('Cannot populate path `' + (options._fullPath || options.path) +
  40. '` because it is not in your schema. Set the `strictPopulate` option ' +
  41. 'to false to override.');
  42. }
  43. for (let i = 0; i < len; i++) {
  44. doc = docs[i];
  45. let justOne = null;
  46. const docSchema = doc != null && doc.$__ != null ? doc.$__schema : modelSchema;
  47. schema = getSchemaTypes(model, docSchema, doc, options.path);
  48. // Special case: populating a path that's a DocumentArray unless
  49. // there's an explicit `ref` or `refPath` re: gh-8946
  50. if (schema != null &&
  51. schema.$isMongooseDocumentArray &&
  52. schema.options.ref == null &&
  53. schema.options.refPath == null) {
  54. continue;
  55. }
  56. const isUnderneathDocArray = schema && schema.$isUnderneathDocArray;
  57. if (isUnderneathDocArray && get(options, 'options.sort') != null) {
  58. return new MongooseError('Cannot populate with `sort` on path ' + options.path +
  59. ' because it is a subproperty of a document array');
  60. }
  61. modelNames = null;
  62. let isRefPath = false;
  63. let normalizedRefPath = null;
  64. let schemaOptions = null;
  65. let modelNamesInOrder = null;
  66. if (schema != null && schema.instance === 'Embedded' && schema.options.ref) {
  67. const data = {
  68. localField: options.path + '._id',
  69. foreignField: '_id',
  70. justOne: true
  71. };
  72. const res = _getModelNames(doc, schema, modelNameFromQuery, model);
  73. const unpopulatedValue = mpath.get(options.path, doc);
  74. const id = mpath.get('_id', unpopulatedValue);
  75. addModelNamesToMap(model, map, available, res.modelNames, options, data, id, doc, schemaOptions, unpopulatedValue);
  76. continue;
  77. }
  78. if (Array.isArray(schema)) {
  79. const schemasArray = schema;
  80. for (const _schema of schemasArray) {
  81. let _modelNames;
  82. let res;
  83. try {
  84. res = _getModelNames(doc, _schema, modelNameFromQuery, model);
  85. _modelNames = res.modelNames;
  86. isRefPath = isRefPath || res.isRefPath;
  87. normalizedRefPath = normalizedRefPath || res.refPath;
  88. justOne = res.justOne;
  89. } catch (error) {
  90. return error;
  91. }
  92. if (isRefPath && !res.isRefPath) {
  93. continue;
  94. }
  95. if (!_modelNames) {
  96. continue;
  97. }
  98. modelNames = modelNames || [];
  99. for (const modelName of _modelNames) {
  100. if (modelNames.indexOf(modelName) === -1) {
  101. modelNames.push(modelName);
  102. }
  103. }
  104. }
  105. } else {
  106. try {
  107. const res = _getModelNames(doc, schema, modelNameFromQuery, model);
  108. modelNames = res.modelNames;
  109. isRefPath = res.isRefPath;
  110. normalizedRefPath = normalizedRefPath || res.refPath;
  111. justOne = res.justOne;
  112. schemaOptions = get(schema, 'options.populate', null);
  113. // Dedupe, because `refPath` can return duplicates of the same model name,
  114. // and that causes perf issues.
  115. if (isRefPath) {
  116. modelNamesInOrder = modelNames;
  117. modelNames = Array.from(new Set(modelNames));
  118. }
  119. } catch (error) {
  120. return error;
  121. }
  122. if (!modelNames) {
  123. continue;
  124. }
  125. }
  126. const data = {};
  127. const localField = options.path;
  128. const foreignField = '_id';
  129. // `justOne = null` means we don't know from the schema whether the end
  130. // result should be an array or a single doc. This can result from
  131. // populating a POJO using `Model.populate()`
  132. if ('justOne' in options && options.justOne !== void 0) {
  133. justOne = options.justOne;
  134. } else if (schema && !schema[schemaMixedSymbol]) {
  135. // Skip Mixed types because we explicitly don't do casting on those.
  136. if (options.path.endsWith('.' + schema.path) || options.path === schema.path) {
  137. justOne = Array.isArray(schema) ?
  138. schema.every(schema => !schema.$isMongooseArray) :
  139. !schema.$isMongooseArray;
  140. }
  141. }
  142. if (!modelNames) {
  143. continue;
  144. }
  145. data.isVirtual = false;
  146. data.justOne = justOne;
  147. data.localField = localField;
  148. data.foreignField = foreignField;
  149. // Get local fields
  150. const ret = _getLocalFieldValues(doc, localField, model, options, null, schema);
  151. const id = String(utils.getValue(foreignField, doc));
  152. options._docs[id] = Array.isArray(ret) ? ret.slice() : ret;
  153. let match = get(options, 'match', null);
  154. const hasMatchFunction = typeof match === 'function';
  155. if (hasMatchFunction) {
  156. match = match.call(doc, doc);
  157. }
  158. data.match = match;
  159. data.hasMatchFunction = hasMatchFunction;
  160. data.isRefPath = isRefPath;
  161. data.modelNamesInOrder = modelNamesInOrder;
  162. if (isRefPath) {
  163. const embeddedDiscriminatorModelNames = _findRefPathForDiscriminators(doc,
  164. modelSchema, data, options, normalizedRefPath, ret);
  165. modelNames = embeddedDiscriminatorModelNames || modelNames;
  166. }
  167. try {
  168. addModelNamesToMap(model, map, available, modelNames, options, data, ret, doc, schemaOptions);
  169. } catch (err) {
  170. return err;
  171. }
  172. }
  173. return map;
  174. function _getModelNames(doc, schema, modelNameFromQuery, model) {
  175. let modelNames;
  176. let isRefPath = false;
  177. let justOne = null;
  178. if (schema && schema.instance === 'Array') {
  179. schema = schema.caster;
  180. }
  181. if (schema && schema.$isSchemaMap) {
  182. schema = schema.$__schemaType;
  183. }
  184. const ref = schema && schema.options && schema.options.ref;
  185. refPath = schema && schema.options && schema.options.refPath;
  186. if (schema != null &&
  187. schema[schemaMixedSymbol] &&
  188. !ref &&
  189. !refPath &&
  190. !modelNameFromQuery) {
  191. return { modelNames: null };
  192. }
  193. if (modelNameFromQuery) {
  194. modelNames = [modelNameFromQuery]; // query options
  195. } else if (refPath != null) {
  196. if (typeof refPath === 'function') {
  197. const subdocPath = options.path.slice(0, options.path.length - schema.path.length - 1);
  198. const vals = mpath.get(subdocPath, doc, lookupLocalFields);
  199. const subdocsBeingPopulated = Array.isArray(vals) ?
  200. utils.array.flatten(vals) :
  201. (vals ? [vals] : []);
  202. modelNames = new Set();
  203. for (const subdoc of subdocsBeingPopulated) {
  204. refPath = refPath.call(subdoc, subdoc, options.path);
  205. modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection).
  206. forEach(name => modelNames.add(name));
  207. }
  208. modelNames = Array.from(modelNames);
  209. } else {
  210. modelNames = modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection);
  211. }
  212. isRefPath = true;
  213. } else {
  214. let ref;
  215. let refPath;
  216. let schemaForCurrentDoc;
  217. let discriminatorValue;
  218. let modelForCurrentDoc = model;
  219. const discriminatorKey = model.schema.options.discriminatorKey;
  220. if (!schema && discriminatorKey && (discriminatorValue = utils.getValue(discriminatorKey, doc))) {
  221. // `modelNameForFind` is the discriminator value, so we might need
  222. // find the discriminated model name
  223. const discriminatorModel = getDiscriminatorByValue(model.discriminators, discriminatorValue) || model;
  224. if (discriminatorModel != null) {
  225. modelForCurrentDoc = discriminatorModel;
  226. } else {
  227. try {
  228. modelForCurrentDoc = _getModelFromConn(model.db, discriminatorValue);
  229. } catch (error) {
  230. return error;
  231. }
  232. }
  233. schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path);
  234. if (schemaForCurrentDoc && schemaForCurrentDoc.caster) {
  235. schemaForCurrentDoc = schemaForCurrentDoc.caster;
  236. }
  237. } else {
  238. schemaForCurrentDoc = schema;
  239. }
  240. if (schemaForCurrentDoc != null) {
  241. justOne = !schemaForCurrentDoc.$isMongooseArray && !schemaForCurrentDoc._arrayPath;
  242. }
  243. if ((ref = get(schemaForCurrentDoc, 'options.ref')) != null) {
  244. if (schemaForCurrentDoc != null &&
  245. typeof ref === 'function' &&
  246. options.path.endsWith('.' + schemaForCurrentDoc.path)) {
  247. // Ensure correct context for ref functions: subdoc, not top-level doc. See gh-8469
  248. modelNames = new Set();
  249. const subdocPath = options.path.slice(0, options.path.length - schemaForCurrentDoc.path.length - 1);
  250. const vals = mpath.get(subdocPath, doc, lookupLocalFields);
  251. const subdocsBeingPopulated = Array.isArray(vals) ?
  252. utils.array.flatten(vals) :
  253. (vals ? [vals] : []);
  254. for (const subdoc of subdocsBeingPopulated) {
  255. modelNames.add(handleRefFunction(ref, subdoc));
  256. }
  257. if (subdocsBeingPopulated.length === 0) {
  258. modelNames = [handleRefFunction(ref, doc)];
  259. } else {
  260. modelNames = Array.from(modelNames);
  261. }
  262. } else {
  263. ref = handleRefFunction(ref, doc);
  264. modelNames = [ref];
  265. }
  266. } else if ((schemaForCurrentDoc = get(schema, 'options.refPath')) != null) {
  267. isRefPath = true;
  268. if (typeof refPath === 'function') {
  269. const subdocPath = options.path.slice(0, options.path.length - schemaForCurrentDoc.path.length - 1);
  270. const vals = mpath.get(subdocPath, doc, lookupLocalFields);
  271. const subdocsBeingPopulated = Array.isArray(vals) ?
  272. utils.array.flatten(vals) :
  273. (vals ? [vals] : []);
  274. modelNames = new Set();
  275. for (const subdoc of subdocsBeingPopulated) {
  276. refPath = refPath.call(subdoc, subdoc, options.path);
  277. modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection).
  278. forEach(name => modelNames.add(name));
  279. }
  280. modelNames = Array.from(modelNames);
  281. } else {
  282. modelNames = modelNamesFromRefPath(refPath, doc, options.path, modelSchema, options._queryProjection);
  283. }
  284. }
  285. }
  286. if (!modelNames) {
  287. // `Model.populate()` on a POJO with no known local model. Default to using the `Model`
  288. if (options._localModel == null) {
  289. modelNames = [model.modelName];
  290. } else {
  291. return { modelNames: modelNames, justOne: justOne, isRefPath: isRefPath, refPath: refPath };
  292. }
  293. }
  294. if (!Array.isArray(modelNames)) {
  295. modelNames = [modelNames];
  296. }
  297. return { modelNames: modelNames, justOne: justOne, isRefPath: isRefPath, refPath: refPath };
  298. }
  299. };
  300. /*!
  301. * ignore
  302. */
  303. function _virtualPopulate(model, docs, options, _virtualRes) {
  304. const map = [];
  305. const available = {};
  306. const virtual = _virtualRes.virtual;
  307. for (const doc of docs) {
  308. let modelNames = null;
  309. const data = {};
  310. // localField and foreignField
  311. let localField;
  312. const virtualPrefix = _virtualRes.nestedSchemaPath ?
  313. _virtualRes.nestedSchemaPath + '.' : '';
  314. if (typeof virtual.options.localField === 'function') {
  315. localField = virtualPrefix + virtual.options.localField.call(doc, doc);
  316. } else if (Array.isArray(virtual.options.localField)) {
  317. localField = virtual.options.localField.map(field => virtualPrefix + field);
  318. } else {
  319. localField = virtualPrefix + virtual.options.localField;
  320. }
  321. data.count = virtual.options.count;
  322. if (virtual.options.skip != null && !options.hasOwnProperty('skip')) {
  323. options.skip = virtual.options.skip;
  324. }
  325. if (virtual.options.limit != null && !options.hasOwnProperty('limit')) {
  326. options.limit = virtual.options.limit;
  327. }
  328. if (virtual.options.perDocumentLimit != null && !options.hasOwnProperty('perDocumentLimit')) {
  329. options.perDocumentLimit = virtual.options.perDocumentLimit;
  330. }
  331. let foreignField = virtual.options.foreignField;
  332. if (!localField || !foreignField) {
  333. return new MongooseError('If you are populating a virtual, you must set the ' +
  334. 'localField and foreignField options');
  335. }
  336. if (typeof localField === 'function') {
  337. localField = localField.call(doc, doc);
  338. }
  339. if (typeof foreignField === 'function') {
  340. foreignField = foreignField.call(doc);
  341. }
  342. data.isRefPath = false;
  343. // `justOne = null` means we don't know from the schema whether the end
  344. // result should be an array or a single doc. This can result from
  345. // populating a POJO using `Model.populate()`
  346. let justOne = null;
  347. if ('justOne' in options && options.justOne !== void 0) {
  348. justOne = options.justOne;
  349. }
  350. if (virtual.options.refPath) {
  351. modelNames =
  352. modelNamesFromRefPath(virtual.options.refPath, doc, options.path);
  353. justOne = !!virtual.options.justOne;
  354. data.isRefPath = true;
  355. } else if (virtual.options.ref) {
  356. let normalizedRef;
  357. if (typeof virtual.options.ref === 'function' && !virtual.options.ref[modelSymbol]) {
  358. normalizedRef = virtual.options.ref.call(doc, doc);
  359. } else {
  360. normalizedRef = virtual.options.ref;
  361. }
  362. justOne = !!virtual.options.justOne;
  363. // When referencing nested arrays, the ref should be an Array
  364. // of modelNames.
  365. if (Array.isArray(normalizedRef)) {
  366. modelNames = normalizedRef;
  367. } else {
  368. modelNames = [normalizedRef];
  369. }
  370. }
  371. data.isVirtual = true;
  372. data.virtual = virtual;
  373. data.justOne = justOne;
  374. // `match`
  375. let match = get(options, 'match', null) ||
  376. get(data, 'virtual.options.match', null) ||
  377. get(data, 'virtual.options.options.match', null);
  378. let hasMatchFunction = typeof match === 'function';
  379. if (hasMatchFunction) {
  380. match = match.call(doc, doc);
  381. }
  382. if (Array.isArray(localField) && Array.isArray(foreignField) && localField.length === foreignField.length) {
  383. match = Object.assign({}, match);
  384. for (let i = 1; i < localField.length; ++i) {
  385. match[foreignField[i]] = convertTo_id(mpath.get(localField[i], doc, lookupLocalFields), model.schema);
  386. hasMatchFunction = true;
  387. }
  388. localField = localField[0];
  389. foreignField = foreignField[0];
  390. }
  391. data.localField = localField;
  392. data.foreignField = foreignField;
  393. data.match = match;
  394. data.hasMatchFunction = hasMatchFunction;
  395. // Get local fields
  396. const ret = _getLocalFieldValues(doc, localField, model, options, virtual);
  397. try {
  398. addModelNamesToMap(model, map, available, modelNames, options, data, ret, doc);
  399. } catch (err) {
  400. return err;
  401. }
  402. }
  403. return map;
  404. }
  405. /*!
  406. * ignore
  407. */
  408. function addModelNamesToMap(model, map, available, modelNames, options, data, ret, doc, schemaOptions, unpopulatedValue) {
  409. // `PopulateOptions#connection`: if the model is passed as a string, the
  410. // connection matters because different connections have different models.
  411. const connection = options.connection != null ? options.connection : model.db;
  412. unpopulatedValue = unpopulatedValue === void 0 ? ret : unpopulatedValue;
  413. if (Array.isArray(unpopulatedValue)) {
  414. unpopulatedValue = utils.cloneArrays(unpopulatedValue);
  415. }
  416. if (modelNames == null) {
  417. return;
  418. }
  419. let k = modelNames.length;
  420. while (k--) {
  421. const modelName = modelNames[k];
  422. if (modelName == null) {
  423. continue;
  424. }
  425. let Model;
  426. if (options.model && options.model[modelSymbol]) {
  427. Model = options.model;
  428. } else if (modelName[modelSymbol]) {
  429. Model = modelName;
  430. } else {
  431. try {
  432. Model = _getModelFromConn(connection, modelName);
  433. } catch (err) {
  434. if (ret !== void 0) {
  435. throw err;
  436. }
  437. Model = null;
  438. }
  439. }
  440. let ids = ret;
  441. const flat = Array.isArray(ret) ? utils.array.flatten(ret) : [];
  442. const modelNamesForRefPath = data.modelNamesInOrder ? data.modelNamesInOrder : modelNames;
  443. if (data.isRefPath && Array.isArray(ret) && flat.length === modelNamesForRefPath.length) {
  444. ids = flat.filter((val, i) => modelNamesForRefPath[i] === modelName);
  445. }
  446. const perDocumentLimit = options.perDocumentLimit == null ?
  447. get(options, 'options.perDocumentLimit', null) :
  448. options.perDocumentLimit;
  449. if (!available[modelName] || perDocumentLimit != null) {
  450. const currentOptions = {
  451. model: Model
  452. };
  453. if (data.isVirtual && get(data.virtual, 'options.options')) {
  454. currentOptions.options = utils.clone(data.virtual.options.options);
  455. } else if (schemaOptions != null) {
  456. currentOptions.options = Object.assign({}, schemaOptions);
  457. }
  458. utils.merge(currentOptions, options);
  459. // Used internally for checking what model was used to populate this
  460. // path.
  461. options[populateModelSymbol] = Model;
  462. available[modelName] = {
  463. model: Model,
  464. options: currentOptions,
  465. match: data.hasMatchFunction ? [data.match] : data.match,
  466. docs: [doc],
  467. ids: [ids],
  468. allIds: [ret],
  469. unpopulatedValues: [unpopulatedValue],
  470. localField: new Set([data.localField]),
  471. foreignField: new Set([data.foreignField]),
  472. justOne: data.justOne,
  473. isVirtual: data.isVirtual,
  474. virtual: data.virtual,
  475. count: data.count,
  476. [populateModelSymbol]: Model
  477. };
  478. map.push(available[modelName]);
  479. } else {
  480. available[modelName].localField.add(data.localField);
  481. available[modelName].foreignField.add(data.foreignField);
  482. available[modelName].docs.push(doc);
  483. available[modelName].ids.push(ids);
  484. available[modelName].allIds.push(ret);
  485. available[modelName].unpopulatedValues.push(unpopulatedValue);
  486. if (data.hasMatchFunction) {
  487. available[modelName].match.push(data.match);
  488. }
  489. }
  490. }
  491. }
  492. function _getModelFromConn(conn, modelName) {
  493. /* If this connection has a parent from `useDb()`, bubble up to parent's models */
  494. if (conn.models[modelName] == null && conn._parent != null) {
  495. return _getModelFromConn(conn._parent, modelName);
  496. }
  497. return conn.model(modelName);
  498. }
  499. /*!
  500. * ignore
  501. */
  502. function handleRefFunction(ref, doc) {
  503. if (typeof ref === 'function' && !ref[modelSymbol]) {
  504. return ref.call(doc, doc);
  505. }
  506. return ref;
  507. }
  508. /*!
  509. * ignore
  510. */
  511. function _getLocalFieldValues(doc, localField, model, options, virtual, schema) {
  512. // Get Local fields
  513. const localFieldPathType = model.schema._getPathType(localField);
  514. const localFieldPath = localFieldPathType === 'real' ?
  515. model.schema.path(localField) :
  516. localFieldPathType.schema;
  517. const localFieldGetters = localFieldPath && localFieldPath.getters ?
  518. localFieldPath.getters : [];
  519. localField = localFieldPath != null && localFieldPath.instance === 'Embedded' ? localField + '._id' : localField;
  520. const _populateOptions = get(options, 'options', {});
  521. const getters = 'getters' in _populateOptions ?
  522. _populateOptions.getters :
  523. get(virtual, 'options.getters', false);
  524. if (localFieldGetters.length !== 0 && getters) {
  525. const hydratedDoc = (doc.$__ != null) ? doc : model.hydrate(doc);
  526. const localFieldValue = utils.getValue(localField, doc);
  527. if (Array.isArray(localFieldValue)) {
  528. const localFieldHydratedValue = utils.getValue(localField.split('.').slice(0, -1), hydratedDoc);
  529. return localFieldValue.map((localFieldArrVal, localFieldArrIndex) =>
  530. localFieldPath.applyGetters(localFieldArrVal, localFieldHydratedValue[localFieldArrIndex]));
  531. } else {
  532. return localFieldPath.applyGetters(localFieldValue, hydratedDoc);
  533. }
  534. } else {
  535. return convertTo_id(mpath.get(localField, doc, lookupLocalFields), schema);
  536. }
  537. }
  538. /*!
  539. * Retrieve the _id of `val` if a Document or Array of Documents.
  540. *
  541. * @param {Array|Document|Any} val
  542. * @return {Array|Document|Any}
  543. */
  544. function convertTo_id(val, schema) {
  545. if (val != null && val.$__ != null) {
  546. return val._id;
  547. }
  548. if (val != null && val._id != null && (schema == null || !schema.$isSchemaMap)) {
  549. return val._id;
  550. }
  551. if (Array.isArray(val)) {
  552. const rawVal = val.__array != null ? val.__array : val;
  553. for (let i = 0; i < rawVal.length; ++i) {
  554. if (rawVal[i] != null && rawVal[i].$__ != null) {
  555. rawVal[i] = rawVal[i]._id;
  556. }
  557. }
  558. if (utils.isMongooseArray(val) && val.$schema()) {
  559. return val.$schema()._castForPopulate(val, val.$parent());
  560. }
  561. return [].concat(val);
  562. }
  563. // `populate('map')` may be an object if populating on a doc that hasn't
  564. // been hydrated yet
  565. if (getConstructorName(val) === 'Object' &&
  566. // The intent here is we should only flatten the object if we expect
  567. // to get a Map in the end. Avoid doing this for mixed types.
  568. (schema == null || schema[schemaMixedSymbol] == null)) {
  569. const ret = [];
  570. for (const key of Object.keys(val)) {
  571. ret.push(val[key]);
  572. }
  573. return ret;
  574. }
  575. // If doc has already been hydrated, e.g. `doc.populate('map')`
  576. // then `val` will already be a map
  577. if (val instanceof Map) {
  578. return Array.from(val.values());
  579. }
  580. return val;
  581. }
  582. /*!
  583. * ignore
  584. */
  585. function _findRefPathForDiscriminators(doc, modelSchema, data, options, normalizedRefPath, ret) {
  586. // Re: gh-8452. Embedded discriminators may not have `refPath`, so clear
  587. // out embedded discriminator docs that don't have a `refPath` on the
  588. // populated path.
  589. if (!data.isRefPath || normalizedRefPath == null) {
  590. return;
  591. }
  592. const pieces = normalizedRefPath.split('.');
  593. let cur = '';
  594. let modelNames = void 0;
  595. for (let i = 0; i < pieces.length; ++i) {
  596. const piece = pieces[i];
  597. cur = cur + (cur.length === 0 ? '' : '.') + piece;
  598. const schematype = modelSchema.path(cur);
  599. if (schematype != null &&
  600. schematype.$isMongooseArray &&
  601. schematype.caster.discriminators != null &&
  602. Object.keys(schematype.caster.discriminators).length !== 0) {
  603. const subdocs = utils.getValue(cur, doc);
  604. const remnant = options.path.substring(cur.length + 1);
  605. const discriminatorKey = schematype.caster.schema.options.discriminatorKey;
  606. modelNames = [];
  607. for (const subdoc of subdocs) {
  608. const discriminatorName = utils.getValue(discriminatorKey, subdoc);
  609. const discriminator = schematype.caster.discriminators[discriminatorName];
  610. const discriminatorSchema = discriminator && discriminator.schema;
  611. if (discriminatorSchema == null) {
  612. continue;
  613. }
  614. const _path = discriminatorSchema.path(remnant);
  615. if (_path == null || _path.options.refPath == null) {
  616. const docValue = utils.getValue(data.localField.substring(cur.length + 1), subdoc);
  617. ret.forEach((v, i) => {
  618. if (v === docValue) {
  619. ret[i] = SkipPopulateValue(v);
  620. }
  621. });
  622. continue;
  623. }
  624. const modelName = utils.getValue(pieces.slice(i + 1).join('.'), subdoc);
  625. modelNames.push(modelName);
  626. }
  627. }
  628. }
  629. return modelNames;
  630. }