monastery 1.28.5 → 1.30.2

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
File without changes
package/docs/errors.md CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/docs/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ![](./assets/imgs/monastery.jpg)
2
2
 
3
- [![NPM](https://img.shields.io/npm/v/monastery.svg)](https://www.npmjs.com/package/monastery) [![Build Status](https://travis-ci.com/boycce/monastery.svg?branch=master)](https://travis-ci.com/boycce/monastery)
3
+ [![NPM](https://img.shields.io/npm/v/monastery.svg)](https://www.npmjs.com/package/monastery) [![Build Status](https://travis-ci.com/boycce/monastery.svg?branch=master)](https://app.travis-ci.com/github/boycce/monastery)
4
4
 
5
5
  ## Features
6
6
 
package/docs/rules.md CHANGED
File without changes
package/lib/index.js CHANGED
File without changes
package/lib/model-crud.js CHANGED
@@ -9,7 +9,7 @@ module.exports = {
9
9
  * @param {object|array} <opts.data> - documents to insert
10
10
  * @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove all blacklisting
11
11
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
12
- * @param {array|string|true} ignoreUndefined - ignore all required fields during insert, or
12
+ * @param {array|string|false} validateUndefined - ignore all required fields during insert, or
13
13
  * undefined subdocument required fields that have a defined parent/grandparent during update
14
14
  * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
15
15
  * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
@@ -23,7 +23,7 @@ module.exports = {
23
23
  opts.model = this
24
24
  let data = opts.data = opts.data || (opts.req? opts.req.body : {})
25
25
  let options = util.omit(opts, [
26
- 'data', 'insert', 'model', 'respond', 'ignoreUndefined', 'skipValidation', 'blacklist'
26
+ 'data', 'insert', 'model', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'
27
27
  ])
28
28
  if (cb && !util.isFunction(cb)) {
29
29
  throw new Error(`The callback passed to ${this.name}.insert() is not a function`)
@@ -194,7 +194,7 @@ module.exports = {
194
194
  * @param {object} <opts.query> - mongodb query object
195
195
  * @param {object|array} <opts.data> - mongodb document update object(s)
196
196
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
197
- * @param {array|string|true} ignoreUndefined - ignore all required fields during insert, or
197
+ * @param {array|string|false} validateUndefined - ignore all required fields during insert, or
198
198
  * undefined subdocument required fields that have a defined parent/grandparent during update
199
199
  * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
200
200
  * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
@@ -215,7 +215,7 @@ module.exports = {
215
215
  data = opts.data = opts.data || (opts.req? opts.req.body : null)
216
216
  operators = util.pluck(opts, [/^\$/])
217
217
  // Operation options
218
- options = util.omit(opts, ['data', 'query', 'respond', 'ignoreUndefined', 'skipValidation', 'blacklist'])
218
+ options = util.omit(opts, ['data', 'query', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'])
219
219
  options.sort = options.sort || { 'createdAt': -1 }
220
220
  options.limit = parseInt(options.limit || 0)
221
221
  // Sort string passed
@@ -12,7 +12,7 @@ module.exports = {
12
12
  * @param {boolean(false)} update - are we validating for insert or update?
13
13
  * @param {array|string|false} blacklist - augment schema blacklist, `false` will remove all blacklisting
14
14
  * @param {array|string} projection - only return these fields, ignores blacklist
15
- * @param {array|string|true} ignoreUndefined - ignore all required fields during insert, or undefined
15
+ * @param {array|string|false} validateUndefined - ignore all required fields during insert, or undefined
16
16
  * subdocument required fields that have a defined parent/grandparent during update
17
17
  * @param {array|string|true} skipValidation - skip validation on these fields
18
18
  * @param {boolean} timestamps - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
@@ -177,7 +177,11 @@ module.exports = {
177
177
  // Object schema errors
178
178
  errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
179
179
  // Recurse if data value is a subdocument, or when inserting, or when updating deep properties (non-root)
180
- if (util.isObject(value) || opts.insert || ((path2||'').match(/\./) && !opts.ignoreUndefined)) {
180
+ if (
181
+ util.isObject(value) ||
182
+ opts.insert ||
183
+ ((path2||'').match(/\./) && (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : true))
184
+ ) {
181
185
  var res = this._validateFields(dataRoot, field, value, opts, path2)
182
186
  errors.push(...res[0])
183
187
  }
@@ -213,6 +217,7 @@ module.exports = {
213
217
  * @param {object} dataRoot - data
214
218
  * @param {object} field - field schema
215
219
  * @param {string} path - full field path
220
+ * @param {object} opts - original validate() options
216
221
  * @this model
217
222
  * @return {array} errors
218
223
  */
@@ -242,38 +247,32 @@ module.exports = {
242
247
 
243
248
  for (let ruleName in field) {
244
249
  if (this._ignoredRules.indexOf(ruleName) > -1) continue
245
- // ignore undefined updated root properties, or
246
- if (util.isUndefined(value) && ((opts.update && !path.match(/\./)) || opts.ignoreUndefined)) continue
247
- let error = this._validateRule(dataRoot, ruleName, field, field[ruleName], value, path)
250
+ let error = this._validateRule(dataRoot, ruleName, field, field[ruleName], value, opts, path)
248
251
  if (error && ruleName == 'required') return [error] // only show the required error
249
252
  if (error) errors.push(error)
250
253
  }
251
254
  return errors
252
255
  },
253
256
 
254
- _validateRule: function(dataRoot, ruleName, field, ruleArg, value, path) {
255
- //this.debug(path, field, ruleName, ruleArg, value)
257
+ _validateRule: function(dataRoot, ruleName, field, ruleArg, value, opts, path) {
258
+ // this.debug(path, field, ruleName, ruleArg, value)
256
259
  // Remove [] from the message path, and simply ignore non-numeric children to test for all array items
257
260
  ruleArg = ruleArg === true? undefined : ruleArg
258
261
  let rule = this.rules[ruleName] || rules[ruleName]
259
262
  let fieldName = path.match(/[^.]+$/)[0]
260
263
  let ruleMessageKey = this._getMostSpecificKeyMatchingPath(this.messages, path)
261
264
  let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
265
+ let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : rule.validateUndefined
262
266
  if (!ruleMessage) ruleMessage = rule.message
263
267
 
264
- if (ruleName !== 'required') {
265
- // Ignore undefined when not testing 'required'
266
- if (typeof value === 'undefined') return
268
+ // Ignore undefined (if updated root property, or ignoring)
269
+ if ((!validateUndefined || (opts.update && !path.match(/\./))) && typeof value === 'undefined') return
267
270
 
268
- // Ignore null if not testing required
269
- if (value === null && !field.isObject && !field.isArray) return
270
-
271
- // Ignore null if nullObject is set on objects or arrays
272
- if (value === null && field.nullObject) return
273
- }
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
274
273
 
275
274
  // Ignore empty strings
276
- if (value === '' && rule.ignoreEmptyString) return
275
+ if (!rule.validateEmptyString && value === '') return
277
276
 
278
277
  // Rule failed
279
278
  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.validateEmptyString == 'undefined') formattedRule.validateEmptyString = true
64
65
  this.rules[ruleName] = formattedRule
65
66
  }
66
67
  }, this)
@@ -241,11 +242,10 @@ Model.prototype._setupFieldsAndWhitelists = function(fields, path) {
241
242
  this.findBLProject = this.findBL.reduce((o, v) => { (o[v] = 0); return o }, {})
242
243
  },
243
244
 
244
- Model.prototype._setupIndexes = function(fields) {
245
+ Model.prototype._setupIndexes = function(fields, opts={}) {
245
246
  /**
246
- * Creates indexes for the model
247
+ * Creates indexes for the model (multikey, and sub-document supported)
247
248
  * Note: only one text index per model(collection) is allowed due to mongodb limitations
248
- * Note: we and currently don't support indexes on sub-collections, but sub-documents yes!
249
249
  * @link https://docs.mongodb.com/manual/reference/command/createIndexes/
250
250
  * @link https://mongodb.github.io/node-mongodb-native/2.1/api/Collection.html#createIndexes
251
251
  * @param {object} <fields>
@@ -278,6 +278,7 @@ Model.prototype._setupIndexes = function(fields) {
278
278
  recurseFields(fields || model.fields, '')
279
279
  // console.log(2, indexes, fields)
280
280
  if (hasTextIndex) indexes.push(textIndex)
281
+ if (opts.dryRun) return Promise.resolve(indexes || [])
281
282
  if (!indexes.length) return Promise.resolve([]) // No indexes defined
282
283
 
283
284
  // Create indexes
@@ -330,22 +331,24 @@ Model.prototype._setupIndexes = function(fields) {
330
331
  util.forEach(fields, (field, name) => {
331
332
  let index = field.index
332
333
  if (index) {
333
- let path = name == 'schema'? parentPath.slice(0, -1) : parentPath + name
334
334
  let options = util.isObject(index)? util.omit(index, ['type']) : {}
335
335
  let type = util.isObject(index)? index.type : index
336
+ let path = name == 'schema'? parentPath.slice(0, -1) : parentPath + name
337
+ let path2 = path.replace(/(^|\.)[0-9]+(\.|$)/g, '$2') // no numirical keys, e.g. pets.1.name
336
338
  if (type === true) type = 1
337
-
338
339
  if (type == 'text') {
339
- hasTextIndex = textIndex.key[path] = 'text'
340
+ hasTextIndex = textIndex.key[path2] = 'text'
340
341
  Object.assign(textIndex, options)
341
342
  } else if (type == '1' || type == '-1' || type == '2dsphere') {
342
- indexes.push({ name: `${path}_${type}`, key: { [path]: type }, ...options })
343
+ indexes.push({ name: `${path2}_${type}`, key: { [path2]: type }, ...options })
343
344
  } else if (type == 'unique') {
344
- indexes.push({ name: `${path}_1`, key: { [path]: 1 }, unique: true, ...options })
345
+ indexes.push({ name: `${path2}_1`, key: { [path2]: 1 }, unique: true, ...options })
345
346
  }
346
347
  }
347
348
  if (util.isSubdocument(field)) {
348
349
  recurseFields(field, parentPath + name + '.')
350
+ } else if (util.isArray(field)) {
351
+ recurseFields(field, parentPath + name + '.')
349
352
  }
350
353
  })
351
354
  }
package/lib/rules.js CHANGED
@@ -5,6 +5,8 @@ let validator = require('validator')
5
5
  module.exports = {
6
6
 
7
7
  required: {
8
+ validateUndefined: true,
9
+ validateEmptyString: true,
8
10
  message: 'This field is required.',
9
11
  fn: function(x) {
10
12
  if (util.isArray(x) && !x.length) return false
@@ -12,9 +14,10 @@ module.exports = {
12
14
  }
13
15
  },
14
16
 
15
- // Rules below ignore null
17
+ // Type rules below ignore undefined (default for custom model rules)
16
18
 
17
19
  'isBoolean': {
20
+ validateEmptyString: true,
18
21
  message: 'Value was not a boolean.',
19
22
  tryParse: function(x) {
20
23
  if (typeof x === 'string' && x === 'true') return true
@@ -25,13 +28,8 @@ module.exports = {
25
28
  return typeof x === 'boolean'
26
29
  }
27
30
  },
28
- 'isNotEmptyString': {
29
- message: 'Value was an empty string.',
30
- fn: function(x) {
31
- return x !== ''
32
- }
33
- },
34
31
  'isArray': {
32
+ validateEmptyString: true,
35
33
  message: 'Value was not an array.',
36
34
  tryParse: function(x) {
37
35
  if (x === '') return null
@@ -44,6 +42,7 @@ module.exports = {
44
42
  }
45
43
  },
46
44
  'isDate': {
45
+ validateEmptyString: true,
47
46
  message: 'Value was not a unix timestamp.',
48
47
  tryParse: function(x) {
49
48
  if (util.isString(x) && x.match(/^[+-]?[0-9]+$/)) return x // keep string nums intact
@@ -55,6 +54,7 @@ module.exports = {
55
54
  }
56
55
  },
57
56
  'isImageObject': {
57
+ validateEmptyString: true,
58
58
  message: 'Invalid image value',
59
59
  messageLong: 'Image fields need to either be null, undefined, file, or an object containing the following '
60
60
  + 'fields \'{ bucket, date, filename, filesize, path, uid }\'',
@@ -70,6 +70,7 @@ module.exports = {
70
70
  }
71
71
  },
72
72
  'isInteger': {
73
+ validateEmptyString: true,
73
74
  message: 'Value was not an integer.',
74
75
  tryParse: function(x) {
75
76
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
@@ -81,6 +82,7 @@ module.exports = {
81
82
  }
82
83
  },
83
84
  'isNumber': {
85
+ validateEmptyString: true,
84
86
  message: 'Value was not a number.',
85
87
  tryParse: function(x) {
86
88
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
@@ -92,6 +94,7 @@ module.exports = {
92
94
  }
93
95
  },
94
96
  'isObject': {
97
+ validateEmptyString: true,
95
98
  message: 'Value was not an object.',
96
99
  tryParse: function(x) {
97
100
  if (x === '') return null
@@ -104,18 +107,21 @@ module.exports = {
104
107
  }
105
108
  },
106
109
  'isString': {
110
+ validateEmptyString: true,
107
111
  message: 'Value was not a string.',
108
112
  fn: function(x) {
109
113
  return typeof x === 'string'
110
114
  }
111
115
  },
112
116
  'isAny': {
117
+ validateEmptyString: true,
113
118
  message: '',
114
119
  fn: function(x) {
115
120
  return true
116
121
  }
117
122
  },
118
123
  'isId': {
124
+ validateEmptyString: true,
119
125
  message: 'Value was not a valid ObjectId.',
120
126
  tryParse: function(x) {
121
127
  // Try and parse value to a mongodb ObjectId
@@ -130,6 +136,7 @@ module.exports = {
130
136
  }
131
137
  },
132
138
  'max': {
139
+ validateEmptyString: true,
133
140
  message: (x, arg) => 'Value was greater than the configured maximum (' + arg + ')',
134
141
  fn: function(x, arg) {
135
142
  if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
@@ -137,16 +144,25 @@ module.exports = {
137
144
  }
138
145
  },
139
146
  'min': {
147
+ validateEmptyString: true,
140
148
  message: (x, arg) => 'Value was less than the configured minimum (' + arg + ')',
141
149
  fn: function(x, arg) {
142
150
  if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
143
151
  return x >= arg
144
152
  }
145
153
  },
154
+ 'isNotEmptyString': {
155
+ validateEmptyString: true,
156
+ message: 'Value was an empty string.',
157
+ fn: function(x) {
158
+ return x !== ''
159
+ }
160
+ },
161
+
162
+ // Rules below ignore undefined, & empty strings
163
+ // (e.g. an empty email field can be saved that isn't required)
146
164
 
147
- // Rules below ignore null & empty strings
148
165
  'enum': {
149
- ignoreEmptyString: true,
150
166
  message: (x, arg) => 'Invalid enum value',
151
167
  fn: function(x, arg) {
152
168
  for (let item of arg) {
@@ -154,62 +170,51 @@ module.exports = {
154
170
  }
155
171
  }
156
172
  },
157
- 'hasAgreed': {
158
- message: (x, arg) => 'Please agree to the terms and conditions.',
159
- fn: function(x, arg) { return !x }
160
- },
173
+ // 'hasAgreed': {
174
+ // message: (x, arg) => 'Please agree to the terms and conditions.',
175
+ // fn: function(x, arg) { return !x }
176
+ // },
161
177
  'isAfter': {
162
- ignoreEmptyString: true,
163
178
  message: (x, arg) => 'Value was before the configured time (' + arg + ')',
164
179
  fn: function(x, arg) { return validator.isAfter(x, arg) }
165
180
  },
166
181
  'isBefore': {
167
- ignoreEmptyString: true,
168
182
  message: (x, arg) => 'Value was after the configured time (' + arg + ')',
169
183
  fn: function(x, arg) { return validator.isBefore(x, arg) }
170
184
  },
171
185
  'isCreditCard': {
172
- ignoreEmptyString: true,
173
186
  message: 'Value was not a valid credit card.',
174
187
  fn: function(x, arg) { return validator.isCreditCard(x, arg) }
175
188
  },
176
189
  'isEmail': {
177
- ignoreEmptyString: true,
178
190
  message: 'Please enter a valid email address.',
179
191
  fn: function(x, arg) { return validator.isEmail(x, arg) }
180
192
  },
181
193
  'isHexColor': {
182
- ignoreEmptyString: true,
183
194
  message: 'Value was not a valid hex color.',
184
195
  fn: function(x, arg) { return validator.isHexColor(x, arg) }
185
196
  },
186
197
  'isIn': {
187
- ignoreEmptyString: true,
188
198
  message: (x, arg) => 'Value was not in the configured whitelist (' + arg.join(', ') + ')',
189
199
  fn: function(x, arg) { return validator.isIn(x, arg) }
190
200
  },
191
201
  'isIP': {
192
- ignoreEmptyString: true,
193
202
  message: 'Value was not a valid IP address.',
194
203
  fn: function(x, arg) { return validator.isIP(x, arg) }
195
204
  },
196
205
  'isNotIn': {
197
- ignoreEmptyString: true,
198
206
  message: (x, arg) => 'Value was in the configured blacklist (' + arg.join(', ') + ')',
199
207
  fn: function(x, arg) { return !validator.isIn(x, arg) }
200
208
  },
201
209
  'isURL': {
202
- ignoreEmptyString: true,
203
210
  message: 'Value was not a valid URL.',
204
211
  fn: function(x, arg) { return validator.isURL(x, arg === true? undefined : arg) }
205
212
  },
206
213
  'isUUID': {
207
- ignoreEmptyString: true,
208
214
  message: 'Value was not a valid UUID.',
209
215
  fn: function(x, arg) { return validator.isUUID(x) }
210
216
  },
211
217
  'minLength': {
212
- ignoreEmptyString: true,
213
218
  message: function(x, arg) {
214
219
  if (typeof x === 'string') return 'Value needs to be at least ' + arg + ' characters long.'
215
220
  else return 'Value needs to contain a minimum of ' + arg + ' items.'
@@ -221,7 +226,6 @@ module.exports = {
221
226
  }
222
227
  },
223
228
  'maxLength': {
224
- ignoreEmptyString: true,
225
229
  message: function(x, arg) {
226
230
  if (typeof x === 'string') return 'Value was longer than the configured maximum length (' + arg + ')'
227
231
  else return 'Value cannot contain more than ' + arg + ' items.'
@@ -233,7 +237,6 @@ module.exports = {
233
237
  }
234
238
  },
235
239
  'regex': {
236
- ignoreEmptyString: true,
237
240
  message: (x, arg) => 'Value did not match the configured regular expression (' + arg + ')',
238
241
  fn: function(x, arg) {
239
242
  if (util.isRegex(arg)) return validator.matches(x, arg)
package/lib/util.js CHANGED
@@ -300,7 +300,7 @@ module.exports = {
300
300
  * @param {boolean} ignoreEmptyArrays - ignore empty arrays
301
301
  * @return obj
302
302
  */
303
- let chunks = path.split('.') // ['pets', '$', 'dog']
303
+ let chunks = path.split('.') // ['pets', '$|0-9', 'dog']
304
304
  let target = obj
305
305
  for (let i=0, l=chunks.length; i<l; i++) {
306
306
  if (l === i+1) { // Last
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.28.5",
5
+ "version": "1.30.2",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",