monastery 1.34.0 → 1.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/model-crud.js CHANGED
@@ -6,33 +6,31 @@ module.exports = {
6
6
  /**
7
7
  * Inserts document(s) with monk after validating data & before hooks.
8
8
  * @param {object} opts
9
- * @param {object|array} <opts.data> - documents to insert
10
- * @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove all blacklisting
9
+ * @param {object|array} opts.data - documents to insert
10
+ * @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove blacklisting
11
+ * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
11
12
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
12
- * @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
13
- * default, but false on update
14
13
  * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
15
14
  * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
16
- * @param {any} <opts.any> - any mongodb option
15
+ * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
16
+ * default, but false on update
17
+ * @param {any} <any mongodb option>
17
18
  * @param {function} <cb> - execute cb(err, data) instead of responding
18
- * @this model
19
19
  * @return promise
20
+ * @this model
20
21
  */
21
22
  if (cb && !util.isFunction(cb)) {
22
23
  throw new Error(`The callback passed to ${this.name}.insert() is not a function`)
23
24
  }
24
25
  try {
25
26
  opts = await this._queryObject(opts, 'insert')
26
- let options = util.omit(opts, [
27
- 'data', 'insert', 'model', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'
28
- ])
29
27
 
30
28
  // Validate
31
- let data = await this.validate(opts.data||{}, { ...opts })
29
+ let data = await this.validate(opts.data || {}, opts) // was { ...opts }
32
30
 
33
31
  // Insert
34
32
  await util.runSeries(this.beforeInsert.map(f => f.bind(opts, data)))
35
- let response = await this._insert(data, options)
33
+ let response = await this._insert(data, util.omit(opts, this._queryOptions))
36
34
  await util.runSeries(this.afterInsert.map(f => f.bind(opts, response)))
37
35
 
38
36
  // Success/error
@@ -51,53 +49,41 @@ module.exports = {
51
49
  /**
52
50
  * Finds document(s) with monk, also auto populates
53
51
  * @param {object} opts
54
- * @param {object} <opts.query> - mongodb query object
55
- * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
56
- * @param {array} <opts.populate> - population, see docs
57
52
  * @param {array|string|false} <opts.blacklist> - augment schema.findBL, `false` will remove all blacklisting
53
+ * @param {array} <opts.populate> - population, see docs
58
54
  * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
59
- * @param {any} <opts.any> - any mongodb option
55
+ * @param {object} <opts.query> - mongodb query object
56
+ * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
57
+ * @param {any} <any mongodb option>
60
58
  * @param {function} <cb> - execute cb(err, data) instead of responding
61
- * @this model
62
59
  * @return promise
60
+ * @this model
63
61
  */
64
62
  if (cb && !util.isFunction(cb)) {
65
63
  throw new Error(`The callback passed to ${this.name}.find() is not a function`)
66
64
  }
67
65
  try {
68
- opts = await this._queryObject(opts, 'find', one)
69
66
  let lookups = []
70
- let options = util.omit(opts, ['blacklist', 'one', 'populate', 'project', 'query', 'respond'])
71
- options.addFields = options.addFields || {}
67
+ opts = await this._queryObject(opts, 'find', one)
68
+
69
+ // Get projection
70
+ if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
71
+ else opts.projection = this._getProjectionFromBlacklist(opts.type, opts.blacklist)
72
72
 
73
- // Using project (can be an inclusion/exclusion)
74
- if (opts.project) {
75
- if (util.isString(opts.project)) {
76
- opts.project = opts.project.trim().split(/\s+/)
77
- }
78
- if (util.isArray(opts.project)) {
79
- options.projection = opts.project.reduce((o, v) => {
80
- o[v.replace(/^-/, '')] = v.match(/^-/)? 0 : 1
81
- return o
82
- }, {})
83
- }
84
- // Or blacklisting
85
- } else {
86
- options.projection = this._getBlacklistProjection('find', opts.blacklist)
87
- }
88
73
  // Has text search?
89
74
  // if (opts.query.$text) {
90
- // options.projection.score = { $meta: 'textScore' }
91
- // options.sort = { score: { $meta: 'textScore' }}
75
+ // opts.projection.score = { $meta: 'textScore' }
76
+ // opts.sort = { score: { $meta: 'textScore' }}
92
77
  // }
78
+
93
79
  // Wanting to populate?
94
80
  if (!opts.populate) {
95
- var response = await this[`_find${opts.one? 'One' : ''}`](opts.query, options)
81
+ var response = await this[`_find${opts.one? 'One' : ''}`](opts.query, util.omit(opts, this._queryOptions))
96
82
  } else {
97
83
  loop: for (let item of opts.populate) {
98
84
  let path = util.isObject(item)? item.as : item
99
85
  // Blacklisted?
100
- if (!this._pathInProjection(path, options.projection, true)) continue loop
86
+ if (this._pathBlacklisted(path, opts.projection)) continue loop
101
87
  // Custom $lookup definition
102
88
  // https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-lookup-expr
103
89
  if (util.isObject(item)) {
@@ -118,7 +104,7 @@ module.exports = {
118
104
  continue
119
105
  }
120
106
  // Populate model (convert array into document & create lookup)
121
- options.addFields[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
107
+ (opts.addFields = opts.addFields || {})[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
122
108
  lookups.push({ $lookup: {
123
109
  from: modelName,
124
110
  localField: path,
@@ -127,23 +113,26 @@ module.exports = {
127
113
  }})
128
114
  }
129
115
  }
130
- // console.log(1, options.projection)
116
+ // console.log(1, opts.projection)
131
117
  // console.log(2, lookups)
132
118
  let aggregate = [
133
119
  { $match: opts.query },
134
- { $sort: options.sort },
135
- { $skip: options.skip },
136
- ...(options.limit? [{ $limit: options.limit }] : []),
120
+ { $sort: opts.sort },
121
+ { $skip: opts.skip },
122
+ ...(opts.limit? [{ $limit: opts.limit }] : []),
137
123
  ...lookups,
138
- ...util.isEmpty(options.addFields)? [] : [{ $addFields: options.addFields }],
139
- { $project: options.projection }
124
+ ...(opts.addFields? [{ $addFields: opts.addFields }] : []),
125
+ ...(opts.projection? [{ $project: opts.projection }] : []),
140
126
  ]
141
127
  response = await this._aggregate(aggregate)
142
128
  this.info('aggregate', JSON.stringify(aggregate))
143
129
  }
144
130
 
131
+ // Returning one?
145
132
  if (opts.one && util.isArray(response)) response = response[0]
146
- response = await this._processAfterFind(response, options.projection, opts)
133
+
134
+ // Process afterFind hooks
135
+ response = await this._processAfterFind(response, opts.projection, opts)
147
136
 
148
137
  // Success
149
138
  if (cb) cb(null, response)
@@ -161,59 +150,120 @@ module.exports = {
161
150
  return this.find(opts, cb, true)
162
151
  },
163
152
 
164
- // findOneAndUpdate: function(opts, cb) {
165
- // return this._findOneAndUpdate(opts, cb)
166
- // },
153
+ findOneAndUpdate: async function(opts, cb) {
154
+ /**
155
+ * Find and update document(s) with monk, also auto populates
156
+ * @param {object} opts
157
+ * @param {array|string|false} <opts.blacklist> - augment findBL/updateBL, `false` will remove all blacklisting
158
+ * @param {array} <opts.populate> - find population, see docs
159
+ * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
160
+ * @param {object} <opts.query> - mongodb query object
161
+ * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
162
+ * @param {any} <any mongodb option>
163
+ *
164
+ * Update options:
165
+ * @param {object|array} opts.data - mongodb document update object(s)
166
+ * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
167
+ * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
168
+ * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
169
+ * default, but false on update
170
+ * @param {function} <cb> - execute cb(err, data) instead of responding
171
+ * @return promise
172
+ * @this model
173
+ */
174
+
175
+ if (opts.populate) {
176
+ try {
177
+ // todo: add transaction flag
178
+ delete opts.multi
179
+ let update = await this.update(opts, null, 'findOneAndUpdate')
180
+ if (update) var response = await this.findOne(opts)
181
+ else response = update
182
+
183
+ // Success
184
+ if (cb) cb(null, response)
185
+ else if (opts.req && opts.respond) opts.req.res.json(response)
186
+ else return Promise.resolve(response)
187
+
188
+ } catch (e) {
189
+ if (cb) cb(e)
190
+ else if (opts && opts.req && opts.respond) opts.req.res.error(e)
191
+ else throw e
192
+ }
193
+ } else {
194
+ return this.update(opts, cb, 'findOneAndUpdate')
195
+ }
196
+ },
167
197
 
168
- update: async function(opts, cb) {
198
+ update: async function(opts, cb, type='update') {
169
199
  /**
170
200
  * Updates document(s) with monk after validating data & before hooks.
171
201
  * @param {object} opts
202
+ * @param {object|array} opts.data - mongodb document update object(s)
203
+ * @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove blacklisting
204
+ * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
172
205
  * @param {object} <opts.query> - mongodb query object
173
- * @param {object|array} <opts.data> - mongodb document update object(s)
174
206
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
175
- * @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
176
- * default, but false on update
177
207
  * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
178
208
  * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
179
- * @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove all blacklisting
180
- * @param {any} <opts.any> - any mongodb option
209
+ * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
210
+ * default, but false on update
211
+ * @param {any} <any mongodb option>
181
212
  * @param {function} <cb> - execute cb(err, data) instead of responding
182
- * @this model
213
+ * @param {function} <type> - 'update', or 'findOneAndUpdate'
183
214
  * @return promise(data)
215
+ * @this model
184
216
  */
185
217
  if (cb && !util.isFunction(cb)) {
186
- throw new Error(`The callback passed to ${this.name}.update() is not a function`)
218
+ throw new Error(`The callback passed to ${this.name}.${type}() is not a function`)
187
219
  }
188
220
  try {
189
- opts = await this._queryObject(opts, 'update')
221
+ opts = await this._queryObject(opts, type)
190
222
  let data = opts.data
191
223
  let response = null
192
- let operators = util.pluck(opts, [/^\$/])
193
- let options = util.omit(opts, ['data', 'query', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'])
224
+ let operators = util.pick(opts, [/^\$/])
194
225
 
195
226
  // Validate
196
- if (util.isDefined(data)) data = await this.validate(opts.data, { ...opts })
227
+ if (util.isDefined(data)) {
228
+ data = await this.validate(opts.data, opts) // was {...opts}
229
+ }
197
230
  if (!util.isDefined(data) && util.isEmpty(operators)) {
198
- throw new Error(`Please pass an update operator to ${this.name}.update(), e.g. data, $unset, etc`)
231
+ throw new Error(`Please pass an update operator to ${this.name}.${type}(), e.g. data, $unset, etc`)
199
232
  }
200
233
  if (util.isDefined(data) && (!data || util.isEmpty(data))) {
201
- throw new Error(`No valid data passed to ${this.name}.update({ data: .. })`)
234
+ throw new Error(`No valid data passed to ${this.name}.${type}({ data: .. })`)
202
235
  }
236
+
203
237
  // Hook: beforeUpdate (has access to original, non-validated opts.data)
204
238
  await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, data||{})))
239
+
205
240
  if (data && operators['$set']) {
206
- this.warn(`'$set' fields take precedence over the data fields for \`${this.name}.update()\``)
241
+ this.warn(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
207
242
  }
208
243
  if (data || operators['$set']) {
209
244
  operators['$set'] = { ...data, ...(operators['$set'] || {}) }
210
245
  }
246
+
247
+ // findOneAndUpdate, get 'find' projection
248
+ if (type == 'findOneAndUpdate') {
249
+ if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
250
+ else opts.projection = this._getProjectionFromBlacklist('find', opts.blacklist)
251
+ // Just peform a normal update if we need to populate a findOneAndUpdate
252
+ if (opts.populate) type = 'update'
253
+ }
254
+
211
255
  // Update
212
- let update = await this._update(opts.query, operators, options)
213
- if (update.n) response = Object.assign(Object.create({ _output: update }), operators['$set']||{})
256
+ let update = await this['_' + type](opts.query, operators, util.omit(opts, this._queryOptions))
257
+ if (type == 'findOneAndUpdate') response = update
258
+ else if (update.n) response = Object.assign(Object.create({ _output: update }), operators['$set']||{})
214
259
 
215
260
  // Hook: afterUpdate (doesn't have access to validated data)
216
- if (update.n) await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)))
261
+ if (response) await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)))
262
+
263
+ // Hook: afterFind if findOneAndUpdate
264
+ if (response && type == 'findOneAndUpdate') {
265
+ response = await this._processAfterFind(response, opts.projection, opts)
266
+ }
217
267
 
218
268
  // Success
219
269
  if (cb) cb(null, response)
@@ -234,22 +284,21 @@ module.exports = {
234
284
  * @param {object} <opts.query> - mongodb query object
235
285
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
236
286
  * @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
237
- * @param {any} <opts.any> - any mongodb option
287
+ * @param {any} <any mongodb option>
238
288
  * @param {function} <cb> - execute cb(err, data) instead of responding
239
- * @this model
240
289
  * @return promise
290
+ * @this model
241
291
  */
242
292
  if (cb && !util.isFunction(cb)) {
243
293
  throw new Error(`The callback passed to ${this.name}.remove() is not a function`)
244
294
  }
245
295
  try {
246
296
  opts = await this._queryObject(opts, 'remove')
247
- let options = util.omit(opts, ['query', 'respond'])
248
297
  if (util.isEmpty(opts.query)) throw new Error('Please specify opts.query')
249
298
 
250
299
  // Remove
251
300
  await util.runSeries(this.beforeRemove.map(f => f.bind(opts)))
252
- let response = await this._remove(opts.query, options)
301
+ let response = await this._remove(opts.query, util.omit(opts, this._queryOptions))
253
302
  await util.runSeries(this.afterRemove.map(f => f.bind(response)))
254
303
 
255
304
  // Success
@@ -264,13 +313,16 @@ module.exports = {
264
313
  }
265
314
  },
266
315
 
267
- _getBlacklistProjection: function(type, customBlacklist) {
316
+ _getProjectionFromBlacklist: function(type, customBlacklist) {
268
317
  /**
269
318
  * Returns an exclusion projection
270
319
  *
320
+ * Path collisions are removed
321
+ * E.g. ['pets.dogs', 'pets.dogs.name', '-cat', 'pets.dogs.age'] = { 'pets.dog': 0 }
322
+ *
271
323
  * @param {string} type - find, insert, or update
272
- * @param {array} customBlacklist - normally passed through options
273
- * @return {array} exclusion projection {'pets.name': 0}
324
+ * @param {array|string|false} customBlacklist - normally passed through options
325
+ * @return {array|undefined} exclusion $project {'pets.name': 0}
274
326
  * @this model
275
327
  *
276
328
  * 1. collate deep-blacklists
@@ -281,16 +333,24 @@ module.exports = {
281
333
  let list = []
282
334
  let manager = this.manager
283
335
  let projection = {}
336
+ if (customBlacklist === false) return
337
+
338
+ // String?
339
+ if (typeof customBlacklist === 'string') {
340
+ customBlacklist = customBlacklist.trim().split(/\s+/)
341
+ }
284
342
 
285
343
  // Concat deep blacklists
286
- util.forEach(this.fieldsFlattened, (schema, path) => {
287
- if (!schema.model) return
288
- let deepBL = manager.model[schema.model][`${type}BL`] || []
289
- let pathWithoutArrays = path.replace(/\.0(\.|$)/, '$1')
290
- list = list.concat(deepBL.map(o => {
291
- return `${o.charAt(0) == '-'? '-' : ''}${pathWithoutArrays}.${o.replace(/^-/, '')}`
292
- }))
293
- })
344
+ if (type == 'find') {
345
+ util.forEach(this.fieldsFlattened, (schema, path) => {
346
+ if (!schema.model) return
347
+ let deepBL = manager.model[schema.model][`${type}BL`] || []
348
+ let pathWithoutArrays = path.replace(/\.0(\.|$)/, '$1')
349
+ list = list.concat(deepBL.map(o => {
350
+ return `${o.charAt(0) == '-'? '-' : ''}${pathWithoutArrays}.${o.replace(/^-/, '')}`
351
+ }))
352
+ })
353
+ }
294
354
 
295
355
  // Concat model, and custom blacklists
296
356
  list = list.concat([...this[`${type}BL`]]).concat(customBlacklist || [])
@@ -302,7 +362,8 @@ module.exports = {
302
362
 
303
363
  // Remove any child fields. E.g remove { user.token: 0 } = key2 if iterating { user: 0 } = key
304
364
  for (let key2 in projection) {
305
- if (key2.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) { // test that scoped to the \.
365
+ // todo: need to write a test, testing that this is scoped to \.
366
+ if (key2.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) {
306
367
  delete projection[key2]
307
368
  }
308
369
  }
@@ -335,6 +396,28 @@ module.exports = {
335
396
  if (projection[key]) delete projection[key]
336
397
  }
337
398
 
399
+ return util.isEmpty(projection) ? undefined : projection
400
+ },
401
+
402
+ _getProjectionFromProject: function(customProject) {
403
+ /**
404
+ * Returns an in/exclusion projection
405
+ * todo: tests
406
+ *
407
+ * @param {object|array|string} customProject - normally passed through options
408
+ * @return {array|undefined} in/exclusion projection {'pets.name': 0}
409
+ * @this model
410
+ */
411
+ let projection
412
+ if (util.isString(customProject)) {
413
+ customProject = customProject.trim().split(/\s+/)
414
+ }
415
+ if (util.isArray(customProject)) {
416
+ projection = customProject.reduce((o, v) => {
417
+ o[v.replace(/^-/, '')] = v.match(/^-/)? 0 : 1
418
+ return o
419
+ }, {})
420
+ }
338
421
  return projection
339
422
  },
340
423
 
@@ -342,11 +425,12 @@ module.exports = {
342
425
  /**
343
426
  * Normalise options
344
427
  * @param {MongoId|string|object} opts
345
- * @param {string} type - operation type
428
+ * @param {string} type - insert, update, find, remove, findOneAndUpdate
346
429
  * @param {boolean} one - return one document
347
- * @this model
348
430
  * @return {Promise} opts
431
+ * @this model
349
432
  *
433
+ * Query parsing logic:
350
434
  * opts == string|MongodId - treated as an id
351
435
  * opts == undefined|null|false - throw error
352
436
  * opts.query == string|MongodID - treated as an id
@@ -370,7 +454,7 @@ module.exports = {
370
454
  throw new Error('Please pass an object or MongoId to options.query')
371
455
  }
372
456
  if (util.isId(opts.query._id)) opts.query._id = this.manager.id(opts.query._id)
373
- if (isIdType(opts.query._id) || one) opts.one = true
457
+ if (isIdType(opts.query._id) || one || type == 'findOneAndUpdate') opts.one = true
374
458
  opts.query = util.removeUndefined(opts.query)
375
459
 
376
460
  // Query options
@@ -389,7 +473,8 @@ module.exports = {
389
473
  if (!util.isDefined(opts.data) && util.isDefined((opts.req||{}).body)) opts.data = opts.req.body
390
474
  if (util.isDefined(opts.data)) opts.data = await util.parseData(opts.data)
391
475
 
392
- opts[type] = true
476
+ opts.type = type
477
+ opts[type] = true // still being included in the operation options..
393
478
  opts.model = this
394
479
  return opts
395
480
  },
@@ -400,11 +485,12 @@ module.exports = {
400
485
  * Recurses through fields that are models and populates missing default-fields and calls model.afterFind([fn,..])
401
486
  * Be sure to add any virtual fields to the schema that your populating on,
402
487
  * e.g. "nurses": [{ model: 'user' }]
488
+ *
403
489
  * @param {object|array|null} data
404
490
  * @param {object} projection - $project object
405
491
  * @param {object} afterFindContext - handy context object given to schema.afterFind
406
- * @this model
407
492
  * @return Promise(data)
493
+ * @this model
408
494
  */
409
495
  // Recurse down from the parent model, ending with the parent model as the parent afterFind hook may
410
496
  // want to manipulate any populated models
@@ -422,8 +508,8 @@ module.exports = {
422
508
  if (!util.isDefined(schema.default) || path.match(/^\.?(createdAt|updatedAt)$/)) return
423
509
  let parentPath = item.fieldName? item.fieldName + '.' : ''
424
510
  let pathWithoutArrays = (parentPath + path).replace(/\.0(\.|$)/, '$1')
425
- // Ignore default fields that are excluded in a blacklist/parent-blacklist
426
- if (!this._pathInProjection(pathWithoutArrays, projection, true)) return
511
+ // Ignore default fields that are blacklisted
512
+ if (this._pathBlacklisted(pathWithoutArrays, projection)) return
427
513
  // console.log(pathWithoutArrays, path, projection)
428
514
  // Set value
429
515
  let value = util.isFunction(schema.default)? schema.default(this.manager) : schema.default
@@ -438,39 +524,34 @@ module.exports = {
438
524
  return util.runSeries(callbackSeries).then(() => data)
439
525
  },
440
526
 
441
- _pathInProjection: function(path, projection, matchParentPaths) {
527
+ _pathBlacklisted: function(path, projection, matchDeepWhitelistedKeys=true) {
442
528
  /**
443
- * Checks if the path is valid within a inclusion/exclusion projection
529
+ * Checks if the path is blacklisted within a inclusion/exclusion projection
444
530
  * @param {string} path - path without array brackets e.g. '.[]'
445
- * @param {object} projection
446
- * @param {boolean} matchParentPaths - match paths included in a key (inclusive only)
531
+ * @param {object} projection - inclusion/exclusion projection, not mixed
532
+ * @param {boolean} matchDeepWhitelistedKeys - match deep whitelisted keys containing path
533
+ * E.g. pets.color == pets.color.age
447
534
  * @return {boolean}
448
535
  */
449
- let inc
450
- // console.log(path, projection)
451
536
  for (let key in projection) {
452
- if (projection[key] && matchParentPaths) {
453
- // Inclusion
454
- // E.g. 'pets.color.age'.match(/^pets.color.age(.|$)/) = match
455
- // E.g. 'pets.color.age'.match(/^pets.color(.|$)/) = match
456
- // E.g. 'pets.color'.match(/^pets.color.age(.|$)/) = match
457
- inc = true
458
- if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '(\\.|$)'))) return true
459
- if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
460
- } else if (projection[key]) {
461
- // Inclusion (equal to key, or key included in path)
462
- // E.g. 'pets.color.age'.match(/^pets.color.age(.|$)/) = match
463
- // E.g. 'pets.color.age'.match(/^pets.color(.|$)/) = match
464
- // E.g. 'pets.color'.match(/^pets.color.age(.|$)/) = no match
465
- inc = true
466
- if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
467
- } else {
468
- // Exclusion
469
- // E.g. 'pets.color.age'.match(/^pets.color(.|$)/) = match
537
+ if (projection[key]) {
538
+ // Inclusion (whitelisted)
539
+ // E.g. pets.color.age == pets.color.age (exact match)
540
+ // E.g. pets.color.age == pets.color (path contains key)
541
+ var inclusion = true
470
542
  if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return false
543
+ if (matchDeepWhitelistedKeys) {
544
+ // E.g. pets.color == pets.color.age (key contains path)
545
+ if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '\\.'))) return false
546
+ }
547
+ } else {
548
+ // Exclusion (blacklisted)
549
+ // E.g. pets.color.age == pets.color.age (exact match)
550
+ // E.g. pets.color.age == pets.color (path contains key)
551
+ if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
471
552
  }
472
553
  }
473
- return inc? false : true
554
+ return inclusion? true : false
474
555
  },
475
556
 
476
557
  _recurseAndFindModels: function(fields, dataArr) {
@@ -550,4 +631,11 @@ module.exports = {
550
631
  }, this)
551
632
  },
552
633
 
634
+ _queryOptions: [
635
+ // todo: remove type properties
636
+ 'blacklist', 'data', 'find', 'findOneAndUpdate', 'insert', 'model', 'one', 'populate', 'project',
637
+ 'projectionValidate', 'query', 'remove', 'req', 'respond', 'skipValidation', 'type', 'update',
638
+ 'validateUndefined',
639
+ ],
640
+
553
641
  }