monastery 1.30.3 → 1.30.4

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.
@@ -266,13 +266,16 @@ module.exports = {
266
266
  if (!ruleMessage) ruleMessage = rule.message
267
267
 
268
268
  // Ignore undefined (if updated root property, or ignoring)
269
- if ((!validateUndefined || (opts.update && !path.match(/\./))) && typeof value === 'undefined') return
269
+ if (typeof value === 'undefined' && (!validateUndefined || (opts.update && !path.match(/\./)))) return
270
270
 
271
- // Ignore null (if nullObject is set on objects or arrays) (todo: change to ignoreNull)
272
- if (field.nullObject && (field.isObject || field.isArray) && value === null) return
271
+ // Ignore null (if nullObject is set on objects or arrays)
272
+ if (value === null && (field.isObject || field.isArray) && field.nullObject) return
273
+
274
+ // Ignore null
275
+ if (value === null && !(field.isObject || field.isArray) && !rule.validateNull) return
273
276
 
274
277
  // Ignore empty strings
275
- if (!rule.validateEmptyString && value === '') return
278
+ if (value === '' && !rule.validateEmptyString) return
276
279
 
277
280
  // Rule failed
278
281
  if (!rule.fn.call(dataRoot, value, ruleArg, path, this)) return {
package/lib/model.js CHANGED
@@ -61,6 +61,7 @@ let Model = module.exports = function(name, opts, manager) {
61
61
  // Update with formatted rule
62
62
  let formattedRule = util.isObject(rule)? rule : { fn: rule }
63
63
  if (!formattedRule.message) formattedRule.message = `Invalid data property for rule "${ruleName}".`
64
+ if (typeof formattedRule.validateNull == 'undefined') formattedRule.validateNull = true
64
65
  if (typeof formattedRule.validateEmptyString == 'undefined') formattedRule.validateEmptyString = true
65
66
  this.rules[ruleName] = formattedRule
66
67
  }
package/lib/rules.js CHANGED
@@ -5,7 +5,8 @@ let validator = require('validator')
5
5
  module.exports = {
6
6
 
7
7
  required: {
8
- validateUndefined: true,
8
+ validateUndefined: true, // (false for custom rules)
9
+ validateNull: true,
9
10
  validateEmptyString: true,
10
11
  message: 'This field is required.',
11
12
  fn: function(x) {
@@ -14,12 +15,13 @@ module.exports = {
14
15
  }
15
16
  },
16
17
 
17
- // Type rules below ignore undefined (default for custom model rules)
18
+ // "Type" rules below ignore undefined and null
18
19
 
19
- 'isBoolean': {
20
+ isBoolean: {
20
21
  validateEmptyString: true,
21
22
  message: 'Value was not a boolean.',
22
23
  tryParse: function(x) {
24
+ if (x === '') return null
23
25
  if (typeof x === 'string' && x === 'true') return true
24
26
  else if (typeof x === 'string' && x === 'false') return false
25
27
  else return x
@@ -28,7 +30,7 @@ module.exports = {
28
30
  return typeof x === 'boolean'
29
31
  }
30
32
  },
31
- 'isArray': {
33
+ isArray: {
32
34
  validateEmptyString: true,
33
35
  message: 'Value was not an array.',
34
36
  tryParse: function(x) {
@@ -41,10 +43,11 @@ module.exports = {
41
43
  return Array.isArray(x)
42
44
  }
43
45
  },
44
- 'isDate': {
46
+ isDate: {
45
47
  validateEmptyString: true,
46
48
  message: 'Value was not a unix timestamp.',
47
49
  tryParse: function(x) {
50
+ if (x === '') return null
48
51
  if (util.isString(x) && x.match(/^[+-]?[0-9]+$/)) return x // keep string nums intact
49
52
  return isNaN(parseInt(x))? x : parseInt(x)
50
53
  },
@@ -53,7 +56,7 @@ module.exports = {
53
56
  return typeof x === 'number' && (parseInt(x) === x)
54
57
  }
55
58
  },
56
- 'isImageObject': {
59
+ isImageObject: {
57
60
  validateEmptyString: true,
58
61
  message: 'Invalid image value',
59
62
  messageLong: 'Image fields need to either be null, undefined, file, or an object containing the following '
@@ -69,10 +72,11 @@ module.exports = {
69
72
  if (isObject && x.bucket && x.date && x.filename && x.filesize && x.path && x.uid) return true
70
73
  }
71
74
  },
72
- 'isInteger': {
75
+ isInteger: {
73
76
  validateEmptyString: true,
74
77
  message: 'Value was not an integer.',
75
78
  tryParse: function(x) {
79
+ if (x === '') return null
76
80
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
77
81
  return isNaN(parseInt(x)) || (!x && x!==0) || x===true? x : parseInt(x)
78
82
  },
@@ -81,10 +85,11 @@ module.exports = {
81
85
  return typeof x === 'number' && (parseInt(x) === x)
82
86
  }
83
87
  },
84
- 'isNumber': {
88
+ isNumber: {
85
89
  validateEmptyString: true,
86
90
  message: 'Value was not a number.',
87
91
  tryParse: function(x) {
92
+ if (x === '') return null
88
93
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
89
94
  return isNaN(Number(x)) || (!x && x!==0) || x===true? x : Number(x)
90
95
  },
@@ -93,7 +98,7 @@ module.exports = {
93
98
  return typeof x === 'number'
94
99
  }
95
100
  },
96
- 'isObject': {
101
+ isObject: {
97
102
  validateEmptyString: true,
98
103
  message: 'Value was not an object.',
99
104
  tryParse: function(x) {
@@ -106,21 +111,25 @@ module.exports = {
106
111
  return x !== null && typeof x === 'object' && !(x instanceof Array)
107
112
  }
108
113
  },
109
- 'isString': {
114
+ isString: {
110
115
  validateEmptyString: true,
111
116
  message: 'Value was not a string.',
117
+ tryParse: function(x) {
118
+ if (typeof x === 'number') return x + ''
119
+ else return x
120
+ },
112
121
  fn: function(x) {
113
122
  return typeof x === 'string'
114
123
  }
115
124
  },
116
- 'isAny': {
125
+ isAny: {
117
126
  validateEmptyString: true,
118
127
  message: '',
119
128
  fn: function(x) {
120
129
  return true
121
130
  }
122
131
  },
123
- 'isId': {
132
+ isId: {
124
133
  validateEmptyString: true,
125
134
  message: 'Value was not a valid ObjectId.',
126
135
  tryParse: function(x) {
@@ -135,34 +144,27 @@ module.exports = {
135
144
  return util.isObject(x) && ObjectId.isValid(x)/*x.get_inc*/? true : false
136
145
  }
137
146
  },
138
- 'max': {
139
- validateEmptyString: true,
147
+
148
+ /* "Number" rules below ignore undefined and null */
149
+
150
+ max: {
140
151
  message: (x, arg) => 'Value was greater than the configured maximum (' + arg + ')',
141
152
  fn: function(x, arg) {
142
153
  if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
143
154
  return x <= arg
144
155
  }
145
156
  },
146
- 'min': {
147
- validateEmptyString: true,
157
+ min: {
148
158
  message: (x, arg) => 'Value was less than the configured minimum (' + arg + ')',
149
159
  fn: function(x, arg) {
150
160
  if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
151
161
  return x >= arg
152
162
  }
153
163
  },
154
- 'isNotEmptyString': {
155
- validateEmptyString: true,
156
- message: 'Value was an empty string.',
157
- fn: function(x) {
158
- return x !== ''
159
- }
160
- },
161
164
 
162
- // Rules below ignore undefined, & empty strings
163
- // (e.g. an empty email field can be saved that isn't required)
165
+ /* "String" rules below ignore undefined, null, and empty strings */
164
166
 
165
- 'enum': {
167
+ enum: {
166
168
  message: (x, arg) => 'Invalid enum value',
167
169
  fn: function(x, arg) {
168
170
  for (let item of arg) {
@@ -170,51 +172,54 @@ module.exports = {
170
172
  }
171
173
  }
172
174
  },
173
- // 'hasAgreed': {
174
- // message: (x, arg) => 'Please agree to the terms and conditions.',
175
- // fn: function(x, arg) { return !x }
176
- // },
177
- 'isAfter': {
175
+ isAfter: {
178
176
  message: (x, arg) => 'Value was before the configured time (' + arg + ')',
179
177
  fn: function(x, arg) { return validator.isAfter(x, arg) }
180
178
  },
181
- 'isBefore': {
179
+ isBefore: {
182
180
  message: (x, arg) => 'Value was after the configured time (' + arg + ')',
183
181
  fn: function(x, arg) { return validator.isBefore(x, arg) }
184
182
  },
185
- 'isCreditCard': {
183
+ isCreditCard: {
186
184
  message: 'Value was not a valid credit card.',
187
185
  fn: function(x, arg) { return validator.isCreditCard(x, arg) }
188
186
  },
189
- 'isEmail': {
187
+ isEmail: {
190
188
  message: 'Please enter a valid email address.',
191
189
  fn: function(x, arg) { return validator.isEmail(x, arg) }
192
190
  },
193
- 'isHexColor': {
191
+ isHexColor: {
194
192
  message: 'Value was not a valid hex color.',
195
193
  fn: function(x, arg) { return validator.isHexColor(x, arg) }
196
194
  },
197
- 'isIn': {
195
+ isIn: {
198
196
  message: (x, arg) => 'Value was not in the configured whitelist (' + arg.join(', ') + ')',
199
197
  fn: function(x, arg) { return validator.isIn(x, arg) }
200
198
  },
201
- 'isIP': {
199
+ isIP: {
202
200
  message: 'Value was not a valid IP address.',
203
201
  fn: function(x, arg) { return validator.isIP(x, arg) }
204
202
  },
205
- 'isNotIn': {
203
+ isNotEmptyString: {
204
+ validateEmptyString: true,
205
+ message: 'Value was an empty string.',
206
+ fn: function(x) {
207
+ return x !== ''
208
+ }
209
+ },
210
+ isNotIn: {
206
211
  message: (x, arg) => 'Value was in the configured blacklist (' + arg.join(', ') + ')',
207
212
  fn: function(x, arg) { return !validator.isIn(x, arg) }
208
213
  },
209
- 'isURL': {
214
+ isURL: {
210
215
  message: 'Value was not a valid URL.',
211
216
  fn: function(x, arg) { return validator.isURL(x, arg === true? undefined : arg) }
212
217
  },
213
- 'isUUID': {
218
+ isUUID: {
214
219
  message: 'Value was not a valid UUID.',
215
220
  fn: function(x, arg) { return validator.isUUID(x) }
216
221
  },
217
- 'minLength': {
222
+ minLength: {
218
223
  message: function(x, arg) {
219
224
  if (typeof x === 'string') return 'Value needs to be at least ' + arg + ' characters long.'
220
225
  else return 'Value needs to contain a minimum of ' + arg + ' items.'
@@ -225,7 +230,7 @@ module.exports = {
225
230
  else return x.length >= arg
226
231
  }
227
232
  },
228
- 'maxLength': {
233
+ maxLength: {
229
234
  message: function(x, arg) {
230
235
  if (typeof x === 'string') return 'Value was longer than the configured maximum length (' + arg + ')'
231
236
  else return 'Value cannot contain more than ' + arg + ' items.'
@@ -236,7 +241,7 @@ module.exports = {
236
241
  else return x.length <= arg
237
242
  }
238
243
  },
239
- 'regex': {
244
+ regex: {
240
245
  message: (x, arg) => 'Value did not match the configured regular expression (' + arg + ')',
241
246
  fn: function(x, arg) {
242
247
  if (util.isRegex(arg)) return validator.matches(x, arg)
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.30.3",
5
+ "version": "1.30.4",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -23,7 +23,8 @@
23
23
  "test-one-example": "jest -t images",
24
24
  "dev": "npm run lint & DEBUG=-monastery:info jest --watchAll --runInBand --verbose=false",
25
25
  "lint": "eslint ./lib ./plugins ./test",
26
- "docs": "cd docs && bundle exec jekyll serve --livereload --livereload-port 4001"
26
+ "docs": "cd docs && bundle exec jekyll serve --livereload --livereload-port 4001",
27
+ "mong": "nodemon resources/mong.js"
27
28
  },
28
29
  "dependencies": {
29
30
  "aws-sdk": "2.1062.0",
@@ -39,6 +40,7 @@
39
40
  "express": "4.17.1",
40
41
  "express-fileupload": "1.2.0",
41
42
  "jest": "27.4.7",
43
+ "nodemon": "2.0.15",
42
44
  "standard-version": "9.3.2",
43
45
  "supertest": "4.0.2"
44
46
  },
package/test/validate.js CHANGED
@@ -2,7 +2,7 @@ let validate = require('../lib/model-validate')
2
2
 
3
3
  module.exports = function(monastery, opendb) {
4
4
 
5
- test('Validation basic errors', async () => {
5
+ test('validation basic errors', async () => {
6
6
  // Setup
7
7
  let db = (await opendb(false)).db
8
8
  let user = db.model('user', { fields: {
@@ -19,7 +19,7 @@ module.exports = function(monastery, opendb) {
19
19
  detail: 'This field is required.',
20
20
  meta: { rule: 'required', model: 'user', field: 'name' }
21
21
  })
22
- await expect(user.validate({ name : '' }, { validateUndefined: false })).rejects.toContainEqual({
22
+ await expect(user.validate({ name : '' })).rejects.toContainEqual({
23
23
  status: '400',
24
24
  title: 'name',
25
25
  detail: 'This field is required.',
@@ -34,7 +34,16 @@ module.exports = function(monastery, opendb) {
34
34
  await expect(user.validate({}, { update: true })).resolves.toEqual({})
35
35
 
36
36
  // Type error (string)
37
- await expect(user.validate({ name: 1 })).rejects.toContainEqual({
37
+ await expect(user.validate({ name: 1 })).resolves.toEqual({ name: '1' })
38
+ await expect(user.validate({ name: 1.123 })).resolves.toEqual({ name: '1.123' })
39
+ await expect(user.validate({ name: undefined }, { validateUndefined: false })).resolves.toEqual({})
40
+ await expect(user.validate({ name: null })).rejects.toContainEqual({
41
+ status: '400',
42
+ title: 'name',
43
+ detail: 'This field is required.',
44
+ meta: { rule: 'required', model: 'user', field: 'name' }
45
+ })
46
+ await expect(user.validate({ name: true })).rejects.toContainEqual({
38
47
  status: '400',
39
48
  title: 'name',
40
49
  detail: 'Value was not a string.',
@@ -42,6 +51,7 @@ module.exports = function(monastery, opendb) {
42
51
  })
43
52
 
44
53
  // Type error (date)
54
+ await expect(user.validate({ name: 'a', date: null })).resolves.toEqual({ name: 'a', date: null })
45
55
  await expect(user.validate({ name: 'a', date: 'fe' })).rejects.toContainEqual({
46
56
  status: '400',
47
57
  title: 'date',
@@ -49,6 +59,30 @@ module.exports = function(monastery, opendb) {
49
59
  meta: { rule: 'isDate', model: 'user', field: 'date' }
50
60
  })
51
61
 
62
+ // Type error (number)
63
+ let usernum = db.model('usernum', { fields: { amount: { type: 'number', required: true }}})
64
+ await expect(usernum.validate({ amount: 0 })).resolves.toEqual({ amount: 0 })
65
+ await expect(usernum.validate({ amount: '0' })).resolves.toEqual({ amount: 0 })
66
+ await expect(usernum.validate({ amount: undefined }, { validateUndefined: false })).resolves.toEqual({})
67
+ await expect(usernum.validate({ amount: false })).rejects.toEqual([{
68
+ status: '400',
69
+ title: 'amount',
70
+ detail: 'Value was not a number.',
71
+ meta: { rule: 'isNumber', model: 'usernum', field: 'amount' }
72
+ }])
73
+ await expect(usernum.validate({ amount: null })).rejects.toEqual([{
74
+ status: '400',
75
+ title: 'amount',
76
+ detail: 'This field is required.',
77
+ meta: { rule: 'required', model: 'usernum', field: 'amount' },
78
+ }])
79
+ await expect(usernum.validate({ amount: null }, { validateUndefined: false })).rejects.toEqual([{
80
+ status: '400',
81
+ title: 'amount',
82
+ detail: 'This field is required.',
83
+ meta: { rule: 'required', model: 'usernum', field: 'amount' },
84
+ }])
85
+
52
86
  // Type error (array)
53
87
  await expect(user.validate({ name: 'a', colors: 1 })).rejects.toContainEqual({
54
88
  status: '400',
@@ -224,7 +258,7 @@ module.exports = function(monastery, opendb) {
224
258
 
225
259
  // Type error within an array (string)
226
260
  await expect(user.validate({
227
- animals: { cats: [1] }
261
+ animals: { cats: [true] }
228
262
  })).rejects.toContainEqual({
229
263
  status: '400',
230
264
  title: 'animals.cats.0',
@@ -239,7 +273,7 @@ module.exports = function(monastery, opendb) {
239
273
  detail: 'This field is required.',
240
274
  meta: { rule: 'required', model: 'user', field: 'color' }
241
275
  }
242
- await expect(user.validate({ animals: { dogs: [{ name: 'sparky', color: 1 }] }}))
276
+ await expect(user.validate({ animals: { dogs: [{ name: 'sparky', color: false }] }}))
243
277
  .rejects.toContainEqual({
244
278
  ...error,
245
279
  detail: 'Value was not a string.',
@@ -623,16 +657,9 @@ module.exports = function(monastery, opendb) {
623
657
  // Ignores invalid data
624
658
  await expect(user.validate({ badprop: true, schema: {} })).resolves.toEqual({})
625
659
 
626
- // Rejects null for non object/array fields
627
- await expect(user.validate({ name: null })).rejects.toEqual([{
628
- 'detail': 'Value was not a string.',
629
- 'meta': {'detailLong': undefined, 'field': 'name', 'model': 'user', 'rule': 'isString'},
630
- 'status':'400',
631
- 'title': 'name'
632
- }])
633
-
634
660
  // String data
635
661
  await expect(user.validate({ name: 'Martin Luther' })).resolves.toEqual({ name: 'Martin Luther' })
662
+ await expect(user.validate({ name: null })).resolves.toEqual({ name: null })
636
663
 
637
664
  // Array data
638
665
  await expect(user.validate({ names: ['blue'] })).resolves.toEqual({ names: ['blue'] })
@@ -652,12 +679,7 @@ module.exports = function(monastery, opendb) {
652
679
 
653
680
  // Subdocument property data (null)
654
681
  await expect(user.validate({ animals: { dog: null }}))
655
- .rejects.toEqual([{
656
- 'detail': 'Value was not a string.',
657
- 'meta': {'detailLong': undefined, 'field': 'dog', 'model': 'user', 'rule': 'isString'},
658
- 'status': '400',
659
- 'title': 'animals.dog',
660
- }])
682
+ .resolves.toEqual({ animals: { dog: null }})
661
683
 
662
684
  // Subdocument property data (unknown data)
663
685
  await expect(user.validate({ animals: { dog: 'sparky', cat: 'grumpy' } }))
@@ -766,15 +788,10 @@ module.exports = function(monastery, opendb) {
766
788
  })
767
789
 
768
790
  // Number valid
769
- await expect(user2.validate({ amount: 0 })).resolves.toEqual({ amount: 0 }) // required
770
- await expect(user.validate({ amount: '0' })).resolves.toEqual({ amount: 0 }) // required
771
- await expect(user.validate({ amount: undefined })).resolves.toEqual({}) // not required
772
- await expect(user.validate({ amount: null })).rejects.toEqual([{ // type error
773
- 'detail': 'Value was not a number.',
774
- 'meta': { 'field': 'amount', 'model': 'user', 'rule': 'isNumber'},
775
- 'status': '400',
776
- 'title': 'amount'
777
- }])
791
+ await expect(user2.validate({ amount: 0 })).resolves.toEqual({ amount: 0 })
792
+ await expect(user2.validate({ amount: '0' })).resolves.toEqual({ amount: 0 })
793
+ await expect(user.validate({ amount: undefined })).resolves.toEqual({})
794
+ await expect(user.validate({ amount: null })).resolves.toEqual({ amount: null })
778
795
 
779
796
  // Number required
780
797
  let mock1 = {