monastery 1.29.0 → 1.30.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/lib/model-crud.js +4 -4
- package/lib/model-validate.js +9 -5
- package/lib/model.js +11 -9
- package/lib/rules.js +16 -43
- package/package.json +1 -1
- package/test/model.js +92 -3
- package/test/validate.js +104 -61
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|
|
|
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', '
|
|
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|
|
|
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', '
|
|
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
|
package/lib/model-validate.js
CHANGED
|
@@ -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|
|
|
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 (
|
|
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
|
|
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 ((
|
|
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.
|
|
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.
|
|
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[
|
|
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: `${
|
|
343
|
+
indexes.push({ name: `${path2}_${type}`, key: { [path2]: type }, ...options })
|
|
344
344
|
} else if (type == 'unique') {
|
|
345
|
-
indexes.push({ name: `${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
+
validateEmptyString: true,
|
|
117
118
|
message: '',
|
|
118
119
|
fn: function(x) {
|
|
119
120
|
return true
|
|
120
121
|
}
|
|
121
122
|
},
|
|
122
123
|
'isId': {
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/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.
|
|
5
|
+
"version": "1.30.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:boycce/monastery",
|
|
8
8
|
"homepage": "https://boycce.github.io/monastery/",
|
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
|
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 : '' }, {
|
|
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({}, {
|
|
31
|
-
await expect(user.validate({}, {
|
|
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
|
-
//
|
|
99
|
-
await expect(user.validate({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
107
|
-
await expect(user.validate({})).rejects.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
//
|
|
115
|
-
await expect(user.validate({})).rejects.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
//
|
|
123
|
-
await expect(user.validate({ animals: {} }, {
|
|
124
|
-
|
|
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
|
|
131
|
-
await expect(user.validate({ animals: {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
139
|
-
await expect(user.validate({ animals: {} }, { update: true })).rejects.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
155
|
-
await expect(user.validate({ animals: {} }, { update: true,
|
|
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
|
|
160
|
-
await expect(user.validate({ animals: { cat: '' }}, { update: 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
|
|
261
|
+
// No undefined item errors with validateUndefined=false
|
|
219
262
|
await expect(user.validate(
|
|
220
263
|
{ animals: { dogs: [{ name: 'sparky' }] }},
|
|
221
|
-
{ update: 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 &&
|
|
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,
|
|
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
|
-
|
|
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({ }, {
|
|
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({})
|