monastery 1.29.0 → 1.30.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/model-crud.js CHANGED
@@ -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
  }
@@ -258,17 +262,17 @@ module.exports = {
258
262
  let fieldName = path.match(/[^.]+$/)[0]
259
263
  let ruleMessageKey = this._getMostSpecificKeyMatchingPath(this.messages, path)
260
264
  let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
261
- let ignoreUndefined = util.isDefined(opts.ignoreUndefined) ? opts.ignoreUndefined : rule.ignoreUndefined
265
+ let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : rule.validateUndefined
262
266
  if (!ruleMessage) ruleMessage = rule.message
263
267
 
264
268
  // Ignore undefined (if updated root property, or ignoring)
265
- if ((ignoreUndefined || (opts.update && !path.match(/\./))) && typeof value === 'undefined') return
269
+ if ((!validateUndefined || (opts.update && !path.match(/\./))) && typeof value === 'undefined') return
266
270
 
267
271
  // Ignore null (if nullObject is set on objects or arrays) (todo: change to ignoreNull)
268
272
  if (field.nullObject && (field.isObject || field.isArray) && value === null) return
269
273
 
270
274
  // Ignore empty strings
271
- if (rule.ignoreEmptyString && value === '') return
275
+ if (!rule.validateEmptyString && value === '') return
272
276
 
273
277
  // Rule failed
274
278
  if (!rule.fn.call(dataRoot, value, ruleArg, path, this)) return {
package/lib/model.js CHANGED
@@ -61,7 +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.ignoreUndefined == 'undefined') formattedRule.ignoreUndefined = true
64
+ if (typeof formattedRule.validateEmptyString == 'undefined') formattedRule.validateEmptyString = true
65
65
  this.rules[ruleName] = formattedRule
66
66
  }
67
67
  }, this)
@@ -242,11 +242,10 @@ Model.prototype._setupFieldsAndWhitelists = function(fields, path) {
242
242
  this.findBLProject = this.findBL.reduce((o, v) => { (o[v] = 0); return o }, {})
243
243
  },
244
244
 
