UNPKG

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