monastery 1.34.0 → 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/.eslintrc.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "plugins": [],
22
22
  "rules": {
23
23
  "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
24
- "max-len": ["error", { "code": 120, "ignorePattern": "^\\s*<(rect|path|line)\\s" }],
24
+ "max-len": ["error", { "code": 125, "ignorePattern": "^\\s*<(rect|path|line)\\s" }],
25
25
  "no-prototype-builtins": "off",
26
26
  "no-unused-vars": ["error", { "args": "none" }],
27
27
  "object-shorthand": ["error", "consistent"],
package/changelog.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [1.35.0](https://github.com/boycce/monastery/compare/1.34.0...1.35.0) (2022-04-08)
6
+
7
+
8
+ ### Features
9
+
10
+ * `false` removes blacklist, added tests for blacklisting/project stirng ([5999859](https://github.com/boycce/monastery/commit/599985972cc14b980148db26c03108feabf23756))
11
+
5
12
  ## [1.34.0](https://github.com/boycce/monastery/compare/1.33.0...1.34.0) (2022-04-05)
6
13
 
7
14
 
package/docs/readme.md CHANGED
@@ -87,11 +87,13 @@ Coming soon...
87
87
  - Add before/afterInsertUpdate
88
88
  - Bug: Setting an object literal on an ID field ('model') saves successfully
89
89
  - Population within array items
90
- - ~~Blackislisitng which aggregates from the order of appearance~~
90
+ - ~~Blacklist `false` removes all blacklisting~~
91
+ - ~~Add project to insert/update/validate~~
91
92
  - ~~Whitelisting a parent will remove any previously blacklisted children~~
93
+ - ~~Blacklist/project works the same across find/insert/update/validate~~
92
94
  - Automatic subdocument ids
93
95
  - Remove ACL default 'public read'
94
- - Public db.arrayWithSchema method
96
+ - ~~Public db.arrayWithSchema method~~
95
97
  - Global after/before hooks
96
98
  - Split away from Monk (unless updated)
97
99
  - docs: Make the implicit ID query conversion more apparent
@@ -248,9 +248,9 @@ schema.messages = {
248
248
  }
249
249
  // You can also target any rules set on the array or sub arrays
250
250
  // e.g.
251
- // let arrayWithSchema = (array, schema) => { array.schema = schema; return array }
252
- // petGroups = arrayWithSchema(
253
- // [arrayWithSchema(
251
+ // // let arrayWithSchema = (array, schema) => { array.schema = schema; return array }, OR you can use db.arrayWithSchema
252
+ // petGroups = db.arrayWithSchema(
253
+ // [db.arrayWithSchema(
254
254
  // [{ name: { type: 'string' }}],
255
255
  // { minLength: 1 }
256
256
  // )],
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', '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,26 +152,27 @@ 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: function(opts, cb) {
156
+ return this._findOneAndUpdate(opts, cb)
157
+ },
167
158
 
168
159
  update: async function(opts, cb) {
169
160
  /**
170
161
  * Updates document(s) with monk after validating data & before hooks.
171
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
172
166
  * @param {object} <opts.query> - mongodb query object
173
- * @param {object|array} <opts.data> - mongodb document update object(s)
174
167
  * @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
168
  * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
178
169
  * @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
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>
181
173
  * @param {function} <cb> - execute cb(err, data) instead of responding
182
- * @this model
183
174
  * @return promise(data)
175
+ * @this model
184
176
  */
185
177
  if (cb && !util.isFunction(cb)) {
186
178
  throw new Error(`The callback passed to ${this.name}.update() is not a function`)
@@ -190,7 +182,7 @@ module.exports = {
190
182
  let data = opts.data
191
183
  let response = null
192
184
  let operators = util.pluck(opts, [/^\$/])
193
- let options = util.omit(opts, ['data', 'query', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'])
185
+ let custom = ['blacklist', 'data', 'query', 'respond', 'skipValidation', 'validateUndefined']
194
186
 
195
187
  // Validate
196
188
  if (util.isDefined(data)) data = await this.validate(opts.data, { ...opts })
@@ -209,7 +201,7 @@ module.exports = {
209
201
  operators['$set'] = { ...data, ...(operators['$set'] || {}) }
210
202
  }
211
203
  // Update
212
- let update = await this._update(opts.query, operators, options)
204
+ let update = await this._update(opts.query, operators, util.omit(opts, custom))
213
205
  if (update.n) response = Object.assign(Object.create({ _output: update }), operators['$set']||{})
214
206
 
215
207
  // Hook: afterUpdate (doesn't have access to validated data)
@@ -234,22 +226,22 @@ module.exports = {
234
226
  * @param {object} <opts.query> - mongodb query object
235
227
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
236
228
  * @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
237
- * @param {any} <opts.any> - any mongodb option
229
+ * @param {any} <any mongodb option>
238
230
  * @param {function} <cb> - execute cb(err, data) instead of responding
239
- * @this model
240
231
  * @return promise
232
+ * @this model
241
233
  */
242
234
  if (cb && !util.isFunction(cb)) {
243
235
  throw new Error(`The callback passed to ${this.name}.remove() is not a function`)
244
236
  }
245
237
  try {
246
238
  opts = await this._queryObject(opts, 'remove')
247
- let options = util.omit(opts, ['query', 'respond'])
239
+ let custom = ['query', 'respond']
248
240
  if (util.isEmpty(opts.query)) throw new Error('Please specify opts.query')
249
241
 
250
242
  // Remove
251
243
  await util.runSeries(this.beforeRemove.map(f => f.bind(opts)))
252
- let response = await this._remove(opts.query, options)
244
+ let response = await this._remove(opts.query, util.omit(opts, custom))
253
245
  await util.runSeries(this.afterRemove.map(f => f.bind(response)))
254
246
 
255
247
  // Success
@@ -264,13 +256,16 @@ module.exports = {
264
256
  }
265
257
  },
266
258
 
267
- _getBlacklistProjection: function(type, customBlacklist) {
259
+ _getProjectionFromBlacklist: function(type, customBlacklist) {
268
260
  /**
269
261
  * Returns an exclusion projection
270
262
  *
263
+ * Path collisions are removed
264
+ * E.g. ['pets.dogs', 'pets.dogs.name', '-cat', 'pets.dogs.age'] = { 'pets.dog': 0 }
265
+ *
271
266
  * @param {string} type - find, insert, or update
272
- * @param {array} customBlacklist - normally passed through options
273
- * @return {array} exclusion projection {'pets.name': 0}
267
+ * @param {array|string|false} customBlacklist - normally passed through options
268
+ * @return {array|undefined} exclusion $project {'pets.name': 0}
274
269
  * @this model
275
270
  *
276
271
  * 1. collate deep-blacklists
@@ -281,16 +276,24 @@ module.exports = {
281
276
  let list = []
282
277
  let manager = this.manager
283
278
  let projection = {}
279
+ if (customBlacklist === false) return
280
+
281
+ // String?
282
+ if (typeof customBlacklist === 'string') {
283
+ customBlacklist = customBlacklist.trim().split(/\s+/)
284
+ }
284
285
 
285
286
  // 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
- })
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
+ }
294
297
 
295
298
  // Concat model, and custom blacklists
296
299
  list = list.concat([...this[`${type}BL`]]).concat(customBlacklist || [])
@@ -302,7 +305,8 @@ module.exports = {
302
305
 
303
306
  // Remove any child fields. E.g remove { user.token: 0 } = key2 if iterating { user: 0 } = key
304
307
  for (let key2 in projection) {
305
- if (key2.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) { // test that scoped to the \.
308
+ // todo: need to write a test, testing that this is scoped to \.
309
+ if (key2.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) {
306
310
  delete projection[key2]
307
311
  }
308
312
  }
@@ -335,6 +339,28 @@ module.exports = {
335
339
  if (projection[key]) delete projection[key]
336
340
  }
337
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
+ }
338
364
  return projection
339
365
  },
340
366
 
@@ -344,9 +370,10 @@ module.exports = {
344
370
  * @param {MongoId|string|object} opts
345
371
  * @param {string} type - operation type
346
372
  * @param {boolean} one - return one document
347
- * @this model
348
373
  * @return {Promise} opts
374
+ * @this model
349
375
  *
376
+ * Query parsing logic:
350
377
  * opts == string|MongodId - treated as an id
351
378
  * opts == undefined|null|false - throw error
352
379
  * opts.query == string|MongodID - treated as an id
@@ -389,6 +416,7 @@ module.exports = {
389
416
  if (!util.isDefined(opts.data) && util.isDefined((opts.req||{}).body)) opts.data = opts.req.body
390
417
  if (util.isDefined(opts.data)) opts.data = await util.parseData(opts.data)
391
418
 
419
+ opts.type = type
392
420
  opts[type] = true
393
421
  opts.model = this
394
422
  return opts
@@ -400,11 +428,12 @@ module.exports = {
400
428
  * Recurses through fields that are models and populates missing default-fields and calls model.afterFind([fn,..])
401
429
  * Be sure to add any virtual fields to the schema that your populating on,
402
430
  * e.g. "nurses": [{ model: 'user' }]
431
+ *
403
432
  * @param {object|array|null} data
404
433
  * @param {object} projection - $project object
405
434
  * @param {object} afterFindContext - handy context object given to schema.afterFind
406
- * @this model
407
435
  * @return Promise(data)
436
+ * @this model
408
437
  */
409
438
  // Recurse down from the parent model, ending with the parent model as the parent afterFind hook may
410
439
  // want to manipulate any populated models
@@ -422,8 +451,8 @@ module.exports = {
422
451
  if (!util.isDefined(schema.default) || path.match(/^\.?(createdAt|updatedAt)$/)) return
423
452
  let parentPath = item.fieldName? item.fieldName + '.' : ''
424
453
  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
454
+ // Ignore default fields that are blacklisted
455
+ if (this._pathBlacklisted(pathWithoutArrays, projection)) return
427
456
  // console.log(pathWithoutArrays, path, projection)
428
457
  // Set value
429
458
  let value = util.isFunction(schema.default)? schema.default(this.manager) : schema.default
@@ -438,39 +467,34 @@ module.exports = {
438
467
  return util.runSeries(callbackSeries).then(() => data)
439
468
  },
440
469
 
441
- _pathInProjection: function(path, projection, matchParentPaths) {
470
+ _pathBlacklisted: function(path, projection, matchDeepWhitelistedKeys=true) {
442
471
  /**
443
- * Checks if the path is valid within a inclusion/exclusion projection
472
+ * Checks if the path is blacklisted within a inclusion/exclusion projection
444
473
  * @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)
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
447
477
  * @return {boolean}
448
478
  */
449
- let inc
450
- // console.log(path, projection)
451
479
  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
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
470
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
471
495
  }
472
496
  }
473
- return inc? false : true
497
+ return inclusion? true : false
474
498
  },
475
499
 
476
500
  _recurseAndFindModels: function(fields, dataArr) {
@@ -6,21 +6,19 @@ module.exports = {
6
6
  validate: function(data, opts, cb) {
7
7
  /**
8
8
  * Validates a model
9
- * @param {instance} model
10
9
  * @param {object} data
11
10
  * @param {object} <opts>
12
- * @param {boolean(false)} update - are we validating for insert or update?
13
- * @param {array|string|false} blacklist - augment schema blacklist, `false` will remove all blacklisting
14
- * @param {array|string} projection - only return these fields, ignores blacklist
15
- * @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
16
- * default, but false on update
17
- * @param {array|string|true} skipValidation - skip validation on these fields
18
- * @param {boolean} timestamps - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
11
+ * @param {array|string|false} <opts.blacklist> - augment insertBL/updateBL, `false` will remove blacklisting
12
+ * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
13
+ * @param {array|string|true} <opts.skipValidation> - skip validation on these fields
14
+ * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
19
15
  * updated, depending on the `options.update` value
16
+ * @param {boolean(false)} <opts.update> - are we validating for insert or update? todo: change to `type`
17
+ * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
18
+ * default, but false on update
20
19
  * @param {function} <cb> - instead of returning a promise
21
- * @this model
22
-
23
20
  * @return promise(errors[] || pruned data{})
21
+ * @this model
24
22
  */
25
23
 
26
24
  // Optional cb and opts
@@ -31,41 +29,12 @@ module.exports = {
31
29
  data = util.deepCopy(data)
32
30
  opts = opts || {}
33
31
  opts.insert = !opts.update
34
- opts.action = opts.update? 'update' : 'insert'
35
- opts.skipValidation = opts.skipValidation === true? true : util.toArray(opts.skipValidation||[])
32
+ opts.action = opts.update ? 'update' : 'insert'
33
+ opts.skipValidation = opts.skipValidation === true ? true : util.toArray(opts.skipValidation||[])
36
34
 
37
- // Blacklist
38
- if (opts.blacklist) {
39
- let whitelist = []
40
- let blacklist = [ ...this[`${opts.action}BL`] ]
41
- if (typeof opts.blacklist === 'string') {
42
- opts.blacklist = opts.blacklist.trim().split(/\s+/)
43
- }
44
- // Auguemnt the schema blacklist
45
- for (let _path of opts.blacklist) {
46
- let path = _path.replace(/^-/, '')
47
- if (_path.match(/^-/)) whitelist.push(path)
48
- else blacklist.push(path)
49
- }
50
- // Remove whitelisted/negated fields
51
- blacklist = blacklist.filter(o => !whitelist.includes(o))
52
- // Remove any deep blacklisted fields that have a whitelisted parent specified.
53
- // E.g remove ['deep.deep2.deep3'] if ['deep'] exists in the whitelist
54
- for (let i=blacklist.length; i--;) {
55
- let split = blacklist[i].split('.')
56
- for (let j=split.length; j--;) {
57
- if (split.length > 1) split.pop()
58
- else continue
59
- if (whitelist.includes(split.join())) {
60
- blacklist.splice(i, 1)
61
- break
62
- }
63
- }
64
- }
65
- opts.blacklist = blacklist
66
- } else {
67
- opts.blacklist = [ ...this[`${opts.action}BL`] ]
68
- }
35
+ // Get projection
36
+ if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
37
+ else opts.projection = this._getProjectionFromBlacklist(opts.action, opts.blacklist)
69
38
 
70
39
  // Run before hook, then recurse through the model's fields
71
40
  return util.runSeries(this.beforeValidate.map(f => f.bind(opts, data))).then(() => {
@@ -140,7 +109,7 @@ module.exports = {
140
109
  let value = util.isArray(fields)? data : (data||{})[fieldName]
141
110
  let indexOrFieldName = util.isArray(fields)? i : fieldName
142
111
  let path2 = `${path}.${indexOrFieldName}`.replace(/^\./, '')
143
- let path3 = path2.replace(/(^|\.)[0-9]+(\.|$)/, '$2') // no numerical keys, e.g. pets.1.name
112
+ let path3 = path2.replace(/(^|\.)[0-9]+(\.|$)/, '$2') // no numerical keys, e.g. pets.1.name = pets.name
144
113
  let isType = 'is' + util.ucFirst(schema.type)
145
114
  let isTypeRule = this.rules[isType] || rules[isType]
146
115
 
@@ -157,7 +126,7 @@ module.exports = {
157
126
  }
158
127
 
159
128
  // Ignore blacklisted
160
- if (opts.blacklist.indexOf(path3) >= 0 && !schema.defaultOverride) return
129
+ if (this._pathBlacklisted(path3, opts.projection) && !schema.defaultOverride) return
161
130
  // Ignore insert only
162
131
  if (opts.update && schema.insertOnly) return
163
132
  // Ignore virtual fields
@@ -218,8 +187,8 @@ module.exports = {
218
187
  * @param {object} field - field schema
219
188
  * @param {string} path - full field path
220
189
  * @param {object} opts - original validate() options
221
- * @this model
222
190
  * @return {array} errors
191
+ * @this model
223
192
  */
224
193
  let errors = []
225
194
  if (opts.skipValidation === true) return []
package/lib/model.js CHANGED
@@ -9,8 +9,8 @@ let Model = module.exports = function(name, opts, manager) {
9
9
  * @param {string} name
10
10
  * @param {object} opts - see mongodb colleciton documentation
11
11
  * @param {boolean} opts.waitForIndexes
12
- * @this model
13
12
  * @return Promise(model) | this
13
+ * @this model
14
14
  */
15
15
  if (!(this instanceof Model)) {
16
16
  return new Model(name, opts, this)
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "monastery",
3
3
  "description": "⛪ A straight forward MongoDB ODM built around Monk",
4
4
  "author": "Ricky Boyce",
5
- "version": "1.34.0",
5
+ "version": "1.35.0",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -1,6 +1,6 @@
1
1
  module.exports = function(monastery, opendb) {
2
2
 
3
- test('find blacklisting', async () => {
3
+ test('find blacklisting basic', async () => {
4
4
  // Setup
5
5
  let db = (await opendb(null)).db
6
6
  let bird = db.model('bird', {
@@ -93,7 +93,7 @@ module.exports = function(monastery, opendb) {
93
93
  }
94
94
  }})
95
95
 
96
- // Test initial blacklist
96
+ // initial blacklist
97
97
  let find1 = await user.findOne({
98
98
  query: user1._id
99
99
  })
@@ -107,18 +107,30 @@ module.exports = function(monastery, opendb) {
107
107
  deepModel: { myBird: bird1._id }
108
108
  })
109
109
 
110
- // Test augmented blacklist
110
+ // augmented blacklist
111
111
  let find2 = await user.findOne({
112
112
  query: user1._id,
113
113
  blacklist: ['pet', 'pet', 'deep', 'deepModel', '-dog', '-animals.cat']
114
114
  })
115
- expect(find2).toEqual({
115
+ let customBlacklist
116
+ expect(find2).toEqual((customBlacklist = {
116
117
  _id: user1._id,
117
118
  dog: 'Bruce',
118
119
  list: [44, 54],
119
120
  pets: [{ name: 'Pluto' }, { name: 'Milo' }],
120
121
  animals: { dog: 'Max', cat: 'Ginger' }
122
+ }))
123
+
124
+ // blacklist string
125
+ let find3 = await user.findOne({
126
+ query: user1._id,
127
+ blacklist: 'pet pet deep deepModel -dog -animals.cat'
121
128
  })
129
+ expect(find3).toEqual(customBlacklist)
130
+
131
+ // blacklist removal
132
+ let find4 = await user.findOne({ query: user1._id, blacklist: false })
133
+ expect(find4).toEqual(user1)
122
134
 
123
135
  db.close()
124
136
  })
@@ -219,6 +231,11 @@ module.exports = function(monastery, opendb) {
219
231
  _id: user1._id,
220
232
  bird5: { ...bird1Base, name: 'ponyo', height: 40 },
221
233
  })
234
+ // blacklist removal
235
+ expect(await user.findOne({ query: user1._id, blacklist: false, populate: ['bird1'] })).toEqual({
236
+ ...user1,
237
+ bird1: { ...bird1Base, height: 40, name: 'ponyo', wing: { size: 1, sizes: { one: 1, two: 1 }} },
238
+ })
222
239
 
223
240
  db.close()
224
241
  })
@@ -243,13 +260,13 @@ module.exports = function(monastery, opendb) {
243
260
  },
244
261
  })
245
262
  // default
246
- expect(db.user._getBlacklistProjection('find')).toEqual({
263
+ expect(db.user._getProjectionFromBlacklist('find')).toEqual({
247
264
  'bird1.wing': 0,
248
265
  'bird1.age': 0,
249
266
  'password': 0,
250
267
  })
251
268
  // blacklist /w invalid field (which goes through)
252
- expect(db.user._getBlacklistProjection('find', ['name', 'invalidfield'])).toEqual({
269
+ expect(db.user._getProjectionFromBlacklist('find', ['name', 'invalidfield'])).toEqual({
253
270
  'bird1.wing': 0,
254
271
  'bird1.age': 0,
255
272
  'invalidfield': 0,
@@ -257,38 +274,38 @@ module.exports = function(monastery, opendb) {
257
274
  'password': 0,
258
275
  })
259
276
  // whitelist
260
- expect(db.user._getBlacklistProjection('find', ['-password', '-bird1.age'])).toEqual({
277
+ expect(db.user._getProjectionFromBlacklist('find', ['-password', '-bird1.age'])).toEqual({
261
278
  'bird1.wing': 0,
262
279
  })
263
280
  // whitelist parent
264
- expect(db.user._getBlacklistProjection('find', ['-bird1'])).toEqual({
281
+ expect(db.user._getProjectionFromBlacklist('find', ['-bird1'])).toEqual({
265
282
  'password': 0,
266
283
  })
267
284
  // whitelist parent, then blacklist child
268
- expect(db.user._getBlacklistProjection('find', ['-bird1', 'bird1.name'])).toEqual({
285
+ expect(db.user._getProjectionFromBlacklist('find', ['-bird1', 'bird1.name'])).toEqual({
269
286
  'password': 0,
270
287
  'bird1.name': 0,
271
288
  })
272
289
  // the model's blacklists are applied after deep model's
273
290
  db.user.findBL = ['-bird1.age']
274
- expect(db.user._getBlacklistProjection('find')).toEqual({
291
+ expect(db.user._getProjectionFromBlacklist('find')).toEqual({
275
292
  'bird1.wing': 0,
276
293
  })
277
294
  // custom blacklists are applied after the model's, which are after deep model's
278
295
  db.user.findBL = ['-bird1.age']
279
- expect(db.user._getBlacklistProjection('find', ['bird1'])).toEqual({
296
+ expect(db.user._getProjectionFromBlacklist('find', ['bird1'])).toEqual({
280
297
  'bird1': 0,
281
298
  })
282
299
  // blacklisted parent with a blacklisted child
283
- expect(db.user._getBlacklistProjection('find', ['bird1', 'bird1.wing'])).toEqual({
300
+ expect(db.user._getProjectionFromBlacklist('find', ['bird1', 'bird1.wing'])).toEqual({
284
301
  'bird1': 0,
285
302
  })
286
303
  // A mess of things
287
- expect(db.user._getBlacklistProjection('find', ['-bird1', 'bird1.wing', '-bird1.wing','bird1.wing.size'])).toEqual({
304
+ expect(db.user._getProjectionFromBlacklist('find', ['-bird1', 'bird1.wing', '-bird1.wing','bird1.wing.size'])).toEqual({
288
305
  'bird1.wing.size': 0,
289
306
  })
290
307
  // blacklisted parent with a whitelisted child (expect blacklist expansion in future version?)
291
- // expect(db.user._getBlacklistProjection('find', ['bird1', '-bird1.wing'])).toEqual({
308
+ // expect(db.user._getProjectionFromBlacklist('find', ['bird1', '-bird1.wing'])).toEqual({
292
309
  // 'bird1.age': 0,
293
310
  // 'bird1.name': 0,
294
311
  // })
@@ -296,7 +313,7 @@ module.exports = function(monastery, opendb) {
296
313
  db.close()
297
314
  })
298
315
 
299
- test('find project', async () => {
316
+ test('find project basic', async () => {
300
317
  // Test mongodb native project option
301
318
  // Setup
302
319
  let db = (await opendb(null)).db
@@ -370,14 +387,14 @@ module.exports = function(monastery, opendb) {
370
387
  color: { type: 'string', default: 'red' },
371
388
  }
372
389
  },
373
- findBL: ['age']
390
+ findBL: ['age'],
374
391
  })
375
392
  let user = db.model('user', {
376
393
  fields: {
377
394
  dog: { type: 'string' },
378
395
  bird: { model: 'bird' },
379
396
  bird2: { model: 'bird' },
380
- bird3: { model: 'bird' }
397
+ bird3: { model: 'bird' },
381
398
  },
382
399
  findBL: [
383
400
  // allll these should be ignored.....?/////
@@ -390,38 +407,47 @@ module.exports = function(monastery, opendb) {
390
407
  name: 'ponyo',
391
408
  age: 3,
392
409
  height: 40,
393
- sub: {}
410
+ sub: {},
394
411
  }})
395
412
  let user1 = await user.insert({ data: {
396
413
  dog: 'Bruce',
397
414
  bird: bird1._id,
398
415
  bird2: bird1._id,
399
- bird3: bird1._id
416
+ bird3: bird1._id,
400
417
  }})
401
418
 
402
- // Test project
419
+ // project
403
420
  let find1 = await user.findOne({
404
421
  query: user1._id,
405
422
  populate: ['bird', 'bird2'],
406
- project: ['bird.age', 'bird2']
423
+ project: ['bird.age', 'bird2'],
407
424
  })
408
425
  expect(find1).toEqual({
409
426
  _id: user1._id,
410
427
  bird: { age: 3 },
411
- bird2: { _id: bird1._id, age: 3, name: 'ponyo', height: 40, color: 'red', sub: { color: 'red' }}
428
+ bird2: { _id: bird1._id, age: 3, name: 'ponyo', height: 40, color: 'red', sub: { color: 'red' }},
412
429
  })
413
430
 
414
- // Test project (different project details)
431
+ // project (different project details)
415
432
  let find2 = await user.findOne({
416
433
  query: user1._id,
417
434
  populate: ['bird', 'bird2'],
418
- project: ['bird', 'bird2.height']
435
+ project: ['bird', 'bird2.height'],
419
436
  })
420
- expect(find2).toEqual({
437
+ let customProject
438
+ expect(find2).toEqual((customProject={
421
439
  _id: user1._id,
422
440
  bird: { _id: bird1._id, age: 3, name: 'ponyo', height: 40, color: 'red', sub: { color: 'red' }},
423
441
  bird2: { height: 40 },
442
+ }))
443
+
444
+ // project string
445
+ let find3 = await user.findOne({
446
+ query: user1._id,
447
+ populate: ['bird', 'bird2'],
448
+ project: 'bird bird2.height',
424
449
  })
450
+ expect(find3).toEqual(customProject)
425
451
 
426
452
  db.close()
427
453
  })
@@ -506,7 +532,8 @@ module.exports = function(monastery, opendb) {
506
532
  '-deep' // blacklist a parent
507
533
  ],
508
534
  })
509
- expect(user2).toEqual({
535
+ let customBlacklist
536
+ expect(user2).toEqual((customBlacklist = {
510
537
  list: [44, 54],
511
538
  dog: 'Bruce',
512
539
  pet: 'Freddy',
@@ -522,6 +549,36 @@ module.exports = function(monastery, opendb) {
522
549
  }
523
550
  }
524
551
  }
552
+ }))
553
+
554
+ // Blacklist string
555
+ let user3 = await user.validate(doc1, {
556
+ blacklist: '-dog -animals.dog pets.name -hiddenList -deep'
557
+ })
558
+ expect(user3).toEqual(customBlacklist)
559
+
560
+ // Blacklist removal
561
+ let user4 = await user.validate(doc1, { blacklist: false })
562
+ expect(user4).toEqual(doc1)
563
+
564
+ // Project whitelist
565
+ let user5 = await user.validate(doc1, {
566
+ project: [
567
+ 'dog',
568
+ 'pets.name',
569
+ 'deep'
570
+ ],
571
+ })
572
+ expect(user5).toEqual({
573
+ dog: 'Bruce',
574
+ pets: [ {name: 'Pluto'}, {name: 'Milo'} ],
575
+ deep: {
576
+ deep2: {
577
+ deep3: {
578
+ deep4: 'hideme'
579
+ }
580
+ }
581
+ }
525
582
  })
526
583
  db.close()
527
584
  })