monastery 1.33.0 → 1.36.0

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