monastery 1.32.5 → 1.35.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,56 +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', '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
-
73
- // Project, or use blacklisting
74
- if (opts.project) {
75
- // Can be an inclusion or exclusion projection
76
- if (util.isString(opts.project)) {
77
- opts.project = opts.project.trim().split(/\s+/)
78
- }
79
- if (util.isArray(opts.project)) {
80
- options.projection = opts.project.reduce((o, v) => {
81
- o[v.replace(/^-/, '')] = v.match(/^-/)? 0 : 1
82
- return o
83
- }, {})
84
- }
85
- } else {
86
- // Calculate the exclusion-projection
87
- let blacklistProjection = { ...this.findBLProject }
88
- blacklistProjection = this._addDeepBlacklists(blacklistProjection, opts.populate)
89
- options.projection = this._addBlacklist(blacklistProjection, opts.blacklist)
90
- }
70
+
71
+ // Get projection
72
+ if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
73
+ else opts.projection = this._getProjectionFromBlacklist(opts.type, opts.blacklist)
74
+
91
75
  // Has text search?
92
76
  // if (opts.query.$text) {
93
- // options.projection.score = { $meta: 'textScore' }
94
- // options.sort = { score: { $meta: 'textScore' }}
77
+ // opts.projection.score = { $meta: 'textScore' }
78
+ // opts.sort = { score: { $meta: 'textScore' }}
95
79
  // }
80
+
96
81
  // Wanting to populate?