245
- Model.prototype._setupIndexes = function(fields) {
245
+ Model.prototype._setupIndexes = function(fields, opts={}) {
246
246
  /**
247
- * Creates indexes for the model
247
+ * Creates indexes for the model (multikey, and sub-document supported)
248
248
  * Note: only one text index per model(collection) is allowed due to mongodb limitations
249
- * Note: we and currently don't support indexes on sub-collections, but sub-documents yes!
250
249
  * @link https://docs.mongodb.com/manual/reference/command/createIndexes/
251
250
  * @link https://mongodb.github.io/node-mongodb-native/2.1/api/Collection.html#createIndexes
252
251
  * @param {object} <fields>
@@ -279,6 +278,7 @@ Model.prototype._setupIndexes = function(fields) {
279
278
  recurseFields(fields || model.fields, '')
280
279
  // console.log(2, indexes, fields)
281
280
  if (hasTextIndex) indexes.push(textIndex)
281
+ if (opts.dryRun) return Promise.resolve(indexes || [])
282
282
  if (!indexes.length) return Promise.resolve([]) // No indexes defined
283
283
 
284
284
  // Create indexes
@@ -331,22 +331,24 @@ Model.prototype._setupIndexes = function(fields) {
331
331
  util.forEach(fields, (field, name) => {
332
332
  let index = field.index
333
333
  if (index) {
334
- let path = name == 'schema'? parentPath.slice(0, -1) : parentPath + name
335
334
  let options = util.isObject(index)? util.omit(index, ['type']) : {}
336
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
337
338
  if (type === true) type = 1
338
-
339
339
  if (type == 'text') {
340
- hasTextIndex = textIndex.key[path] = 'text'
340
+ hasTextIndex = textIndex.key[path2] = 'text'
341
341
  Object.assign(textIndex, options)
342
342
  } else if (type == '1' || type == '-1' || type == '2dsphere') {
343
- indexes.push({ name: `${path}_${type}`, key: { [path]: type }, ...options })
343
+ indexes.push({ name: `${path2}_${type}`, key: { [path2]: type }, ...options })
344
344
  } else if (type == 'unique') {
345
- indexes.push({ name: `${path}_1`, key: { [path]: 1 }, unique: true, ...options })
345
+ indexes.push({ name: `${path2}_1`, key: { [path2]: 1 }, unique: true, ...options })
346
346
  }
347
347
  }
348
348
  if (util.isSubdocument(field)) {
349
349
  recurseFields(field, parentPath + name + '.')
350
+ } else if (util.isArray(field)) {
351
+ recurseFields(field, parentPath + name + '.')
350
352
  }
351
353
  })
352
354
  }
package/lib/rules.js CHANGED
@@ -5,7 +5,8 @@ let validator = require('validator')
5
5
  module.exports = {
6
6
 
7
7
  required: {
8
- ignoreUndefined: false,
8
+ validateUndefined: true,
9
+ validateEmptyString: true,
9
10
  message: 'This field is required.',
10
11
  fn: function(x) {
11
12
  if (util.isArray(x) && !x.length) return false
@@ -16,7 +17,7 @@ module.exports = {
16
17
  // Type rules below ignore undefined (default for custom model rules)
17
18
 
18
19
  'isBoolean': {
19
- ignoreUndefined: true,
20
+ validateEmptyString: true,
20
21
  message: 'Value was not a boolean.',
21
22
  tryParse: function(x) {
22
23
  if (typeof x === 'string' && x === 'true') return true
@@ -28,7 +29,7 @@ module.exports = {
28
29
  }
29
30
  },
30
31
  'isArray': {
31
- ignoreUndefined: true,
32
+ validateEmptyString: true,
32
33
  message: 'Value was not an array.',
33
34
  tryParse: function(x) {
34
35
  if (x === '') return null
@@ -41,7 +42,7 @@ module.exports = {
41
42
  }
42
43
  },
43
44
  'isDate': {
44
- ignoreUndefined: true,
45
+ validateEmptyString: true,
45
46
  message: 'Value was not a unix timestamp.',
46
47
  tryParse: function(x) {
47
48
  if (util.isString(x) && x.match(/^[+-]?[0-9]+$/)) return x // keep string nums intact
@@ -53,7 +54,7 @@ module.exports = {
53
54
  }
54
55
  },
55
56
  'isImageObject': {
56
- ignoreUndefined: true,
57
+ validateEmptyString: true,
57
58
  message: 'Invalid image value',
58
59
  messageLong: 'Image fields need to either be null, undefined, file, or an object containing the following '
59
60
  + 'fields \'{ bucket, date, filename, filesize, path, uid }\'',
@@ -69,7 +70,7 @@ module.exports = {
69
70
  }
70
71
  },
71
72
  'isInteger': {
72
- ignoreUndefined: true,
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,7 +82,7 @@ module.exports = {
81
82
  }
82
83
  },
83
84
  'isNumber': {
84
- ignoreUndefined: true,
85
+ validateEmptyString: true,
85
86
  message: 'Value was not a number.',
86
87
  tryParse: function(x) {
87
88
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
@@ -93,7 +94,7 @@ module.exports = {
93
94
  }
94
95
  },
95
96
  'isObject': {
96
- ignoreUndefined: true,
97
+ validateEmptyString: true,
97
98
  message: 'Value was not an object.',
98
99
  tryParse: function(x) {
99
100
  if (x === '') return null
@@ -106,21 +107,21 @@ module.exports = {
106
107
  }
107
108
  },
108
109
  'isString': {
109
- ignoreUndefined: true,
110
+ validateEmptyString: true,
110
111
  message: 'Value was not a string.',
111
112
  fn: function(x) {
112
113
  return typeof x === 'string'
113
114
  }
114
115
  },
115
116
  'isAny': {
116
- ignoreUndefined: true,
117
+ validateEmptyString: true,
117
118
  message: '',
118
119
  fn: function(x) {
119
120
  return true
120
121
  }
121
122
  },
122
123
  'isId': {
123
- ignoreUndefined: true,
124
+ validateEmptyString: true,
124
125
  message: 'Value was not a valid ObjectId.',
125
126
  tryParse: function(x) {
126
127
  // Try and parse value to a mongodb ObjectId
@@ -135,7 +136,7 @@ module.exports = {
135
136
  }
136
137
  },
137
138
  'max': {
138
- ignoreUndefined: true,
139
+ validateEmptyString: true,
139
140
  message: (x, arg) => 'Value was greater than the configured maximum (' + arg + ')',
140
141
  fn: function(x, arg) {
141
142
  if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
@@ -143,7 +144,7 @@ module.exports = {
143
144
  }
144
145
  },
145
146
  'min': {
146
- ignoreUndefined: true,
147
+ validateEmptyString: true,
147
148
  message: (x, arg) => 'Value was less than the configured minimum (' + arg + ')',
148
149
  fn: function(x, arg) {
149
150
  if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
@@ -151,19 +152,17 @@ module.exports = {
151
152
  }
152
153
  },
153
154
  'isNotEmptyString': {
154
- ignoreUndefined: true,
155
+ validateEmptyString: true,
155
156
  message: 'Value was an empty string.',
156
157
  fn: function(x) {
157
158
  return x !== ''
158
159
  }
159
160
  },
160
161
 
161
- // Rules below ignore undefined & empty strings
162
+ // Rules below ignore undefined, & empty strings
162
163
  // (e.g. an empty email field can be saved that isn't required)
163
164
 
164
165
  'enum': {
165
- ignoreEmptyString: true,
166
- ignoreUndefined: true,
167
166
  message: (x, arg) => 'Invalid enum value',
168
167
  fn: function(x, arg) {
169
168
  for (let item of arg) {
@@ -176,68 +175,46 @@ module.exports = {
176
175
  // fn: function(x, arg) { return !x }
177
176
  // },
178
177
  'isAfter': {
179
- ignoreEmptyString: true,
180
- ignoreUndefined: true,
181
178
  message: (x, arg) => 'Value was before the configured time (' + arg + ')',
182
179
  fn: function(x, arg) { return validator.isAfter(x, arg) }
183
180
  },
184
181
  'isBefore': {
185
- ignoreEmptyString: true,
186
- ignoreUndefined: true,
187
182
  message: (x, arg) => 'Value was after the configured time (' + arg + ')',
188
183
  fn: function(x, arg) { return validator.isBefore(x, arg) }
189
184
  },
190
185
  'isCreditCard': {
191
- ignoreEmptyString: true,
192
- ignoreUndefined: true,
193
186
  message: 'Value was not a valid credit card.',
194
187
  fn: function(x, arg) { return validator.isCreditCard(x, arg) }
195
188
  },
196
189
  'isEmail': {
197
- ignoreEmptyString: true,
198
- ignoreUndefined: true,
199
190
  message: 'Please enter a valid email address.',
200
191
  fn: function(x, arg) { return validator.isEmail(x, arg) }
201
192
  },
202
193
  'isHexColor': {
203
- ignoreEmptyString: true,
204
- ignoreUndefined: true,
205
194
  message: 'Value was not a valid hex color.',
206
195
  fn: function(x, arg) { return validator.isHexColor(x, arg) }
207
196
  },
208
197
  'isIn': {
209
- ignoreEmptyString: true,
210
- ignoreUndefined: true,
211
198
  message: (x, arg) => 'Value was not in the configured whitelist (' + arg.join(', ') + ')',
212
199
  fn: function(x, arg) { return validator.isIn(x, arg) }
213
200
  },
214
201
  'isIP': {
215
- ignoreEmptyString: true,
216
- ignoreUndefined: true,
217
202
  message: 'Value was not a valid IP address.',
218
203
  fn: function(x, arg) { return validator.isIP(x, arg) }
219
204
  },
220
205
  'isNotIn': {
221
- ignoreEmptyString: true,
222
- ignoreUndefined: true,
223
206
  message: (x, arg) => 'Value was in the configured blacklist (' + arg.join(', ') + ')',
224
207
  fn: function(x, arg) { return !validator.isIn(x, arg) }
225
208
  },
226
209
  'isURL': {
227
- ignoreEmptyString: true,
228
- ignoreUndefined: true,
229
210
  message: 'Value was not a valid URL.',
230
211
  fn: function(x, arg) { return validator.isURL(x, arg === true? undefined : arg) }
231
212
  },
232
213
  'isUUID': {
233
- ignoreEmptyString: true,
234
- ignoreUndefined: true,
235
214
  message: 'Value was not a valid UUID.',
236
215
  fn: function(x, arg) { return validator.isUUID(x) }
237
216
  },
238
217
  'minLength': {
239
- ignoreEmptyString: true,
240
- ignoreUndefined: true,
241
218
  message: function(x, arg) {
242
219
  if (typeof x === 'string') return 'Value needs to be at least ' + arg + ' characters long.'
243
220
  else return 'Value needs to contain a minimum of ' + arg + ' items.'
@@ -249,8 +226,6 @@ module.exports = {
249
226
  }
250
227
  },
251
228
  'maxLength': {
252
- ignoreEmptyString: true,
253
- ignoreUndefined: true,
254
229
  message: function(x, arg) {
255
230
  if (typeof x === 'string') return 'Value was longer than the configured maximum length (' + arg + ')'
256
231
  else return 'Value cannot contain more than ' + arg + ' items.'
@@ -262,8 +237,6 @@ module.exports = {
262
237
  }
263
238
  },
264
239
  'regex': {
265
- ignoreEmptyString: true,
266
- ignoreUndefined: true,
267
240
  message: (x, arg) => 'Value did not match the configured regular expression (' + arg + ')',
268
241
  fn: function(x, arg) {
269
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.29.0",
5
+ "version": "1.30.3",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -54,8 +54,11 @@ let plugin = module.exports = {
54
54
  model.imageFields = plugin._findAndTransformImageFields(model.fields, '')
55
55
 
56
56
  if (model.imageFields.length) {
57
- // Update image fields and whitelists with the new object schema
58
- // model._setupFieldsAndWhitelists(model.fields)
57
+ // Todo?: Update image fields and whitelists with the new object schema
58
+ // model._setupFieldsAndWhitelists(model.fields)
59
+ model.beforeValidate.push(function(data, n) {
60
+ plugin.keepImagePlacement(this, data).then(() => n(null, data)).catch(e => n(e))
61
+ })
59
62
  model.beforeUpdate.push(function(data, n) {
60
63
  plugin.removeImages(this, data).then(() => n(null, data)).catch(e => n(e))
61
64
  })
@@ -91,7 +94,9 @@ let plugin = module.exports = {
91
94
  * @param {object} options - monastery operation options {model, query, files, ..}
92
95
  * @param {object} data -
93
96
  * @param {boolean} test -
94
- * @return promise
97
+ * @return promise(
98
+ * {object} data - data object containing new S3 image-object
99
+ * ])
95
100
  * @this plugin
96
101
  */
97
102
  let { model, query, files } = options
@@ -167,7 +172,33 @@ let plugin = module.exports = {
167
172
  })
168
173
  },
