monastery 1.32.3 → 1.33.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/changelog.md CHANGED
@@ -2,6 +2,22 @@
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.33.0](https://github.com/boycce/monastery/compare/1.32.5...1.33.0) (2022-04-05)
6
+
7
+
8
+ ### Features
9
+
10
+ * whitelisting a parent will remove any previously blacklisted children ([d335c2e](https://github.com/boycce/monastery/commit/d335c2e2e9e691b2c0b406f06cb9c2ed14f8bb25))
11
+
12
+ ### [1.32.5](https://github.com/boycce/monastery/compare/1.32.4...1.32.5) (2022-03-21)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * dates not enforcing numbers ([d5a2a53](https://github.com/boycce/monastery/commit/d5a2a533f497b3c9e50cbeb66407cf9bc08dd2e4))
18
+
19
+ ### [1.32.4](https://github.com/boycce/monastery/compare/1.32.3...1.32.4) (2022-03-07)
20
+
5
21
  ### [1.32.3](https://github.com/boycce/monastery/compare/1.32.2...1.32.3) (2022-03-07)
6
22
 
7
23
 
package/docs/readme.md CHANGED
@@ -81,6 +81,20 @@ npm run dev -- -t 'Model indexes'
81
81
 
82
82
  Coming soon...
83
83
 
84
+ ## Roadmap
85
+
86
+ - Add FindOneAndUpdate
87
+ - Add before/afterInsertUpdate
88
+ - Bug: Setting an object literal on an ID field ('model') saves successfully
89
+ - Population within array items
90
+ - Deep blackislisitng which aggregates from the order of appearance
91
+ - Automatic subdocument ids
92
+ - Remove ACL default 'public read'
93
+ - Public db.arrayWithSchema method
94
+ - Global after/before hooks
95
+ - Split away from Monk (unless updated)
96
+ - docs: Make the implicit ID query conversion more apparent
97
+
84
98
  ## Special Thanks
85
99
 
86
100
  [Jerome Gravel-Niquet](https://github.com/jeromegn)
package/lib/model-crud.js CHANGED
@@ -70,9 +70,8 @@ module.exports = {
70
70
  let options = util.omit(opts, ['blacklist', 'one', 'populate', 'project', 'query', 'respond'])
71
71
  options.addFields = options.addFields || {}
72
72
 
73
- // Project, or use blacklisting
73
+ // Using project (can be an inclusion/exclusion)
74
74
  if (opts.project) {
75
- // Can be an inclusion or exclusion projection
76
75
  if (util.isString(opts.project)) {
77
76
  opts.project = opts.project.trim().split(/\s+/)
78
77
  }
@@ -82,11 +81,9 @@ module.exports = {
82
81
  return o
83
82
  }, {})
84
83
  }
84
+ // Or blacklisting
85
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)
86
+ options.projection = this._getBlacklistProjection('find', opts.blacklist)
90
87
  }
91
88
  // Has text search?
92
89
  // if (opts.query.$text) {
@@ -146,9 +143,6 @@ module.exports = {
146
143
  }
147
144
 
148
145
  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
146
  response = await this._processAfterFind(response, options.projection, opts)
153
147
 
154
148
  // Success
@@ -270,6 +264,80 @@ module.exports = {
270
264
  }
271
265
  },
272
266
 
267
+ _getBlacklistProjection: function(type, customBlacklist) {
268
+ /**
269
+ * Returns an exclusion projection
270
+ *
271
+ * @param {string} type - find, insert, or update
272
+ * @param {array} customBlacklist - normally passed through options
273
+ * @return {array} exclusion projection {'pets.name': 0}
274
+ * @this model
275
+ *
276
+ * 1. collate deep-blacklists
277
+ * 2. concatenate the model's blacklist and any custom blacklist
278
+ * 3. create an exclusion projection object from the blacklist, overriding from left to right
279
+ */
280
+
281
+ let list = []
282
+ let manager = this.manager
283
+ let projection = {}
284
+
285
+ // 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
+ })
294
+
295
+ // Concat model, and custom blacklists
296
+ list = list.concat([...this[`${type}BL`]]).concat(customBlacklist || [])
297
+
298
+ // Loop blacklists
299
+ for (let _key of list) {
300
+ let key = _key.replace(/^-/, '')
301
+ let whitelisted = _key.match(/^-/)
302
+
303
+ // Remove any child fields. E.g remove { user.token: 0 } = key2 if iterating { user: 0 } = key
304
+ for (let key2 in projection) {
305
+ if (key2.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) { // test that scoped to the \.
306
+ delete projection[key2]
307
+ }
308
+ }
309
+
310
+ // Whitelist
311
+ if (whitelisted) {
312
+ projection[key] = 1
313
+ // Whitelisting a child of a blacklisted field (blacklist expansion)
314
+ // let parent = '' // highest blacklisted parent
315
+ // for (let key2 in projection) {
316
+ // if (key2.length > parent.length && key.match(new RegExp('^' + key2.replace(/\./g, '\\.')))) {
317
+ // parent = key2
318
+ // }
319
+ // }
320
+
321
+ // Blacklist (only if there isn't a parent blacklisted)
322
+ } else {
323
+ let parent
324
+ for (let key2 in projection) { // E.g. [address = key2, addresses.country = key]
325
+ if (projection[key2] == 0 && key.match(new RegExp('^' + key2.replace(/\./g, '\\.') + '\\.'))) {
326
+ parent = key2
327
+ }
328
+ }
329
+ if (!parent) projection[key] = 0
330
+ }
331
+ }
332
+
333
+ // Remove whitelist projections
334
+ for (let key in projection) {
335
+ if (projection[key]) delete projection[key]
336
+ }
337
+
338
+ return projection
339
+ },
340
+
273
341
  _queryObject: async function(opts, type, one) {
274
342
  /**
275
343
  * Normalise options
@@ -347,17 +415,18 @@ module.exports = {
347
415
 
348
416
  // Loop found model/deep-model data objects, and populate missing default-fields and call afterFind on each
349
417
  for (let item of modelData) {
350
- // Populuate missing default fields if data !== null
418
+ // Populate missing default fields if data !== null
351
419
  // NOTE: maybe only call functions if default is being set.. fine for now
352
420
  if (item.dataRef) {
353
- util.forEach(model[item.modelName].defaultFieldsFlattened, (schema, path) => {
421
+ util.forEach(model[item.modelName].fieldsFlattened, (schema, path) => {
422
+ if (!util.isDefined(schema.default) || path.match(/^\.?(createdAt|updatedAt)$/)) return
354
423
  let parentPath = item.fieldName? item.fieldName + '.' : ''
355
424
  let pathWithoutArrays = (parentPath + path).replace(/\.0(\.|$)/, '$1')
356
425
  // Ignore default fields that are excluded in a blacklist/parent-blacklist
357
426
  if (!this._pathInProjection(pathWithoutArrays, projection, true)) return
358
- // Ignore default
427
+ // console.log(pathWithoutArrays, path, projection)
428
+ // Set value
359
429
  let value = util.isFunction(schema.default)? schema.default(this.manager) : schema.default
360
- // console.log(item.dataRef)
361
430
  util.setDeepValue(item.dataRef, path.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
362
431
  })
363
432
  }
@@ -369,157 +438,6 @@ module.exports = {
369
438
  return util.runSeries(callbackSeries).then(() => data)
370
439
  },
371
440
 
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) {
440
- /**
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
441
  _pathInProjection: function(path, projection, matchParentPaths) {
524
442
  /**
525
443
  * Checks if the path is valid within a inclusion/exclusion projection
@@ -630,6 +548,6 @@ module.exports = {
630
548
  cb(path + fieldName, field)
631
549
  }
632
550
  }, this)
633
- }
551
+ },
634
552
 
635
553
  }
package/lib/model.js CHANGED
@@ -69,12 +69,8 @@ let Model = module.exports = function(name, opts, manager) {
69
69
  }, this)
70
70
 
71
71
  // Extend default fields with passed in fields and check for invalid fields
72
- this.fields = Object.assign({}, this._timestampFields, this.fields)
73
- this._setupFieldsAndWhitelists(this.fields)
74
-
75
- // console.log(0, this.fieldlist)
76
- // console.log(0, this.findBL)
77
- // console.log(0, this.findBLProject)
72
+ this._setupFields(this.fields = Object.assign({}, this._timestampFields, this.fields))
73
+ this.fieldsFlattened = this._getFieldsFlattened(this.fields, '') // test output?
78
74
 
79
75
  // Extend model with monk collection actions
80
76
  this._collection = manager.get? manager.get(name, { castIds: false }) : null
@@ -107,52 +103,24 @@ let Model = module.exports = function(name, opts, manager) {
107
103
  else this._setupIndexes().catch(errHandler) // returns this
108
104
  }
109
105
 
110
- Model.prototype._getFieldlist = function(fields, path) {
111
- /**
112
- * Get all field paths (without array indices, handy for blacklisting)
113
- * @param {object|array} fields - subdocument or array
114
- * @param {string} path
115
- * @return {array} e.g. ['name', 'pets.dog']
116
- */
117
- let list = []
118
- util.forEach(fields, function(field, fieldName) {
119
- // Don't append array indexes to the new path e.g. '0.'
120
- let newPath = util.isArray(fields)? path : path + fieldName + '.'
121
- if (fieldName == 'schema') {
122
- return
123
- /*} else if (this.findBL.includes(newPath.replace(/\.$/, ''))) {
124
- return*/
125
- } else if (util.isArray(field)) {
126
- list = list.concat(this._getFieldlist(field, newPath))
127
- } else if (util.isSubdocument(field)) {
128
- list = list.concat(this._getFieldlist(field, newPath))
129
- } else {
130
- list.push(newPath.replace(/\.$/, ''))
131
- }
132
- }, this)
133
- return list
134
- }
135
-
136
106
  Model.prototype._getFieldsFlattened = function(fields, path) {
137
107
  /**
138
108
  * Flatten fields
139
- * @param {object|array} fields - subdocument or array
109
+ * @param {object|array} fields - can be a nested subdocument or array
140
110
  * @param {string} path
141
- * @return {object} e.g. ['name', 'pets.dog']
111
+ * @return {object} e.g. {'name': Schema, 'pets.dog': Schema}
142
112
  */
143
113
  let obj = {}
144
114
  util.forEach(fields, function(field, fieldName) {
145
- let newPath = path + fieldName + '.'
115
+ let newPath = /*util.isArray(fields)? path : */path + fieldName + '.'
146
116
  if (fieldName == 'schema') {
147
117
  return
148
118
  } else if (util.isArray(field)) {
149
119
  Object.assign(obj, this._getFieldsFlattened(field, newPath))
150
120
  } else if (util.isSubdocument(field)) {
151
121
  Object.assign(obj, this._getFieldsFlattened(field, newPath))
152
- } else if (fieldName != 'createdAt' && fieldName != 'updatedAt') {
153
- if (util.isDefined(field.default)) {
154
- obj[newPath.replace(/\.$/, '')] = field
155
- }
122
+ } else {
123
+ obj[newPath.replace(/\.$/, '')] = field
156
124
  }
157
125
  }, this)
158
126
  return obj
@@ -234,19 +202,6 @@ Model.prototype._setupFields = function(fields) {
234
202
  }, this)