97
82
  if (!opts.populate) {
98
- 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))
99
84
  } else {
100
85
  loop: for (let item of opts.populate) {
101
86
  let path = util.isObject(item)? item.as : item
102
87
  // Blacklisted?
103
- if (!this._pathInProjection(path, options.projection, true)) continue loop
88
+ if (this._pathBlacklisted(path, opts.projection)) continue loop
104
89
  // Custom $lookup definition
105
90
  // https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-lookup-expr
106
91
  if (util.isObject(item)) {
@@ -121,7 +106,7 @@ module.exports = {
121
106
  continue
122
107
  }
123
108
  // Populate model (convert array into document & create lookup)
124
- options.addFields[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
109
+ (opts.addFields = opts.addFields || {})[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
125
110
  lookups.push({ $lookup: {
126
111
  from: modelName,
127
112
  localField: path,
@@ -130,26 +115,26 @@ module.exports = {
130
115
  }})
131
116
  }
132
117
  }
133
- // console.log(1, options.projection)
118
+ // console.log(1, opts.projection)
134
119
  // console.log(2, lookups)
135
120
  let aggregate = [
136
121
  { $match: opts.query },
137
- { $sort: options.sort },
138
- { $skip: options.skip },
139
- ...(options.limit? [{ $limit: options.limit }] : []),
122
+ { $sort: opts.sort },
123
+ { $skip: opts.skip },
124
+ ...(opts.limit? [{ $limit: opts.limit }] : []),
140
125
  ...lookups,
141
- ...util.isEmpty(options.addFields)? [] : [{ $addFields: options.addFields }],
142
- { $project: options.projection }
126
+ ...(opts.addFields? [{ $addFields: opts.addFields }] : []),
127
+ ...(opts.projection? [{ $project: opts.projection }] : []),
143
128
  ]
144
129
  response = await this._aggregate(aggregate)
145
130
  this.info('aggregate', JSON.stringify(aggregate))
146
131
  }
147
132
 
133
+ // Returning one?
148
134
  if (opts.one && util.isArray(response)) response = response[0]
149
- // (Not using) Project works with lookup.
150
- // Remove blacklisted properties from joined models, because subpiplines with 'project' are slower
151
- // if (opts.populate) this._depreciated_removeBlacklisted(data, opts.populate, options.projection)
152
- response = await this._processAfterFind(response, options.projection, opts)
135
+
136
+ // Process afterFind hooks
137
+ response = await this._processAfterFind(response, opts.projection, opts)
153
138
 
154
139
  // Success
155
140
  if (cb) cb(null, response)
@@ -167,26 +152,27 @@ module.exports = {
167
152
  return this.find(opts, cb, true)
168
153
  },
169
154
 
170
- // findOneAndUpdate: function(opts, cb) {
171
- // return this._findOneAndUpdate(opts, cb)
172
- // },
155
+ findOneAndUpdate: function(opts, cb) {
156
+ return this._findOneAndUpdate(opts, cb)
157
+ },
173
158
 
174
159
  update: async function(opts, cb) {
175
160
  /**
176
161
  * Updates document(s) with monk after validating data & before hooks.
177
162
  * @param {object} opts
163
+ * @param {object|array} opts.data - mongodb document update object(s)
164
+ * @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove blacklisting
165
+ * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
178
166
  * @param {object} <opts.query> - mongodb query object
179
- * @param {object|array} <opts.data> - mongodb document update object(s)
180
167
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
181
- * @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
182
- * default, but false on update
183
168
  * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
184
169
  * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
185
- * @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove all blacklisting
186
- * @param {any} <opts.any> - any mongodb option
170
+ * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
171
+ * default, but false on update
172
+ * @param {any} <any mongodb option>
187
173
  * @param {function} <cb> - execute cb(err, data) instead of responding
188
- * @this model
189
174
  * @return promise(data)
175
+ * @this model
190
176
  */
191
177
  if (cb && !util.isFunction(cb)) {
192
178
  throw new Error(`The callback passed to ${this.name}.update() is not a function`)
@@ -196,7 +182,7 @@ module.exports = {
196
182
  let data = opts.data
197
183
  let response = null
198
184
  let operators = util.pluck(opts, [/^\$/])
199
- let options = util.omit(opts, ['data', 'query', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'])
185
+ let custom = ['blacklist', 'data', 'query', 'respond', 'skipValidation', 'validateUndefined']
200
186
 
201
187
  // Validate
202
188
  if (util.isDefined(data)) data = await this.validate(opts.data, { ...opts })
@@ -215,7 +201,7 @@ module.exports = {
215
201
  operators['$set'] = { ...data, ...(operators['$set'] || {}) }
216
202
  }
217
203
  // Update
218
- let update = await this._update(opts.query, operators, options)
204
+ let update = await this._update(opts.query, operators, util.omit(opts, custom))
219
205
  if (update.n) response = Object.assign(Object.create({ _output: update }), operators['$set']||{})
220
206
 
221
207
  // Hook: afterUpdate (doesn't have access to validated data)
@@ -240,22 +226,22 @@ module.exports = {
240
226
  * @param {object} <opts.query> - mongodb query object
241
227
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
242
228
  * @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
243
- * @param {any} <opts.any> - any mongodb option
229
+ * @param {any} <any mongodb option>
244
230
  * @param {function} <cb> - execute cb(err, data) instead of responding
245
- * @this model
246
231
  * @return promise
232
+ * @this model
247
233
  */
248
234
  if (cb && !util.isFunction(cb)) {
249
235
  throw new Error(`The callback passed to ${this.name}.remove() is not a function`)
250
236
  }
251
237
  try {
252
238
  opts = await this._queryObject(opts, 'remove')
253
- let options = util.omit(opts, ['query', 'respond'])
239
+ let custom = ['query', 'respond']
254
240
  if (util.isEmpty(opts.query)) throw new Error('Please specify opts.query')
255
241
 
256
242
  // Remove
257
243
  await util.runSeries(this.beforeRemove.map(f => f.bind(opts)))
258
- let response = await this._remove(opts.query, options)
244
+ let response = await this._remove(opts.query, util.omit(opts, custom))
259
245
  await util.runSeries(this.afterRemove.map(f => f.bind(response)))
260
246
 
261
247
  // Success
@@ -270,15 +256,124 @@ module.exports = {
270
256
  }
271
257
  },
272
258
 
259
+ _getProjectionFromBlacklist: function(type, customBlacklist) {
260
+ /**
261
+ * Returns an exclusion projection
262
+ *
263
+ * Path collisions are removed
264
+ * E.g. ['pets.dogs', 'pets.dogs.name', '-cat', 'pets.dogs.age'] = { 'pets.dog': 0 }
265
+ *
266
+ * @param {string} type - find, insert, or update
267
+ * @param {array|string|false} customBlacklist - normally passed through options
268
+ * @return {array|undefined} exclusion $project {'pets.name': 0}
269
+ * @this model
270
+ *
271
+ * 1. collate deep-blacklists
272
+ * 2. concatenate the model's blacklist and any custom blacklist
273
+ * 3. create an exclusion projection object from the blacklist, overriding from left to right
274
+ */
275
+
276
+ let list = []
277
+ let manager = this.manager
278
+ let projection = {}
279
+ if (customBlacklist === false) return
280
+
281
+ // String?
282
+ if (typeof customBlacklist === 'string') {
283
+ customBlacklist = customBlacklist.trim().split(/\s+/)
284
+ }
285
+
286
+ // Concat deep blacklists
287
+ if (type == 'find') {
288
+ util.forEach(this.fieldsFlattened, (schema, path) => {
289
+ if (!schema.model) return
290
+ let deepBL = manager.model[schema.model][`${type}BL`] || []
291
+ let pathWithoutArrays = path.replace(/\.0(\.|$)/, '$1')
292
+ list = list.concat(deepBL.map(o => {
293
+ return `${o.charAt(0) == '-'? '-' : ''}${pathWithoutArrays}.${o.replace(/^-/, '')}`
294
+ }))
295
+ })
296
+ }
297
+
298
+ // Concat model, and custom blacklists
299
+ list = list.concat([...this[`${type}BL`]]).concat(customBlacklist || [])
300
+
301
+ // Loop blacklists
302
+ for (let _key of list) {
303
+ let key = _key.replace(/^-/, '')
304
+ let whitelisted = _key.match(/^-/)
305
+
306
+ // Remove any child fields. E.g remove { user.token: 0 } = key2 if iterating { user: 0 } = key
307
+ for (let key2 in projection) {
308
+ // todo: need to write a test, testing that this is scoped to \.
309
+ if (key2.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) {
310
+ delete projection[key2]
311
+ }
312
+ }
313
+
314
+ // Whitelist
315
+ if (whitelisted) {
316
+ projection[key] = 1
317
+ // Whitelisting a child of a blacklisted field (blacklist expansion)
318
+ // let parent = '' // highest blacklisted parent
319
+ // for (let key2 in projection) {
320
+ // if (key2.length > parent.length && key.match(new RegExp('^' + key2.replace(/\./g, '\\.')))) {
321
+ // parent = key2
322
+ // }
323
+ // }
324
+
325
+ // Blacklist (only if there isn't a parent blacklisted)
326
+ } else {
327
+ let parent
328
+ for (let key2 in projection) { // E.g. [address = key2, addresses.country = key]
329
+ if (projection[key2] == 0 && key.match(new RegExp('^' + key2.replace(/\./g, '\\.') + '\\.'))) {
330
+ parent = key2
331
+ }
332
+ }
333
+ if (!parent) projection[key] = 0
334
+ }
335
+ }
336
+
337
+ // Remove whitelist projections
338
+ for (let key in projection) {
339
+ if (projection[key]) delete projection[key]
340
+ }
341
+
342
+ return util.isEmpty(projection) ? undefined : projection
343
+ },
344
+
345
+ _getProjectionFromProject: function(customProject) {
346
+ /**
347
+ * Returns an in/exclusion projection
348
+ * todo: tests
349
+ *
350
+ * @param {object|array|string} customProject - normally passed through options
351
+ * @return {array|undefined} in/exclusion projection {'pets.name': 0}
352
+ * @this model
353
+ */
354
+ let projection
355
+ if (util.isString(customProject)) {
356
+ customProject = customProject.trim().split(/\s+/)
357
+ }
358
+ if (util.isArray(customProject)) {
359
+ projection = customProject.reduce((o, v) => {
360
+ o[v.replace(/^-/, '')] = v.match(/^-/)? 0 : 1
361
+ return o
362
+ }, {})
363
+ }
364
+ return projection
365
+ },
366
+
273
367
  _queryObject: async function(opts, type, one) {
274
368
  /**
275
369
  * Normalise options
276
370
  * @param {MongoId|string|object} opts
277
371
  * @param {string} type - operation type
278
372
  * @param {boolean} one - return one document
279
- * @this model
280
373
  * @return {Promise} opts
374
+ * @this model
281
375
  *
376
+ * Query parsing logic:
282
377
  * opts == string|MongodId - treated as an id
283
378
  * opts == undefined|null|false - throw error
284
379
  * opts.query == string|MongodID - treated as an id
@@ -321,6 +416,7 @@ module.exports = {
321
416
  if (!util.isDefined(opts.data) && util.isDefined((opts.req||{}).body)) opts.data = opts.req.body
322
417
  if (util.isDefined(opts.data)) opts.data = await util.parseData(opts.data)
323
418
 
419
+ opts.type = type
324
420
  opts[type] = true
325
421
  opts.model = this
326
422
  return opts
@@ -332,11 +428,12 @@ module.exports = {
332
428
  * Recurses through fields that are models and populates missing default-fields and calls model.afterFind([fn,..])
333
429
  * Be sure to add any virtual fields to the schema that your populating on,
334
430
  * e.g. "nurses": [{ model: 'user' }]
431
+ *
335
432
  * @param {object|array|null} data
336
433
  * @param {object} projection - $project object
337
434
  * @param {object} afterFindContext - handy context object given to schema.afterFind
338
- * @this model
339
435
  * @return Promise(data)
436
+ * @this model
340
437
  */
341
438
  // Recurse down from the parent model, ending with the parent model as the parent afterFind hook may
342
439
  // want to manipulate any populated models
@@ -347,17 +444,18 @@ module.exports = {
347
444
 
348
445
  // Loop found model/deep-model data objects, and populate missing default-fields and call afterFind on each
349
446
  for (let item of modelData) {
350
- // Populuate missing default fields if data !== null
447
+ // Populate missing default fields if data !== null
351
448
  // NOTE: maybe only call functions if default is being set.. fine for now
352
449
  if (item.dataRef) {
353
- util.forEach(model[item.modelName].defaultFieldsFlattened, (schema, path) => {
450
+ util.forEach(model[item.modelName].fieldsFlattened, (schema, path) => {
451
+ if (!util.isDefined(schema.default) || path.match(/^\.?(createdAt|updatedAt)$/)) return
354
452
  let parentPath = item.fieldName? item.fieldName + '.' : ''
355
453
  let pathWithoutArrays = (parentPath + path).replace(/\.0(\.|$)/, '$1')
356
- // Ignore default fields that are excluded in a blacklist/parent-blacklist
357
- if (!this._pathInProjection(pathWithoutArrays, projection, true)) return
358
- // Ignore default
454
+ // Ignore default fields that are blacklisted
455
+ if (this._pathBlacklisted(pathWithoutArrays, projection)) return
456
+ // console.log(pathWithoutArrays, path, projection)
457
+ // Set value
359
458
  let value = util.isFunction(schema.default)? schema.default(this.manager) : schema.default
360
- // console.log(item.dataRef)
361
459
  util.setDeepValue(item.dataRef, path.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
362
460
  })
363
461
  }
@@ -369,190 +467,34 @@ module.exports = {
369
467
  return util.runSeries(callbackSeries).then(() => data)
370
468
  },
371
469
 
372
- _addDeepBlacklists: function(blacklistProjection, populate) {
373
- /**
374
- * Include deep-model blacklists into the projection
375
- * @param {object} blacklistProject
376
- * @param {array} populate - find populate array
377
- * @return {object} exclusion blacklist
378
- * @this model
379
- */
380
- let model = this
381
- let manager = this.manager
382
- let paths = (populate||[]).map(o => o && o.as? o.as : o)
383
-
384
- if (!paths.length) return blacklistProjection
385
- this._recurseFields(model.fields, '', function(path, field) {
386
- // Remove array indexes from the path e.g. '0.'
387
- path = path.replace(/(\.[0-9]+)(\.|$)/, '$2')
388
- if (!field.model || !paths.includes(path)) return
389
- loop: for (let prop of manager.model[field.model].findBL) {
390
- // Don't include any deep model projection keys that already have a parent specified. E.g if
391
- // both { users.secrets: 1 } and { users.secrets.token: 1 } exist, remove the later
392
- for (let key in blacklistProjection) {
393
- if ((path + '.' + prop).match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) {
394
- continue loop
395
- }
396
- }
397
- // Add model property to blacklist
398
- blacklistProjection[path + '.' + prop] = 0
399
- }
400
- })
401
- return blacklistProjection
402
- },
403
-
404
- _addBlacklist: function(blacklistProjection, blacklist) {
405
- /**
406
- * Merge blacklist in
407
- * @param {object} blacklistProjection
408
- * @param {array} paths - e.g. ['password', '-email'] - email will be whitelisted / removed from
409
- * exlcusion projection
410
- * @return {object} exclusion blacklist
411
- * @this model
412
- */
413
- if (blacklist === false) {
414
- return {}
415
- } else if (!blacklist) {
416
- return blacklistProjection
417
- } else {
418
- // Loop blacklist
419
- for (let _key of blacklist) {
420
- let key = _key.replace(/^-/, '')
421
- let negated = _key.match(/^-/)
422
- // Remove any deep projection keys that already have a parent specified. E.g if
423
- // both { users.secrets: 0 } and { users.secrets.token: 0 } exist, remove the later
424
- for (let key2 in blacklistProjection) {
425
- if (key2.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) {
426
- delete blacklistProjection[key2]
427
- }
428
- }
429
- if (negated) {
430
- if (blacklistProjection.hasOwnProperty(key)) delete blacklistProjection[key]
431
- } else {
432
- blacklistProjection[key] = 0
433
- }
434
- }
435
- return blacklistProjection
436
- }
437
- },
438
-
439
- _depreciated_removeBlacklisted: function(data, populate, whitelist) {
470
+ _pathBlacklisted: function(path, projection, matchDeepWhitelistedKeys=true) {
440
471
  /**
441
- * Remove blacklisted fields, takes foreign model blacklists into account
442
- * @param {object|array} data
443
- * @param {array} <populate> find populate list
444
- * @param {array} <whitelist>
445
- */
446
- let findBL = this.findBL
447
- let findWL = [ '_id', ...this.findWL ]
448
- let model = this.manager.model
449
-
450
- this._recurseFields(this.fields, '', function(path, field) {
451
- // Remove array indexes from the path e.g. '0.'
452
- path = path.replace(/(\.[0-9]+)(\.|$)/, '$2')
453
- //if (field.type == 'any') findWL.push(path) //exclude.push(path)
454
-
455
- // Below: Merge in model whitelists (we should cache the results)
456
- if (!field.model) return
457
- else if (!model[field.model]) return
458
-
459
- // Has this path been blacklisted already?
460
- for (let blacklisted of findBL) {
461
- if (blacklisted === path || path.includes(blacklisted + '.', 0)) {
462
- return
463
- }
464
- }
465
-
466
- // Populating?
467
- if ((populate||[]).includes(path)) {
468
- // Remove the model-field from the whitelist if populating
469
- let parentIndex = findWL.indexOf(path)
470
- if (parentIndex !== -1) findWL.splice(parentIndex, 1)
471
-
472
- // Okay, merge in the model's whitelist
473
- findWL.push(path + '.' + '_id')
474
- for (let prop of model[field.model].findWL) {
475
- // Dont add model properties that are already blacklisted
476
- if (findBL.includes(path + '.' + prop)) continue
477
- // Add model prop to whitelist
478
- findWL.push(path + '.' + prop)
479
- }
480
- }
481
- })
482
-
483
- // Merge in the passed in whitelist
484
- findWL = findWL.concat(whitelist || [])
485
-
486
- // Fill in missing parents e.g. pets.dog.name, pets.dog doesn't exist
487
- // Doesn't matter what order these are added in
488
- // for (let whitelisted of findWL) {
489
- // let parents = whitelisted.split('.')
490
- // let target = ''
491
- // for (let parent of parents) {
492
- // if (!findWL.includes(target + parent)) findWL.push(target + parent)
493
- // target = target + parent + '.'
494
- // }
495
- // }
496
-
497
- console.log(1, findWL)
498
- console.log(2, data)
499
-
500
- // "data.cat.colour" needs to add "data.cat"
501
- // "data.cat" needs to everything
502
-
503
- function recurseAndDeleteData(data, path) {
504
- // Remove array indexes from the path e.g. '0.'
505
- let newpath = path.replace(/(\.[0-9]+)(\.|$)/, '$2')
506
- util.forEach(data, function(field, fieldName) {
507
- if ((util.isArray(field) || util.isObjectAndNotID(field))/* && !exclude.includes(newpath + fieldName)*/) {
508
- if (findWL.includes(newpath + fieldName)) return
509
- recurseAndDeleteData(field, newpath + fieldName + '.')
510
- } else if (!findWL.includes(newpath + fieldName) && !util.isNumber(fieldName)) {
511
- console.log(3, fieldName)
512
- delete data[fieldName]
513
- }
514
- })
515
- }
516
-
517
- for (let doc of util.toArray(data)) {
518
- recurseAndDeleteData(doc, '')
519
- }
520
- return data
521
- },
522
-
523
- _pathInProjection: function(path, projection, matchParentPaths) {
524
- /**
525
- * Checks if the path is valid within a inclusion/exclusion projection
472
+ * Checks if the path is blacklisted within a inclusion/exclusion projection
526
473
  * @param {string} path - path without array brackets e.g. '.[]'
527
- * @param {object} projection
528
- * @param {boolean} matchParentPaths - match paths included in a key (inclusive only)
474
+ * @param {object} projection - inclusion/exclusion projection, not mixed
475
+ * @param {boolean} matchDeepWhitelistedKeys - match deep whitelisted keys containing path
476
+ * E.g. pets.color == pets.color.age
529
477
  * @return {boolean}
530
478
  */
531
- let inc
532
- // console.log(path, projection)
533
479
  for (let key in projection) {
534
- if (projection[key] && matchParentPaths) {
535
- // Inclusion
536
- // E.g. 'pets.color.age'.match(/^pets.color.age(.|$)/) = match
537
- // E.g. 'pets.color.age'.match(/^pets.color(.|$)/) = match
538
- // E.g. 'pets.color'.match(/^pets.color.age(.|$)/) = match
539
- inc = true
540
- if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '(\\.|$)'))) return true
541
- if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
542
- } else if (projection[key]) {
543
- // Inclusion (equal to key, or key included in path)
544
- // E.g. 'pets.color.age'.match(/^pets.color.age(.|$)/) = match
545
- // E.g. 'pets.color.age'.match(/^pets.color(.|$)/) = match
546
- // E.g. 'pets.color'.match(/^pets.color.age(.|$)/) = no match
547
- inc = true
548
- if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
549
- } else {
550
- // Exclusion
551
- // E.g. 'pets.color.age'.match(/^pets.color(.|$)/) = match
480
+ if (projection[key]) {
481
+ // Inclusion (whitelisted)
482
+ // E.g. pets.color.age == pets.color.age (exact match)
483
+ // E.g. pets.color.age == pets.color (path contains key)
484
+ var inclusion = true
552
485
  if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return false
486
+ if (matchDeepWhitelistedKeys) {
487
+ // E.g. pets.color == pets.color.age (key contains path)
488
+ if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '\\.'))) return false
489
+ }
490
+ } else {
491
+ // Exclusion (blacklisted)
492
+ // E.g. pets.color.age == pets.color.age (exact match)
493
+ // E.g. pets.color.age == pets.color (path contains key)
494
+ if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
553
495
  }
554
496
  }
555
- return inc? false : true
497
+ return inclusion? true : false
556
498
  },
557
499
 
558
500
  _recurseAndFindModels: function(fields, dataArr) {
@@ -630,6 +572,6 @@ module.exports = {
630
572
  cb(path + fieldName, field)
631
573
  }
632
574
  }, this)
633
- }
575
+ },
634
576
 
635
577
  }