169
174
 
170
- removeImages: function(options, data, test) {
175
+ keepImagePlacement: async function(options, data) {
176
+ /**
177
+ * Hook before update/remove
178
+ * Since monastery removes undefined array items on validate, we need to convert any
179
+ * undefined array items to null where files are located to maintain image ordering
180
+ * Todo: maybe dont remove undefined array items in general
181
+ *
182
+ * E.g.
183
+ * req.body = 'photos[0]' : undefined || non existing (set to null)
184
+ * req.files = 'photos[0]' : { ...binary }
185
+ *
186
+ * @param {object} options - monastery operation options {query, model, files, multi, ..}
187
+ * @return promise
188
+ * @this plugin
189
+ */
190
+ if (typeof options.files == 'undefined') return
191
+ // Check upload errors and find valid uploaded images
192
+ let files = await plugin._findValidImages(options.files || {}, options.model)
193
+ // Set undefined primative-array items to null where files are located
194
+ for (let filesArray of files) {
195
+ if (filesArray.inputPath.match(/\.[0-9]+$/)) {
196
+ util.setDeepValue(data, filesArray.inputPath, null, true, false, true)
197
+ }
198
+ }
199
+ },
200
+
201
+ removeImages: async function(options, data, test) {
171
202
  /**
172
203
  * Hook before update/remove
173
204
  * Removes images not found in data, this means you will need to pass the image objects to every update operation
@@ -179,108 +210,113 @@ let plugin = module.exports = {
179
210
  *
180
211
  * @ref https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#deleteObjects-property
181
212
  * @param {object} options - monastery operation options {query, model, files, multi, ..}
182
- * @return promise
213
+ * @return promise([
214
+ * {object} useCount - images that wont be removed, e.g. { lion1: 1 }
215
+ * {array} unused - S3 image uris to be removed, e.g. [{ Key: 'small/lion1.jpg' }, ..]
216
+ * ])
183
217
  * @this plugin
184
218
  */
185
219
  let pre
186
220
  let preExistingImages = []
187
221
  let useCount = {}
188
- if (typeof options.files == 'undefined') return new Promise(res => res())
222
+ if (typeof options.files == 'undefined') return
189
223
 
190
224
  // Find all documents from the same query
191
- return options.model._find(options.query, options)
192
- .then(docs => {
193
- // Find all pre-existing image objects in documents
194
- for (let doc of util.toArray(docs)) { //x2
195
- for (let imageField of options.model.imageFields) { //x5
196
- let images = plugin._findImagesInData(doc, imageField, 0, '').filter(o => o.image)
197
- for (let image of images) {
198
- preExistingImages.push(image)
199
- useCount[image.image.uid] = (useCount[image.image.uid] || 0) + 1
200
- }
201
- }
225
+ let docs = await options.model._find(options.query, options)
226
+
227
+ // Find all pre-existing image objects in documents
228
+ for (let doc of util.toArray(docs)) { //x2
229
+ for (let imageField of options.model.imageFields) { //x5
230
+ let images = plugin._findImagesInData(doc, imageField, 0, '').filter(o => o.image)
231
+ for (let image of images) {
232
+ preExistingImages.push(image)
233
+ useCount[image.image.uid] = (useCount[image.image.uid] || 0) + 1
202
234
  }
235
+ }
236
+ }
203
237
 
204
- // console.log(1, useCount, preExistingImages)
238
+ // console.log(1, useCount, preExistingImages)
205
239
 
206
- // Assign pre-existing images within undefined deep objects and missing array items to null
207
- // ignore undefined root images
208
- let dataFilled = util.deepCopy(data)
209
- for (let key in dataFilled) {
210
- for (let pre of preExistingImages) {
211
- if (!pre.dataPath.match(new RegExp('^' + key + '(\\.|$)'))) continue
212
- util.setDeepValue(dataFilled, pre.dataPath, null, true)
213
- }
214
- }
240
+ // Assign pre-existing images within undefined deep objects and missing array items to null,
241
+ // ignore undefined root images
242
+ let dataFilled = util.deepCopy(data)
243
+ for (let key in dataFilled) {
244
+ for (let pre of preExistingImages) {
245
+ if (!pre.dataPath.match(new RegExp('^' + key + '(\\.|$)'))) continue
246
+ util.setDeepValue(dataFilled, pre.dataPath, null, true)
247
+ }
248
+ }
249
+ // console.log(dataFilled)
215
250
 
216
- // Loop all schema image fields
217
- for (let imageField of options.model.imageFields) { //x5
218
- let images = plugin._findImagesInData(dataFilled, imageField, 0, '')
219
- if (!images.length) continue
220
- // console.log(images)
251
+ // Check upload errors and find valid uploaded images
252
+ let files = await plugin._findValidImages(options.files || {}, options.model)
221
253
 
222
- // Data contains null images that once had a pre-existing image
223
- for (let image of images) {
224
- if (image.image == null && (pre = preExistingImages.find(o => o.dataPath == image.dataPath))) {
225
- useCount[pre.image.uid]--
226
- }
227
- }
254
+ // Loop all schema image fields
255
+ for (let imageField of options.model.imageFields) { //x5
256
+ let images = plugin._findImagesInData(dataFilled, imageField, 0, '')
257
+ if (!images.length) continue
258
+ // console.log(images)
228
259
 
229
- // Data contains valid (pre-existing) image-objects? And are we overriding a pre-existing image?
230
- for (let image of images) {
231
- if (image.image != null) {
232
- let pre = preExistingImages.find(o => o.dataPath == image.dataPath)
233
- if (typeof useCount[image.image.uid] == 'undefined') {
234
- throw `The passed image object for '${image.dataPath}' does not match any pre-existing
235
- images saved on this document.`
236
- } else if (pre && pre.image.uid != image.image.uid) {
237
- useCount[pre.image.uid]--
238
- useCount[image.image.uid]++
239
- } else if (!pre) {
240
- useCount[image.image.uid]++
241
- }
242
- }
243
- }
260
+ // Data contains null images that once had a pre-existing image
261
+ for (let image of images) {
262
+ if (image.image == null && (pre = preExistingImages.find(o => o.dataPath == image.dataPath))) {
263
+ useCount[pre.image.uid]--
244
264
  }
245
- // Check upload errors and find valid uploaded images. If any file is overriding a
246
- // pre-existing image, push to unused
247
- return plugin._findValidImages(options.files || {}, options.model).then(files => {
265
+ }
266
+
267
+ // Loop images found in the data
268
+ for (let image of images) {
269
+ if (image.image != null) {
270
+ let preExistingImage = preExistingImages.find(o => o.dataPath == image.dataPath)
271
+ // valid image-object?
272
+ if (typeof useCount[image.image.uid] == 'undefined') {
273
+ throw `The passed image object for '${image.dataPath}' does not match any pre-existing
274
+ images saved on this document.`
275
+ // Different image from prexisting image
276
+ } else if (preExistingImage && preExistingImage.image.uid != image.image.uid) {
277
+ useCount[preExistingImage.image.uid]--
278
+ useCount[image.image.uid]++
279
+ // No pre-existing image found
280
+ } else if (!preExistingImage) {
281
+ useCount[image.image.uid]++
282
+ }
283
+ // Any file overriding this image?
248
284
  for (let filesArray of files) {
249
- if ((pre = preExistingImages.find(o => o.dataPath == filesArray.inputPath))) {
250
- useCount[pre.image.uid]--
285
+ if (image.dataPath == filesArray.inputPath) {
286
+ useCount[image.image.uid]--
251
287
  }
252
288
  }
253
- })
254
-
255
- }).then(() => {
256
- // Retrieve all the unused files
257
- let unused = []
258
- // console.log(3, useCount)
259
- for (let key in useCount) {
260
- if (useCount[key] > 0) continue
261
- let pre = preExistingImages.find(o => o.image.uid == key)
262
- unused.push(
263
- // original key can have a different extension
264
- { Key: pre.image.path },
265
- { Key: `small/${key}.jpg` },
266
- { Key: `medium/${key}.jpg` },
267
- { Key: `large/${key}.jpg` }
268
- )
269
- this.manager.info(
270
- `Removing '${pre.image.filename}' from '${pre.image.bucket}/${pre.image.path}'`
271
- )
272
289
  }
273
- if (test) return Promise.resolve([useCount, unused])
274
- // Delete any unused images from s3. If the image is on a different bucket
275
- // the file doesnt get deleted, we only delete from plugin.awsBucket.
276
- if (!unused.length) return
277
- return new Promise((resolve, reject) => {
278
- plugin.s3.deleteObjects({ Bucket: plugin.awsBucket, Delete: { Objects: unused }}, (err, data) => {
279
- if (err) reject(err)
280
- resolve()
281
- })
282
- })
290
+ }
291
+ }
292
+
293
+ // Retrieve all the unused files
294
+ // console.log(3, useCount)
295
+ let unused = []
296
+ for (let key in useCount) {
297
+ if (useCount[key] > 0) continue
298
+ let pre = preExistingImages.find(o => o.image.uid == key)
299
+ unused.push(
300
+ // original key can have a different extension
301
+ { Key: pre.image.path },
302
+ { Key: `small/${key}.jpg` },
303
+ { Key: `medium/${key}.jpg` },
304
+ { Key: `large/${key}.jpg` }
305
+ )
306
+ this.manager.info(
307
+ `Removing '${pre.image.filename}' from '${pre.image.bucket}/${pre.image.path}'`
308
+ )
309
+ }
310
+ if (test) return [useCount, unused]
311
+ // Delete any unused images from s3. If the image is on a different bucket
312
+ // the file doesnt get deleted, we only delete from plugin.awsBucket.
313
+ if (!unused.length) return
314
+ await new Promise((resolve, reject) => {
315
+ plugin.s3.deleteObjects({ Bucket: plugin.awsBucket, Delete: { Objects: unused }}, (err, data) => {
316
+ if (err) reject(err)
317
+ resolve()
283
318
  })
319
+ })
284
320
  },
285
321
 
286
322
  _addImageObjectsToData: function(path, data, image) {
package/test/model.js CHANGED
@@ -116,10 +116,9 @@ module.exports = function(monastery, opendb) {
116
116
  })
117
117
 
118
118
  test('Model indexes', async () => {
119
- // Need to test different types of indexes
119
+ // Setup: Need to test different types of indexes
120
120
  let db = (await opendb(null)).db
121
-
122
- // Drop previously tested collections
121
+ // Setup: Drop previously tested collections
123
122
  if ((await db._db.listCollections().toArray()).find(o => o.name == 'userIndexRaw')) {
124
123
  await db._db.collection('userIndexRaw').drop()
125
124
  }
@@ -190,6 +189,96 @@ module.exports = function(monastery, opendb) {
190
189
  db.close()
191
190
  })
192
191
 
192
+ test('Model subdocument indexes', async () => {
193
+ // Setup: Need to test different types of indexes
194
+ let db = (await opendb(null)).db
195
+ // Setup: Drop previously tested collections
196
+ if ((await db._db.listCollections().toArray()).find(o => o.name == 'userIndexSubdoc')) {
197
+ await db._db.collection('userIndexSubdoc').drop()
198
+ }
199
+ // Run
200
+ let userModel = await db.model('userIndexSubdoc', {
201
+ fields: {}
202
+ })
203
+ await expect(userModel._setupIndexes(
204
+ {
205
+ animals: {
206
+ name: { type: 'string', index: 'unique' },
207
+ },
208
+ animals2: {
209
+ names: {
210
+ name: { type: 'string', index: 'unique' },
211
+ },
212
+ },
213
+ animals3: {
214
+ names: {
215
+ name: { type: 'string', index: 'text' },
216
+ },
217
+ },
218
+ }, {
219
+ dryRun: true
220
+ }
221
+ )).resolves.toEqual([{
222
+ 'key': { 'animals.name': 1 },
223
+ 'name': 'animals.name_1',
224
+ 'unique': true,
225
+ }, {
226
+ 'key': { 'animals2.names.name': 1 },
227
+ 'name': 'animals2.names.name_1',
228
+ 'unique': true,
229
+ }, {
230
+ 'key': { 'animals3.names.name': 'text' },
231
+ 'name': 'text',
232
+ }])
233
+
234
+ db.close()
235
+ })
236
+
237
+ test('Model array indexes', async () => {
238
+ // Setup: Need to test different types of indexes
239
+ let db = (await opendb(null)).db
240
+ // Setup: Drop previously tested collections
241
+ if ((await db._db.listCollections().toArray()).find(o => o.name == 'userIndexArray')) {
242
+ await db._db.collection('userIndexArray').drop()
243
+ }
244
+ // Run
245
+ let userModel = await db.model('userIndexArray', {
246
+ fields: {}
247
+ })
248
+ await expect(userModel._setupIndexes(
249
+ {
250
+ animals: [{
251
+ name: { type: 'string', index: 'unique' },
252
+ }],
253
+ animals2: [{ type: 'string', index: true }],
254
+ animals3: [[{ type: 'string', index: true }]],
255
+ animals4: [{
256
+ names: [{
257
+ name: { type: 'string', index: 'unique' },
258
+ }],
259
+ }],
260
+ }, {
261
+ dryRun: true
262
+ }
263
+ )).resolves.toEqual([{
264
+ 'key': { 'animals.name': 1 },
265
+ 'name': 'animals.name_1',
266
+ 'unique': true,
267
+ }, {
268
+ 'key': { 'animals2': 1 },
269
+ 'name': 'animals2_1',
270
+ }, {
271
+ 'key': { 'animals3.0': 1 },
272
+ 'name': 'animals3.0_1',
273
+ }, {
274
+ 'key': { 'animals4.names.name': 1 },
275
+ 'name': 'animals4.names.name_1',
276
+ 'unique': true,
277
+ }])
278
+
279
+ db.close()
280
+ })
281
+
193
282
  test('Model 2dsphere indexes', async () => {
194
283
  // Setup. The tested model needs to be unique as race condition issue arises when the same model
195
284
  // with text indexes are setup at the same time
@@ -1,3 +1,5 @@
1
+ let util = require('../lib/util')
2
+
1
3
  module.exports = function(monastery, opendb) {
2
4
 
3
5
  // Data no images doesn't throw error
@@ -299,13 +301,8 @@ module.exports = function(monastery, opendb) {
299
301
 
300
302
  plugin.removeImages(options, req.body, true)
301
303
  .then(res => {
302
- expect(res[0]).toEqual({ test1: 0, test2: -1, test3: 1, test4: 0 })
304
+ expect(res[0]).toEqual({ test1: 1, test2: 0, test3: 1, test4: 0 })
303
305
  expect(res[1]).toEqual([
304
- { Key: 'dir/test1.png' },
305
- { Key: 'small/test1.jpg' },
306
- { Key: 'medium/test1.jpg' },
307
- { Key: 'large/test1.jpg' },
308
-
309
306
  { Key: 'dir/test2.png' },
310
307
  { Key: 'small/test2.jpg' },
311
308
  { Key: 'medium/test2.jpg' },
@@ -560,4 +557,154 @@ module.exports = function(monastery, opendb) {
560
557
  })()
561
558
  })
562
559
 
560
+ test('images reorder', async () => {
561
+ let db = (await opendb(null, {
562
+ timestamps: false,
563
+ serverSelectionTimeoutMS: 2000,
564
+ imagePlugin: { awsBucket: 'fake', awsAccessKeyId: 'fake', awsSecretAccessKey: 'fake' }
565
+ })).db
566
+
567
+ let user = db.model('user', { fields: {
568
+ logos: [{ type: 'image' }],
569
+ }})
570
+
571
+ let image = {
572
+ bucket: 'test',
573
+ date: 1234,
574
+ filename: 'lion1.png',
575
+ filesize: 1234,
576
+ path: 'test/lion1.png',
577
+ uid: 'lion1'
578
+ }
579
+
580
+ let user1 = await db.user._insert({
581
+ logos: [image],
582
+ })
583
+
584
+ let plugin = db.imagePluginFile
585
+ let supertest = require('supertest')
586
+ let express = require('express')
587
+ let upload = require('express-fileupload')
588
+ let app = express()
589
+ app.use(upload())
590
+
591
+ // Reorder
592
+ app.post('/', async function(req, res) {
593
+ try {
594
+ req.body.logos = JSON.parse(req.body.logos)
595
+ let options = { files: req.files, model: user, query: { _id: user1._id } }
596
+ let response = await plugin.removeImages(options, req.body, true)
597
+ expect(response[0]).toEqual({ lion1: 1 })
598
+ expect(response[1]).toEqual([])
599
+ res.send()
600
+ } catch (e) {
601
+ console.log(e.message || e)
602
+ res.status(500).send()
603
+ }
604
+ })
605
+ await supertest(app)
606
+ .post('/')
607
+ .field('logos', JSON.stringify([ null, image ]))
608
+ .expect(200)
609
+
610
+ db.close()
611
+ })
612
+
613
+ test('images reorder and added image', async () => {
614
+ // latest (2022.02)
615
+ let db = (await opendb(null, {
616
+ timestamps: false,
617
+ serverSelectionTimeoutMS: 2000,
618
+ imagePlugin: { awsBucket: 'fake', awsAccessKeyId: 'fake', awsSecretAccessKey: 'fake' }
619
+ })).db
620
+
621
+ let user = db.model('user', { fields: {
622
+ photos: [{ type: 'image' }],
623
+ }})
624
+
625
+ let image = {
626
+ bucket: 'test',
627
+ date: 1234,
628
+ filename: 'lion1.png',
629
+ filesize: 1234,
630
+ path: 'test/lion1.png',
631
+ uid: 'lion1'
632
+ }
633
+
634
+ let user1 = await db.user._insert({
635
+ photos: [image],
636
+ })
637
+
638
+ let plugin = db.imagePluginFile
639
+ let supertest = require('supertest')
640
+ let express = require('express')
641
+ let upload = require('express-fileupload')
642
+ let app = express()
643
+ app.use(upload())
644
+
645
+ app.post('/', async function(req, res) {
646
+ try {
647
+ // Parse and validate data which is used before in update/insert
648
+ let options = { files: req.files, model: user, query: { _id: user1._id } }
649
+ let data = await util.parseData(req.body)
650
+ data = await user.validate(data, { ...options, update: true })
651
+
652
+ // Empty photo placeholder not removed in validate?
653
+ expect(data.photos[0]).toEqual(null)
654
+ expect(data.photos[1]).toEqual(image)
655
+
656
+ // Remove images
657
+ let response = await plugin.removeImages(options, data, true)
658
+ expect(response[0]).toEqual({ lion1: 1 }) // useCount
659
+ expect(response[1]).toEqual([]) // unused
660
+
661
+ // New file exists
662
+ let validFiles = await plugin._findValidImages(req.files, user)
663
+ expect(((validFiles||[])[0]||{}).inputPath).toEqual('photos.0') // Valid inputPath
664
+
665
+ // Add images
666
+ response = await plugin.addImages(options, data, true)
667
+ expect(response[0]).toEqual({
668
+ photos: [{
669
+ bucket: 'fake',
670
+ date: expect.any(Number),
671
+ filename: 'lion2.jpg',
672
+ filesize: expect.any(Number),
673
+ path: expect.any(String),
674
+ uid: expect.any(String)
675
+ }, {
676
+ bucket: 'test', // still the same image-object reference (nothing new)
677
+ date: expect.any(Number),
678
+ filename: 'lion1.png',
679
+ filesize: expect.any(Number),
680
+ path: expect.any(String),
681
+ uid: expect.any(String)
682
+ },],
683
+ })
684
+
685
+ res.send()
686
+ } catch (e) {
687
+ console.log(e.message || e)
688
+ res.status(500).send()
689
+ }
690
+ })
691
+
692
+ await supertest(app)
693
+ .post('/')
694
+ // Mock multipart/form-data syntax which is not supported by supertest (formdata sent with axios)
695
+ // E.g.
696
+ // req.body = 'photos[1][bucket]' : '...'
697
+ // req.files = 'photos[0]' : { ...binary }
698
+ .field('photos[1][bucket]', image.bucket)
699
+ .field('photos[1][date]', image.date)
700
+ .field('photos[1][filename]', image.filename)
701
+ .field('photos[1][filesize]', image.filesize)
702
+ .field('photos[1][path]', image.path)
703
+ .field('photos[1][uid]', image.uid)
704
+ .attach('photos[0]', `${__dirname}/assets/lion2.jpg`)
705
+ .expect(200)
706
+
707
+ db.close()
708
+ })
709
+
563
710
  }
package/test/validate.js CHANGED
@@ -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 : '' }, { ignoreUndefined: true })).rejects.toContainEqual({
22
+ await expect(user.validate({ name : '' }, { validateUndefined: false })).rejects.toContainEqual({
23
23
  status: '400',
24
24
  title: 'name',
25
25
  detail: 'This field is required.',
@@ -27,8 +27,8 @@ module.exports = function(monastery, opendb) {
27
27
  })
28
28
 
29
29
  // Required error (insert, and with ignoreRequired)
30
- await expect(user.validate({}, { ignoreUndefined: true })).resolves.toEqual({})
31
- await expect(user.validate({}, { ignoreUndefined: true, update: true })).resolves.toEqual({})
30
+ await expect(user.validate({}, { validateUndefined: false })).resolves.toEqual({})
31
+ await expect(user.validate({}, { validateUndefined: false, update: true })).resolves.toEqual({})
32
32
 
33
33
  // No required error (update)
34
34
  await expect(user.validate({}, { update: true })).resolves.toEqual({})
@@ -87,77 +87,120 @@ module.exports = function(monastery, opendb) {
87
87
  let db = (await opendb(false)).db
88
88
  let user = db.model('user', { fields: {
89
89
  animals: {
90
- cat: { type: 'string', required: true },
90
+ cat: { type: 'string', required: true }, // {} = required on insert
91
91
  dog: {
92
92
  name: { type: 'string' },
93
- color: { type: 'string', required: true }
93
+ color: { type: 'string', required: true } // {} = required on insert
94
94
  }
95
95
  }
96
96
  }})
97
97
 
98
- // Invalid subdocument type
99
- await expect(user.validate({ animals: { dog: 1 }})).rejects.toContainEqual({
100
- status: '400',
101
- title: 'animals.dog',
102
- detail: 'Value was not an object.',
103
- meta: { rule: 'isObject', model: 'user', field: 'dog' }
104
- })
98
+ // Insert: Required subdocument properties
99
+ await expect(user.validate({})).rejects.toEqual(
100
+ expect.arrayContaining([
101
+ expect.objectContaining({
102
+ status: '400',
103
+ title: 'animals.cat',
104
+ detail: 'This field is required.',
105
+ meta: { rule: 'required', model: 'user', field: 'cat' }
106
+ }),
107
+ expect.objectContaining({
108
+ status: '400',
109
+ title: 'animals.dog.color',
110
+ detail: 'This field is required.',
111
+ meta: { rule: 'required', model: 'user', field: 'color' }
112
+ }),
113
+ ])
114
+ )
105
115
 
106
- // Required subdocument property (required on insert)
107
- await expect(user.validate({})).rejects.toContainEqual({
108
- status: '400',
109
- title: 'animals.cat',
110
- detail: 'This field is required.',
111
- meta: { rule: 'required', model: 'user', field: 'cat' }
112
- })
116
+ // Insert: Required subdocument properties
117
+ await expect(user.validate({ animals: {} })).rejects.toEqual(
118
+ expect.arrayContaining([
119
+ expect.objectContaining({
120
+ status: '400',
121
+ title: 'animals.cat',
122
+ detail: 'This field is required.',
123
+ meta: { rule: 'required', model: 'user', field: 'cat' }
124
+ }),
125
+ expect.objectContaining({
126
+ status: '400',
127
+ title: 'animals.dog.color',
128
+ detail: 'This field is required.',
129
+ meta: { rule: 'required', model: 'user', field: 'color' }
130
+ }),
131
+ ])
132
+ )
113
133
 
114
- // Required subdocument property (required on insert)
115
- await expect(user.validate({})).rejects.toContainEqual({
116
- status: '400',
117
- title: 'animals.dog.color',
118
- detail: 'This field is required.',
119
- meta: { rule: 'required', model: 'user', field: 'color' }
120
- })
134
+ // Insert: Invalid subdocument type
135
+ await expect(user.validate({ animals: { dog: 1 }})).rejects.toEqual(
136
+ expect.arrayContaining([
137
+ expect.objectContaining({
138
+ status: '400',
139
+ title: 'animals.cat',
140
+ detail: 'This field is required.',
141
+ meta: { rule: 'required', model: 'user', field: 'cat' }
142
+ }),
143
+ expect.objectContaining({
144
+ status: '400',
145
+ title: 'animals.dog',
146
+ detail: 'Value was not an object.',
147
+ meta: { rule: 'isObject', model: 'user', field: 'dog' }
148
+ }),
149
+ ])
150
+ )
121
151
 
122
- // Required subdocument property (required on update when a parent is specified)
123
- await expect(user.validate({ animals: {} }, { update: true })).rejects.toContainEqual({
124
- status: '400',
125
- title: 'animals.cat',
126
- detail: 'This field is required.',
127
- meta: { rule: 'required', model: 'user', field: 'cat' }
152
+ // Insert: Ignore required subdocument property with a defined parent
153
+ await expect(user.validate({ animals: {} }, { validateUndefined: false })).resolves.toEqual({
154
+ animals: {}
128
155
  })
129
156
 
130
- // Required subdocument property (required on update when a parent is specified)
131
- await expect(user.validate({ animals: { dog: {}} }, { update: true })).rejects.toContainEqual({
132
- status: '400',
133
- title: 'animals.dog.color',
134
- detail: 'This field is required.',
135
- meta: { rule: 'required', model: 'user', field: 'color' }
136
- })
157
+ // Update: Required subdocument property when a parent/grandparent is specified
158
+ await expect(user.validate({ animals: {} }, { update: true })).rejects.toEqual(
159
+ expect.arrayContaining([
160
+ expect.objectContaining({
161
+ status: '400',
162
+ title: 'animals.cat',
163
+ detail: 'This field is required.',
164
+ meta: { rule: 'required', model: 'user', field: 'cat' }
165
+ }),
166
+ expect.objectContaining({
167
+ status: '400',
168
+ title: 'animals.dog.color',
169
+ detail: 'This field is required.',
170
+ meta: { rule: 'required', model: 'user', field: 'color' }
171
+ }),
172
+ ])
173
+ )
137
174
 
138
- // Required subdocument property (required on update when a grand-parent is specified)
139
- await expect(user.validate({ animals: {} }, { update: true })).rejects.toContainEqual({
140
- status: '400',
141
- title: 'animals.dog.color',
142
- detail: 'This field is required.',
143
- meta: { rule: 'required', model: 'user', field: 'color' }
144
- })
175
+ // Update: Required subdocument property when a parent is specified
176
+ await expect(user.validate({ animals: { dog: {}} }, { update: true })).rejects.toEqual(
177
+ expect.arrayContaining([
178
+ expect.objectContaining({
179
+ status: '400',
180
+ title: 'animals.cat',
181
+ detail: 'This field is required.',
182
+ meta: { rule: 'required', model: 'user', field: 'cat' }
183
+ }),
184
+ expect.objectContaining({
185
+ status: '400',
186
+ title: 'animals.dog.color',
187
+ detail: 'This field is required.',
188
+ meta: { rule: 'required', model: 'user', field: 'color' }
189
+ }),
190
+ ])
191
+ )
145
192
 
146
- // Ignore required subdocument property (not required on update)
193
+ // Update: Ignore required subdocument property when root parent is undefined
147
194
  await expect(user.validate({}, { update: true })).resolves.toEqual({})
148
195
 
149
- // Ignore required subdocument property with a defined parent (update) (not required if ignoreUndefined set)
150
- await expect(user.validate({ animals: {} }, { ignoreUndefined: true })).resolves.toEqual({
151
- animals: {}
152
- })
153
196
 
154
- // Ignore required subdocument property with a defined parent (not required if ignoreUndefined set)
155
- await expect(user.validate({ animals: {} }, { update: true, ignoreUndefined: true })).resolves.toEqual({
197
+ // Update: Ignore required subdocument property with a defined parent when validateUndefined = false
198
+ await expect(user.validate({ animals: {} }, { update: true, validateUndefined: false })).resolves.toEqual({
156
199
  animals: {}
157
200
  })
158
201
 
159
- // Required subdocument property (defined with ignoreUndefined)
160
- await expect(user.validate({ animals: { cat: '' }}, { update: true, ignoreUndefined: true }))
202
+ // Update: Required defined subdocument property when validateUndefined = false
203
+ await expect(user.validate({ animals: { cat: '' }}, { update: true, validateUndefined: false }))
161
204
  .rejects.toContainEqual({
162
205
  status: '400',
163
206
  title: 'animals.cat',
@@ -215,17 +258,17 @@ module.exports = function(monastery, opendb) {
215
258
  await expect(user.validate({ animals: { dogs: [] }}))
216
259
  .resolves.toEqual({ animals: { dogs: [] }})
217
260
 
218
- // No undefined item errors with ignoreUndefined
261
+ // No undefined item errors with validateUndefined=false
219
262
  await expect(user.validate(
220
263
  { animals: { dogs: [{ name: 'sparky' }] }},
221
- { update: true, ignoreUndefined: true }
264
+ { update: true, validateUndefined: false }
222
265
  ))
223
266
  .resolves.toEqual({ animals: { dogs: [{ name: 'sparky' }] }})
224
267
 
225
- // Requried error within an array subdocument (even during update when parent defined && ignoreUndefined = true)
268
+ // Requried error within an array subdocument (even during update when parent defined && validateUndefined = false)
226
269
  await expect(user.validate(
227
270
  { animals: { dogs: [{ name: 'sparky', color: '' }] }},
228
- { update: true, ignoreUndefined: true }
271
+ { update: true, validateUndefined: false }
229
272
  ))
230
273
  .rejects.toContainEqual(error)
231
274
  })
@@ -497,7 +540,7 @@ module.exports = function(monastery, opendb) {
497
540
  },
498
541
  rules: {
499
542
  requiredIfNoName: {
500
- ignoreUndefined: false,
543
+ validateUndefined: true,
501
544
  fn: function(value, ruleArg) {
502
545
  return value || this.name
503
546
  }
@@ -547,7 +590,7 @@ module.exports = function(monastery, opendb) {
547
590
  'title': 'age'
548
591
  }
549
592
  ])
550
- await expect(user2.validate({ }, { ignoreUndefined: true })).resolves.toEqual({})
593
+ await expect(user2.validate({ }, { validateUndefined: false })).resolves.toEqual({})
551
594
 
552
595
  // Required rule based off another field (update)
553
596
  await expect(user2.validate({ }, { update: true })).resolves.toEqual({})