queryHelpers.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. 'use strict';
  2. /*!
  3. * Module dependencies
  4. */
  5. const PopulateOptions = require('./options/populateOptions');
  6. const checkEmbeddedDiscriminatorKeyProjection =
  7. require('./helpers/discriminator/checkEmbeddedDiscriminatorKeyProjection');
  8. const get = require('./helpers/get');
  9. const getDiscriminatorByValue =
  10. require('./helpers/discriminator/getDiscriminatorByValue');
  11. const isDefiningProjection = require('./helpers/projection/isDefiningProjection');
  12. const clone = require('./helpers/clone');
  13. const isPathSelectedInclusive = require('./helpers/projection/isPathSelectedInclusive');
  14. /**
  15. * Prepare a set of path options for query population. This is the MongooseQuery
  16. * version
  17. *
  18. * @param {Query} query
  19. * @param {Object} options
  20. * @return {Array}
  21. */
  22. exports.preparePopulationOptionsMQ = function preparePopulationOptionsMQ(query, options) {
  23. const _populate = query._mongooseOptions.populate;
  24. const pop = Object.keys(_populate).reduce((vals, key) => vals.concat([_populate[key]]), []);
  25. // lean options should trickle through all queries
  26. if (options.lean != null) {
  27. pop
  28. .filter(p => p?.options?.lean == null)
  29. .forEach(makeLean(options.lean));
  30. }
  31. const session = query?.options?.session || null;
  32. if (session != null) {
  33. pop.forEach(path => {
  34. if (path.options == null) {
  35. path.options = { session: session };
  36. return;
  37. }
  38. if (!('session' in path.options)) {
  39. path.options.session = session;
  40. }
  41. });
  42. }
  43. const projection = query._fieldsForExec();
  44. for (let i = 0; i < pop.length; ++i) {
  45. if (pop[i] instanceof PopulateOptions) {
  46. pop[i] = new PopulateOptions({
  47. ...pop[i],
  48. _queryProjection: projection,
  49. _localModel: query.model
  50. });
  51. } else {
  52. pop[i]._queryProjection = projection;
  53. pop[i]._localModel = query.model;
  54. }
  55. }
  56. return pop;
  57. };
  58. /**
  59. * If the document is a mapped discriminator type, it returns a model instance for that type, otherwise,
  60. * it returns an instance of the given model.
  61. *
  62. * @param {Model} model
  63. * @param {Object} doc
  64. * @param {Object} fields
  65. *
  66. * @return {Document}
  67. */
  68. exports.createModel = function createModel(model, doc, fields, userProvidedFields, options) {
  69. model.hooks.execPreSync('createModel', doc);
  70. const discriminatorMapping = model.schema ?
  71. model.schema.discriminatorMapping :
  72. null;
  73. const key = discriminatorMapping?.isRoot ?
  74. discriminatorMapping.key :
  75. null;
  76. const value = doc[key];
  77. if (key && value && model.discriminators) {
  78. const discriminator = model.discriminators[value] || getDiscriminatorByValue(model.discriminators, value);
  79. if (discriminator) {
  80. const _fields = clone(userProvidedFields);
  81. exports.applyPaths(_fields, discriminator.schema);
  82. return new discriminator(undefined, _fields, { skipId: true });
  83. }
  84. }
  85. const _opts = {
  86. skipId: true,
  87. isNew: false,
  88. willInit: true
  89. };
  90. if (options != null && 'defaults' in options) {
  91. _opts.defaults = options.defaults;
  92. }
  93. return new model(undefined, fields, _opts);
  94. };
  95. /*!
  96. * ignore
  97. */
  98. exports.createModelAndInit = function createModelAndInit(model, doc, fields, userProvidedFields, options, populatedIds, callback) {
  99. const initOpts = populatedIds ?
  100. { populated: populatedIds } :
  101. undefined;
  102. const casted = exports.createModel(model, doc, fields, userProvidedFields, options);
  103. try {
  104. casted.$init(doc, initOpts, callback);
  105. } catch (error) {
  106. callback(error, casted);
  107. }
  108. };
  109. /*!
  110. * ignore
  111. */
  112. exports.applyPaths = function applyPaths(fields, schema, sanitizeProjection) {
  113. // determine if query is selecting or excluding fields
  114. let exclude;
  115. let keys;
  116. const minusPathsToSkip = new Set();
  117. if (fields) {
  118. keys = Object.keys(fields);
  119. // Collapse minus paths
  120. const minusPaths = [];
  121. for (let i = 0; i < keys.length; ++i) {
  122. const key = keys[i];
  123. if (keys[i][0] !== '-') {
  124. continue;
  125. }
  126. delete fields[key];
  127. if (key === '-_id') {
  128. fields['_id'] = 0;
  129. } else {
  130. minusPaths.push(key.slice(1));
  131. }
  132. }
  133. keys = Object.keys(fields);
  134. for (let keyIndex = 0; keyIndex < keys.length; ++keyIndex) {
  135. if (keys[keyIndex][0] === '+') {
  136. continue;
  137. }
  138. const field = fields[keys[keyIndex]];
  139. // Skip `$meta` and `$slice`
  140. if (!isDefiningProjection(field)) {
  141. continue;
  142. }
  143. if (keys[keyIndex] === '_id' && keys.length > 1) {
  144. continue;
  145. }
  146. if (keys[keyIndex] === schema.options.discriminatorKey && keys.length > 1 && field != null && !field) {
  147. continue;
  148. }
  149. exclude = !field;
  150. break;
  151. }
  152. // Potentially add back minus paths based on schema-level path config
  153. // and whether the projection is inclusive
  154. for (const path of minusPaths) {
  155. const type = schema.path(path);
  156. // If the path isn't selected by default or the projection is not
  157. // inclusive, minus path is treated as equivalent to `key: 0`.
  158. // But we also allow using `-name` to remove `name` from an inclusive
  159. // projection if `name` has schema-level `select: true`.
  160. if (!type?.selected || exclude !== false) {
  161. fields[path] = 0;
  162. exclude = true;
  163. } else if (type?.selected && exclude === false) {
  164. // Make a note of minus paths that are overwriting paths that are
  165. // included by default.
  166. minusPathsToSkip.add(path);
  167. }
  168. }
  169. }
  170. // if selecting, apply default schematype select:true fields
  171. // if excluding, apply schematype select:false fields
  172. const selected = [];
  173. const excluded = [];
  174. const stack = [];
  175. analyzeSchema(schema);
  176. switch (exclude) {
  177. case true:
  178. for (const fieldName of excluded) {
  179. fields[fieldName] = 0;
  180. }
  181. break;
  182. case false:
  183. if (schema?.paths['_id']?.options?.select === false) {
  184. fields._id = 0;
  185. }
  186. for (const fieldName of selected) {
  187. if (minusPathsToSkip.has(fieldName)) {
  188. continue;
  189. }
  190. if (isPathSelectedInclusive(fields, fieldName)) {
  191. continue;
  192. }
  193. fields[fieldName] = fields[fieldName] || 1;
  194. }
  195. break;
  196. case undefined:
  197. if (fields == null) {
  198. break;
  199. }
  200. // Any leftover plus paths must in the schema, so delete them (gh-7017)
  201. for (const key of Object.keys(fields || {})) {
  202. if (key.startsWith('+')) {
  203. delete fields[key];
  204. }
  205. }
  206. // user didn't specify fields, implies returning all fields.
  207. // only need to apply excluded fields and delete any plus paths
  208. for (const fieldName of excluded) {
  209. if (fields[fieldName] != null) {
  210. // Skip applying default projections to fields with non-defining
  211. // projections, like `$slice`
  212. continue;
  213. }
  214. fields[fieldName] = 0;
  215. }
  216. break;
  217. }
  218. function analyzeSchema(schema, prefix) {
  219. prefix || (prefix = '');
  220. // avoid recursion
  221. if (stack.indexOf(schema) !== -1) {
  222. return [];
  223. }
  224. stack.push(schema);
  225. const addedPaths = [];
  226. schema.eachPath(function(path, type) {
  227. if (prefix) path = prefix + '.' + path;
  228. if (type.$isSchemaMap || path.endsWith('.$*')) {
  229. const plusPath = '+' + path;
  230. const hasPlusPath = fields && plusPath in fields;
  231. if (type.options?.select === false && !hasPlusPath) {
  232. excluded.push(path);
  233. }
  234. return;
  235. }
  236. let addedPath = analyzePath(path, type);
  237. // arrays
  238. if (addedPath == null && !Array.isArray(type) && type.$isMongooseArray && !type.$isMongooseDocumentArray) {
  239. addedPath = analyzePath(path, type.embeddedSchemaType);
  240. }
  241. if (addedPath != null) {
  242. addedPaths.push(addedPath);
  243. }
  244. // nested schemas
  245. if (type.schema) {
  246. const _addedPaths = analyzeSchema(type.schema, path);
  247. // Special case: if discriminator key is the only field that would
  248. // be projected in, remove it.
  249. if (exclude === false) {
  250. checkEmbeddedDiscriminatorKeyProjection(fields, path, type.schema,
  251. selected, _addedPaths);
  252. }
  253. }
  254. });
  255. stack.pop();
  256. return addedPaths;
  257. }
  258. function analyzePath(path, type) {
  259. if (fields == null) {
  260. return;
  261. }
  262. // If schema-level selected not set, nothing to do
  263. if (typeof type.selected !== 'boolean') {
  264. return;
  265. }
  266. // User overwriting default exclusion
  267. if (type.selected === false && fields[path]) {
  268. if (sanitizeProjection) {
  269. fields[path] = 0;
  270. }
  271. return;
  272. }
  273. // If set to 0, we're explicitly excluding the discriminator key. Can't do this for all fields,
  274. // because we have tests that assert that using `-path` to exclude schema-level `select: true`
  275. // fields counts as an exclusive projection. See gh-11546
  276. if (!exclude && type.selected && path === schema.options.discriminatorKey && fields[path] != null && !fields[path]) {
  277. delete fields[path];
  278. return;
  279. }
  280. if (exclude === false && type.selected && fields[path] != null && !fields[path]) {
  281. delete fields[path];
  282. return;
  283. }
  284. const plusPath = '+' + path;
  285. const hasPlusPath = fields && plusPath in fields;
  286. if (hasPlusPath) {
  287. // forced inclusion
  288. delete fields[plusPath];
  289. // if there are other fields being included, add this one
  290. // if no other included fields, leave this out (implied inclusion)
  291. if (exclude === false && keys.length > 1 && !~keys.indexOf(path) && !sanitizeProjection) {
  292. fields[path] = 1;
  293. } else if (exclude == null && sanitizeProjection && type.selected === false) {
  294. fields[path] = 0;
  295. }
  296. return;
  297. }
  298. // check for parent exclusions
  299. const pieces = path.split('.');
  300. let cur = '';
  301. for (let i = 0; i < pieces.length; ++i) {
  302. cur += cur.length ? '.' + pieces[i] : pieces[i];
  303. if (excluded.indexOf(cur) !== -1) {
  304. return;
  305. }
  306. }
  307. // Special case: if user has included a parent path of a discriminator key,
  308. // don't explicitly project in the discriminator key because that will
  309. // project out everything else under the parent path
  310. if (!exclude && (type?.options?.$skipDiscriminatorCheck || false)) {
  311. let cur = '';
  312. for (let i = 0; i < pieces.length; ++i) {
  313. cur += (cur.length === 0 ? '' : '.') + pieces[i];
  314. const projection = get(fields, cur, false) || get(fields, cur + '.$', false);
  315. if (projection && typeof projection !== 'object') {
  316. return;
  317. }
  318. }
  319. }
  320. (type.selected ? selected : excluded).push(path);
  321. return path;
  322. }
  323. };
  324. /**
  325. * Set each path query option to lean
  326. *
  327. * @param {Object} option
  328. */
  329. function makeLean(val) {
  330. return function(option) {
  331. option.options || (option.options = {});
  332. if (Array.isArray(val?.virtuals)) {
  333. val = Object.assign({}, val);
  334. val.virtuals = val.virtuals.
  335. filter(path => typeof path === 'string' && path.startsWith(option.path + '.')).
  336. map(path => path.slice(option.path.length + 1));
  337. }
  338. option.options.lean = val;
  339. };
  340. }