235
203
  },
236
204
 
237
- Model.prototype._setupFieldsAndWhitelists = function(fields, path) {
238
- /**
239
- * Setup fields and retrieve a handy schema field list. This can be called mulitple times.
240
- * Note: the project fields are only required when finding with projections.
241
- * @param {object} fields - can be a nested subdocument or array
242
- * @param {string} <path>
243
- */
244
- this._setupFields(fields)
245
- this.fieldlist = this._getFieldlist(fields, path || '')
246
- this.defaultFieldsFlattened = this._getFieldsFlattened(fields, path || '') // test output?
247
- this.findBLProject = this.findBL.reduce((o, v) => { (o[v] = 0); return o }, {})
248
- },
249
-
250
205
  Model.prototype._setupIndexes = function(fields, opts={}) {
251
206
  /**
252
207
  * Creates indexes for the model (multikey, and sub-document supported)
package/lib/rules.js CHANGED
@@ -1,3 +1,4 @@
1
+ // Todo: remove stringnums in date/number/integer rules
1
2
  let ObjectId = require('mongodb').ObjectId
2
3
  let util = require('./util')
3
4
  let validator = require('validator')
@@ -48,12 +49,12 @@ module.exports = {
48
49
  message: 'Value was not a unix timestamp.',
49
50
  tryParse: function(x) {
50
51
  if (x === '') return null
51
- if (util.isString(x) && x.match(/^[+-]?[0-9]+$/)) return x // keep string nums intact
52
- return isNaN(parseInt(x))? x : parseInt(x)
52
+ if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
53
+ return isNaN(Number(x)) || (!x && x!==0) || x === true? x : Number(x)
53
54
  },
54
55
  fn: function(x) {
55
- if (util.isString(x) && x.match(/^[+-]?[0-9]+$/)) return true
56
- return typeof x === 'number' && (parseInt(x) === x)
56
+ if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return true
57
+ return typeof x === 'number'
57
58
  }
58
59
  },
59
60
  isImageObject: {
@@ -78,10 +79,10 @@ module.exports = {
78
79
  tryParse: function(x) {
79
80
  if (x === '') return null
80
81
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
81
- return isNaN(parseInt(x)) || (!x && x!==0) || x===true? x : parseInt(x)
82
+ return isNaN(parseInt(x)) || (!x && x!==0) || x === true? x : parseInt(x)
82
83
  },
83
84
  fn: function(x) {
84
- if (util.isString(x) && x.match(/^[+-]?[0-9]+$/)) return true
85
+ if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return true
85
86
  return typeof x === 'number' && (parseInt(x) === x)
86
87
  }
87
88
  },
@@ -91,7 +92,7 @@ module.exports = {
91
92
  tryParse: function(x) {
92
93
  if (x === '') return null
93
94
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
94
- return isNaN(Number(x)) || (!x && x!==0) || x===true? x : Number(x)
95
+ return isNaN(Number(x)) || (!x && x!==0) || x === true? x : Number(x)
95
96
  },
96
97
  fn: function(x) {
97
98
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return true
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.32.3",
5
+ "version": "1.33.0",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -56,8 +56,8 @@ let plugin = module.exports = {
56
56
  model.imageFields = plugin._findAndTransformImageFields(model.fields, '')
57
57
 
58
58
  if (model.imageFields.length) {
59
- // Todo?: Update image fields and whitelists with the new object schema
60
- // model._setupFieldsAndWhitelists(model.fields)
59
+ // Todo?: Update image fields / blacklists with the new object schema
60
+ // model._setupFields(model.fields)/model._getFieldsFlattened(model.fields)
61
61
  model.beforeValidate.push(function(data, n) {
62
62
  plugin.keepImagePlacement(this, data).then(() => n(null, data)).catch(e => n(e))
63
63
  })
@@ -93,8 +93,6 @@ module.exports = function(monastery, opendb) {
93
93
  }
94
94
  }})
95
95
 
96
- // Note: testing mongodb projections
97
-
98
96
  // Test initial blacklist
99
97
  let find1 = await user.findOne({
100
98
  query: user1._id
@@ -122,37 +120,184 @@ module.exports = function(monastery, opendb) {
122
120
  animals: { dog: 'Max', cat: 'Ginger' }
123
121
  })
124
122
 
125
- // Test positive projection
126
- let find3 = await user.findOne({
127
- query: user1._id,
128
- project: ['dog', 'list', 'pets.age']
123
+ db.close()
124
+ })
125
+
126
+ test('find blacklisting population', async () => {
127
+ // inprogresss
128
+ // Setup
129
+ let db = monastery('localhost/monastery', {
130
+ timestamps: false,
131
+ serverSelectionTimeoutMS: 2000,
129
132
  })
130
- expect(find3).toEqual({
131
- _id: user1._id,
132
- dog: 'Bruce',
133
- list: [44, 54],
134
- pets: [{ age: 5 }, { age: 4 }]
133
+ let bird = db.model('bird', {
134
+ fields: {
135
+ color: { type: 'string', default: 'red' },
136
+ height: { type: 'number' },
137
+ name: { type: 'string' },
138
+ sub: {
139
+ color: { type: 'string', default: 'red' },
140
+ },
141
+ subs: [{
142
+ color: { type: 'string', default: 'red'},
143
+ }],
144
+ wing: {
145
+ size: { type: 'number' },
146
+ sizes: {
147
+ one: { type: 'number' },
148
+ two: { type: 'number' },
149
+ }
150
+ },
151
+ },
152
+ findBL: ['wing']
135
153
  })
136
-
137
- // Test negative projection
138
- let find5 = await user.findOne({
139
- query: user1._id,
140
- project: [
141
- '-list', '-hiddenDeepModel', '-pet', '-pet', '-pets', '-deep', '-deeper', '-deepModel',
142
- '-dog', '-animals.cat'
154
+ let user = db.model('user', {
155
+ fields: {
156
+ dog: { type: 'string' },
157
+ bird1: { model: 'bird' },
158
+ bird2: { model: 'bird' },
159
+ bird3: { model: 'bird' },
160
+ bird4: { model: 'bird' },
161
+ bird5: { model: 'bird' },
162
+ },
163
+ findBL: [
164
+ 'bird1.name', // bird1.name & bird1.wing blacklisted
165
+ '-bird2', 'bird2.name', // bird2.name blacklisted
166
+ 'bird3.name', '-bird3', 'bird3.height', // bird3.height blacklisted
167
+ '-bird4.wing.sizes.one', '-bird4.wing.size', // ignored
168
+ // bird4.wing.sizes.two blacklisted (expand in future verion)
169
+ '-bird5.wing.sizes.one', // bird5.wing.sizes.one ignored, wing blacklisted
170
+ // bird5.wing.sizes.two, wing.size blacklisted (expand in future verion)
143
171
  ]
144
172
  })
145
- expect(find5).toEqual({
173
+ let bird1 = await bird.insert({
174
+ data: {
175
+ name: 'ponyo',
176
+ height: 40,
177
+ sub: {},
178
+ wing: { size: 1, sizes: { one: 1, two: 1 }}
179
+ }
180
+ })
181
+ let userData = {
182
+ dog: 'Bruce',
183
+ bird1: bird1._id,
184
+ bird2: bird1._id,
185
+ bird3: bird1._id,
186
+ bird4: bird1._id,
187
+ bird5: bird1._id
188
+ }
189
+ let user1 = await user.insert({ data: userData })
190
+ let bird1Base = { _id: bird1._id, color: 'red', sub: { color: 'red' }}
191
+
192
+ // Test bird1
193
+ expect(await user.findOne({ query: user1._id, populate: ['bird1'] })).toEqual({
194
+ ...userData,
146
195
  _id: user1._id,
147
- animals: { dog: 'Max' },
148
- hiddenList: [12, 23],
149
- hiddenPets: [{ name: 'secretPet' }]
196
+ bird1: { ...bird1Base, height: 40 },
197
+ })
198
+ // Test bird2
199
+ expect(await user.findOne({ query: user1._id, populate: ['bird2'] })).toEqual({
200
+ ...userData,
201
+ _id: user1._id,
202
+ bird2: { ...bird1Base, height: 40, wing: { size: 1, sizes: { one: 1, two: 1 }} },
203
+ })
204
+ // Test bird3
205
+ expect(await user.findOne({ query: user1._id, populate: ['bird3'] })).toEqual({
206
+ ...userData,
207
+ _id: user1._id,
208
+ bird3: { ...bird1Base, name: 'ponyo', wing: { size: 1, sizes: { one: 1, two: 1 }} },
209
+ })
210
+ // Test bird4
211
+ expect(await user.findOne({ query: user1._id, populate: ['bird4'] })).toEqual({
212
+ ...userData,
213
+ _id: user1._id,
214
+ bird4: { ...bird1Base, name: 'ponyo', height: 40 },
215
+ })
216
+ // Test bird5
217
+ expect(await user.findOne({ query: user1._id, populate: ['bird5'] })).toEqual({
218
+ ...userData,
219
+ _id: user1._id,
220
+ bird5: { ...bird1Base, name: 'ponyo', height: 40 },
150
221
  })
151
222
 
152
223
  db.close()
153
224
  })
154
225
 
155
- test('find blacklisting (default fields)', async () => {
226
+ test('find blacklisting getProjection', async () => {
227
+ let db = (await opendb(null)).db
228
+ // Setup
229
+ db.model('bird', {
230
+ fields: {
231
+ age: { type: 'number' },
232
+ name: { type: 'string' },
233
+ wing: {
234
+ size: { type: 'number' },
235
+ },
236
+ },
237
+ findBL: ['age', 'wing']
238
+ })
239
+ db.model('user', {
240
+ fields: {
241
+ name: { type: 'string' },
242
+ bird1: { model: 'bird' },
243
+ },
244
+ })
245
+ // default
246
+ expect(db.user._getBlacklistProjection('find')).toEqual({
247
+ 'bird1.wing': 0,
248
+ 'bird1.age': 0,
249
+ 'password': 0,
250
+ })
251
+ // blacklist /w invalid field (which goes through)
252
+ expect(db.user._getBlacklistProjection('find', ['name', 'invalidfield'])).toEqual({
253
+ 'bird1.wing': 0,
254
+ 'bird1.age': 0,
255
+ 'invalidfield': 0,
256
+ 'name': 0,
257
+ 'password': 0,
258
+ })
259
+ // whitelist
260
+ expect(db.user._getBlacklistProjection('find', ['-password', '-bird1.age'])).toEqual({
261
+ 'bird1.wing': 0,
262
+ })
263
+ // whitelist parent
264
+ expect(db.user._getBlacklistProjection('find', ['-bird1'])).toEqual({
265
+ 'password': 0,
266
+ })
267
+ // whitelist parent, then blacklist child
268
+ expect(db.user._getBlacklistProjection('find', ['-bird1', 'bird1.name'])).toEqual({
269
+ 'password': 0,
270
+ 'bird1.name': 0,
271
+ })
272
+ // the model's blacklists are applied after deep model's
273
+ db.user.findBL = ['-bird1.age']
274
+ expect(db.user._getBlacklistProjection('find')).toEqual({
275
+ 'bird1.wing': 0,
276
+ })
277
+ // custom blacklists are applied after the model's, which are after deep model's
278
+ db.user.findBL = ['-bird1.age']
279
+ expect(db.user._getBlacklistProjection('find', ['bird1'])).toEqual({
280
+ 'bird1': 0,
281
+ })
282
+ // blacklisted parent with a blacklisted child
283
+ expect(db.user._getBlacklistProjection('find', ['bird1', 'bird1.wing'])).toEqual({
284
+ 'bird1': 0,
285
+ })
286
+ // A mess of things
287
+ expect(db.user._getBlacklistProjection('find', ['-bird1', 'bird1.wing', '-bird1.wing','bird1.wing.size'])).toEqual({
288
+ 'bird1.wing.size': 0,
289
+ })
290
+ // blacklisted parent with a whitelisted child (expect blacklist expansion in future version?)
291
+ // expect(db.user._getBlacklistProjection('find', ['bird1', '-bird1.wing'])).toEqual({
292
+ // 'bird1.age': 0,
293
+ // 'bird1.name': 0,
294
+ // })
295
+
296
+ db.close()
297
+ })
298
+
299
+ test('find project', async () => {
300
+ // Test mongodb native project option
156
301
  // Setup
157
302
  let db = (await opendb(null)).db
158
303
  let user = db.model('user', {
@@ -184,7 +329,6 @@ module.exports = function(monastery, opendb) {
184
329
  let find1 = await user.findOne({
185
330
  query: user1._id,
186
331
  project: ['animal.name', 'animals.name']
187
- //blacklist: ['animal.color', 'animals']
188
332
  })
189
333
  expect(find1).toEqual({
190
334
  _id: user1._id,
@@ -209,97 +353,74 @@ module.exports = function(monastery, opendb) {
209
353
  ]
210
354
  })
211
355
 
212
- // Test exclusion blacklist
213
- let find3 = await user.findOne({
214
- query: user1._id,
215
- blacklist: ['animal.color', 'animals', 'color']
216
- })
217
- expect(find3).toEqual({
218
- _id: user1._id,
219
- name: 'Bruce',
220
- animal: { name: 'max' }
221
- })
222
-
223
356
  db.close()
224
357
  })
225
358
 
226
- test('find blacklisting (populate)', async () => {
359
+ test('find project population', async () => {
360
+ // Test mongodb native project option
227
361
  // Setup
228
362
  let db = (await opendb(null)).db
229
363
  let bird = db.model('bird', {
230
364
  fields: {
231
365
  name: { type: 'string' },
232
366
  age: { type: 'number' },
367
+ height: { type: 'number' },
233
368
  color: { type: 'string', default: 'red' },
234
369
  sub: {
235
370
  color: { type: 'string', default: 'red' },
236
371
  }
237
- }
372
+ },
373
+ findBL: ['age']
238
374
  })
239
375
  let user = db.model('user', {
240
376
  fields: {
241
377
  dog: { type: 'string' },
242
- myBird: { model: 'bird' },
243
- myBird2: { model: 'bird' }
378
+ bird: { model: 'bird' },
379
+ bird2: { model: 'bird' },
380
+ bird3: { model: 'bird' }
244
381
  },
382
+ findBL: [
383
+ // allll these should be ignored.....?/////
384
+ 'bird.name', // bird.name & bird.age blacklisted
385
+ '-bird2', 'bird2.name', // bird2.name blacklisted
386
+ 'bird3.name', '-bird3', 'bird3.height', // bird3.height blacklisted
387
+ ]
245
388
  })
246
389
  let bird1 = await bird.insert({ data: {
247
390
  name: 'ponyo',
248
391
  age: 3,
392
+ height: 40,
249
393
  sub: {}
250
394
  }})
251
395
  let user1 = await user.insert({ data: {
252
396
  dog: 'Bruce',
253
- myBird: bird1._id,
254
- myBird2: bird1._id
397
+ bird: bird1._id,
398
+ bird2: bird1._id,
399
+ bird3: bird1._id
255
400
  }})
256
401
 
257
402
  // Test project
258
403
  let find1 = await user.findOne({
259
404
  query: user1._id,
260
- populate: ['myBird', 'myBird2'],
261
- project: ['myBird.age', 'myBird2']
405
+ populate: ['bird', 'bird2'],
406
+ project: ['bird.age', 'bird2']
262
407
  })
263
408
  expect(find1).toEqual({
264
409
  _id: user1._id,
265
- myBird: { age: 3 },
266
- myBird2: { _id: bird1._id, age: 3, name: 'ponyo', color: 'red', sub: { color: 'red' }},
410
+ bird: { age: 3 },
411
+ bird2: { _id: bird1._id, age: 3, name: 'ponyo', height: 40, color: 'red', sub: { color: 'red' }}
267
412
  })
268
413
 
269
414
  // Test project (different project details)
270
415
  let find2 = await user.findOne({
271
416
  query: user1._id,
272
- populate: ['myBird', 'myBird2'],
273
- project: ['myBird', 'myBird2.age']
417
+ populate: ['bird', 'bird2'],
418
+ project: ['bird', 'bird2.height']
274
419
  })
275
420
  expect(find2).toEqual({
276
421
  _id: user1._id,
277
- myBird: { _id: bird1._id, age: 3, name: 'ponyo', color: 'red', sub: { color: 'red' }},
278
- myBird2: { age: 3 },
279
- })
280
-
281
- // Test blacklisting
282
- let find22 = await user.findOne({
283
- query: user1._id,
284
- populate: ['myBird', 'myBird2'],
285
- blacklist: ['dog', 'myBird2.name', 'myBird2._id']
286
- })
287
- expect(find22).toEqual({
288
- _id: user1._id,
289
- myBird: { _id: bird1._id, age: 3, name: 'ponyo', color: 'red', sub: { color: 'red' }},
290
- myBird2: { age: 3, color: 'red', sub: { color: 'red' }}
291
- })
292
-
293
- // Test blacklisting overrides
294
- let find3 = await user.findOne({
295
- query: user1._id,
296
- populate: ['myBird', 'myBird2'],
297
- blacklist: ['dog', 'myBird2.name', 'myBird2._id', '-myBird2']
298
- })
299
- expect(find3).toEqual({
300
- _id: user1._id,
301
- myBird: { _id: bird1._id, age: 3, name: 'ponyo', color: 'red', sub: { color: 'red' }},
302
- myBird2: { _id: bird1._id, age: 3, name: 'ponyo', color: 'red', sub: { color: 'red' }}
422
+ bird: { _id: bird1._id, age: 3, name: 'ponyo', height: 40, color: 'red', sub: { color: 'red' }},
423
+ bird2: { height: 40 },
303
424
  })
304
425
 
305
426
  db.close()
package/test/crud.js CHANGED
@@ -391,7 +391,10 @@ module.exports = function(monastery, opendb) {
391
391
  expect(find1).toEqual({
392
392
  _id: inserted2._id,
393
393
  name: 'Martin Luther',
394
- addresses: [{ city: 'Frankfurt', country: 'Germany' }, { city: 'Christchurch', country: 'New Zealand' }],
394
+ addresses: [
395
+ { city: 'Frankfurt', country: 'Germany' },
396
+ { city: 'Christchurch', country: 'New Zealand' }
397
+ ],
395
398
  address: { country: 'Germany' },
396
399
  pet: { dog: { _id: inserted._id, name: 'Scruff', user: inserted2._id }},
397
400
  dogs: [{ _id: inserted._id, name: 'Scruff', user: inserted2._id }]
@@ -407,6 +410,7 @@ module.exports = function(monastery, opendb) {
407
410
  as: 'dogs'
408
411
  }],
409
412
  blacklist: ['address', 'addresses.country', 'dogs.name']
413
+ // ^ great test (address should cancel addresses if not stopping at the .)
410
414
  })
411
415
  expect(find2).toEqual({
412
416
  _id: inserted2._id,
package/test/model.js CHANGED
@@ -411,169 +411,4 @@ module.exports = function(monastery, opendb) {
411
411
  db.close()
412
412
  })
413
413
 
414
- test('model findBL findBLProject', async () => {
415
- let db = (await opendb(null)).db
416
- db.model('bird', { fields: {
417
- name: { type: 'string' }
418
- }})
419
- let user = db.model('user', {
420
- findBL: [
421
- 'pets.hiddenAge',
422
- 'animals.hiddenCat',
423
- 'hiddenDog',
424
- 'hiddenPets',
425
- 'hiddenList',
426
- 'deep.deep2.hiddenDeep3',
427
- 'hiddenDeeper',
428
- 'hiddenDeepModel',
429
- 'hiddenDeepModels'
430
- ],
431
- fields: {
432
- list: [{ type: 'number' }],
433
- pet: { type: 'string' },
434
- anyPet: { type: 'any' },
435
- pets: [{ name: { type: 'string'}, hiddenAge: { type: 'number'} }],
436
- animals: {
437
- dog: { type: 'string' },
438
- hiddenCat: { type: 'string' }
439
- },
440
- deep: {
441
- deep2: {
442
- hiddenDeep3: {
443
- deep4: { type: 'string' }
444
- }
445
- }
446
- },
447
- deepModel: {
448
- myBird: { model: 'bird' }
449
- },
450
- deepModel2: {
451
- myBird: { model: 'bird' }
452
- },
453
- hiddenDog: { type: 'string' },
454
- hiddenPets: [{
455
- name: { type: 'string'}
456
- }],
457
- hiddenList: [{ type: 'number'}],
458
- hiddenDeeper: {
459
- deeper2: {
460
- deeper3: {
461
- deeper4: { type: 'string' }
462
- }
463
- }
464
- },
465
- hiddenDeepModel: {
466
- myBird: { model: 'bird' }
467
- },
468
- hiddenDeepModels: [{ model: 'bird' }]
469
- }
470
- })
471
-
472
- // Test find whitelist
473
- // expect(user.fieldlist.filter(o => !user.findBL.includes(o))).toEqual([
474
- // 'list',
475
- // 'pet',
476
- // 'anyPet',
477
- // 'pets.name',
478
- // 'animals.dog',
479
- // 'deepModel.myBird',
480
- // 'deepModel2.myBird'
481
- // ])
482
-
483
- // Test findBLProject
484
- expect(user.findBLProject).toEqual({
485
- 'pets.hiddenAge': 0,
486
- 'animals.hiddenCat': 0,
487
- 'hiddenDog': 0,
488
- 'hiddenPets': 0,
489
- 'hiddenList': 0,
490
- 'deep.deep2.hiddenDeep3': 0,
491
- 'hiddenDeeper': 0,
492
- 'hiddenDeepModel': 0,
493
- 'hiddenDeepModels': 0
494
- })
495
-
496
- // Test inclusion of deep model blacklists
497
- var findBLProject = user._addDeepBlacklists(user.findBLProject, [
498
- 'deepModel.myBird',
499
- 'hiddenDeepModel.myBird',
500
- 'hiddenDeepModels',
501
- {
502
- // raw $lookup object
503
- as: 'deepModel2.myBird'
504
- }
505
- ])
506
- expect(findBLProject).toEqual({
507
- 'pets.hiddenAge': 0,
508
- 'animals.hiddenCat': 0,
509
- 'hiddenDog': 0,
510
- 'hiddenPets': 0,
511
- 'hiddenList': 0,
512
- 'deep.deep2.hiddenDeep3': 0,
513
- 'hiddenDeeper': 0,
514
- 'hiddenDeepModel': 0,
515
- 'hiddenDeepModels': 0,
516
- // Deep model blacklists
517
- 'deepModel.myBird.password': 0,
518
- 'deepModel2.myBird.password': 0
519
- })
520
-
521
- // Test whitelisting
522
- expect(user._addBlacklist(findBLProject, ['-pets', '-deep.deep2.hiddenDeep3'])).toEqual({
523
- // 'pets.hiddenAge': 0,
524
- // 'deep.deep2.hiddenDeep3': 0,
525
- 'animals.hiddenCat': 0,
526
- 'hiddenDog': 0,
527
- 'hiddenPets': 0,
528
- 'hiddenList': 0,
529
- 'hiddenDeeper': 0,
530
- 'hiddenDeepModel': 0,
531
- 'hiddenDeepModels': 0,
532
- // Deep model blacklists
533
- 'deepModel.myBird.password': 0,
534
- 'deepModel2.myBird.password': 0
535
- })
536
-
537
- // Test aggregate with projection exclusions. This is mainly to test how $lookup reacts
538
- let bird1 = await db.bird.insert({ data: { name: 'bird1' } })
539
- let bird2 = await db.bird.insert({ data: { name: 'bird2' } })
540
- let user1 = await db.user.insert({ data: {
541
- pet: 'carla',
542
- pets: [{ name: 'carla', hiddenAge: 12 }, { name: 'sparky', hiddenAge: 14 }],
543
- anyPet: { hi: 1234 },
544
- hiddenDeepModel: { myBird: bird1._id },
545
- hiddenDeepModels: [bird1._id, bird2._id]
546
- }})
547
- let res = await db.user._aggregate([
548
- { $match: { _id: user1._id } },
549
- { $lookup: {
550
- from: 'bird',
551
- localField: 'hiddenDeepModel.myBird',
552
- foreignField: '_id',
553
- as: 'hiddenDeepModel.myBird'
554
- }},
555
- { $lookup: {
556
- from: 'bird',
557
- localField: 'hiddenDeepModels',
558
- foreignField: '_id',
559
- as: 'hiddenDeepModels'
560
- }},
561
- { $project: {
562
- 'anyPet': 0,
563
- 'pets.hiddenAge': 0,
564
- 'hiddenDeepModel.myBird': 0,
565
- 'hiddenDeepModels.name': 0,
566
- }}
567
- ])
568
- expect(res[0]).toEqual({
569
- _id: user1._id,
570
- pet: 'carla',
571
- pets: [ { name: 'carla' }, { name: 'sparky' } ],
572
- hiddenDeepModel: {},
573
- hiddenDeepModels: [ { _id: bird1._id }, { _id: bird2._id } ]
574
- })
575
-
576
- db.close()
577
- })
578
-
579
414
  }
@@ -161,7 +161,7 @@ module.exports = function(monastery, opendb) {
161
161
  let express = require('express')
162
162
  let upload = require('express-fileupload')
163
163
  let app = express()
164
- app.use(upload({ limits: { filesize: 1 * 1000 * 1000, files: 10 }}))
164
+ app.use(upload({ limits: { fileSize: 1 * 1000 * 1000, files: 10 }}))
165
165
 
166
166
  app.post('/', function(req, res) {
167
167
  // Files exist
@@ -292,7 +292,7 @@ module.exports = function(monastery, opendb) {
292
292
  let express = require('express')
293
293
  let upload = require('express-fileupload')
294
294
  let app = express()
295
- app.use(upload({ limits: { filesize: 1 * 1000 * 1000, files: 10 }}))
295
+ app.use(upload({ limits: { fileSize: 1 * 1000 * 1000, files: 10 }}))
296
296
 
297
297
  app.post('/', function(req, res) {
298
298
  req.body.logos = JSON.parse(req.body.logos)
@@ -368,7 +368,7 @@ module.exports = function(monastery, opendb) {
368
368
  let express = require('express')
369
369
  let upload = require('express-fileupload')
370
370
  let app = express()
371
- app.use(upload({ limits: { filesize: 1 * 1000 * 1000, files: 10 }}))
371
+ app.use(upload({ limits: { fileSize: 1 * 1000 * 1000, files: 10 }}))
372
372
 
373
373
  app.post('/', function(req, res) {
374
374
  try {
@@ -426,7 +426,7 @@ module.exports = function(monastery, opendb) {
426
426
  let express = require('express')
427
427
  let upload = require('express-fileupload')
428
428
  let app = express()
429
- app.use(upload({ limits: { filesize: 1000 * 200, files: 10 }}))
429
+ app.use(upload({ limits: { fileSize: 1000 * 200, files: 10 }}))
430
430
 
431
431
  app.post('/', async (req, res) => {
432
432
  let imageSvgBad = { imageSvgBad: req.files.imageSvgBad }
package/test/validate.js CHANGED
@@ -1,3 +1,5 @@
1
+ // Todo: split out basic 'type' tests
2
+
1
3
  let validate = require('../lib/model-validate')
2
4
 
3
5
  module.exports = function(monastery, opendb) {
@@ -51,21 +53,55 @@ module.exports = function(monastery, opendb) {
51
53
  meta: { rule: 'isString', model: 'user', field: 'name' }
52
54
  })
53
55
 
56
+
54
57
  // Type error (date)
55
- await expect(user.validate({ name: 'a', date: null })).resolves.toEqual({ name: 'a', date: null })
56
- await expect(user.validate({ name: 'a', date: 'fe' })).rejects.toContainEqual({
58
+ let userdate = db.model('userdate', { fields: { amount: { type: 'date', required: true }}})
59
+ let userdate2 = db.model('userdate2', { fields: { amount: { type: 'date' }}})
60
+ await expect(userdate.validate({ amount: 0 })).resolves.toEqual({ amount: 0 })
61
+ await expect(userdate.validate({ amount: '0' })).resolves.toEqual({ amount: 0 })
62
+ await expect(userdate.validate({ amount: '1646778655000' })).resolves.toEqual({ amount: 1646778655000 })
63
+ await expect(userdate2.validate({ amount: '' })).resolves.toEqual({ amount: null })
64
+ await expect(userdate2.validate({ amount: null })).resolves.toEqual({ amount: null })
65
+ await expect(userdate.validate({ amount: 'badnum' })).rejects.toEqual([{
57
66
  status: '400',
58
- title: 'date',
67
+ title: 'amount',
59
68
  detail: 'Value was not a unix timestamp.',
60
- meta: { rule: 'isDate', model: 'user', field: 'date' }
61
- })
69
+ meta: { rule: 'isDate', model: 'userdate', field: 'amount' }
70
+ }])
71
+ await expect(userdate.validate({ amount: false })).rejects.toEqual([{
72
+ status: '400',
73
+ title: 'amount',
74
+ detail: 'Value was not a unix timestamp.',
75
+ meta: { rule: 'isDate', model: 'userdate', field: 'amount' }
76
+ }])
77
+ await expect(userdate.validate({ amount: undefined })).rejects.toEqual([{
78
+ status: '400',
79
+ title: 'amount',
80
+ detail: 'This field is required.',
81
+ meta: { rule: 'required', model: 'userdate', field: 'amount' },
82
+ }])
83
+ await expect(userdate.validate({ amount: null })).rejects.toEqual([{
84
+ status: '400',
85
+ title: 'amount',
86
+ detail: 'This field is required.',
87
+ meta: { rule: 'required', model: 'userdate', field: 'amount' },
88
+ }])
89
+
62
90
 
63
91
  // Type error (number)
64
92
  let usernum = db.model('usernum', { fields: { amount: { type: 'number', required: true }}})
65
93
  let usernum2 = db.model('usernum2', { fields: { amount: { type: 'number' }}})
66
94
  await expect(usernum.validate({ amount: 0 })).resolves.toEqual({ amount: 0 })
67
95
  await expect(usernum.validate({ amount: '0' })).resolves.toEqual({ amount: 0 })
96
+ await expect(usernum.validate({ amount: '1646778655000' })).resolves.toEqual({ amount: 1646778655000 })
68
97
  await expect(usernum2.validate({ amount: '' })).resolves.toEqual({ amount: null })
98
+ await expect(usernum2.validate({ amount: null })).resolves.toEqual({ amount: null })
99
+ await expect(usernum.validate({ amount: 'badnum' })).rejects.toEqual([{
100
+ status: '400',
101
+ title: 'amount',
102
+ detail: 'Value was not a number.',
103
+ meta: { rule: 'isNumber', model: 'usernum', field: 'amount' }
104
+ }])
69
105
  await expect(usernum.validate({ amount: false })).rejects.toEqual([{
70
106
  status: '400',
71
107
  title: 'amount',