monastery 1.27.3 → 1.28.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/docs/readme.md +6 -0
- package/docs/schema.md +32 -10
- package/lib/model-validate.js +35 -6
- package/lib/model.js +17 -14
- package/lib/rules.js +17 -12
- package/package.json +1 -1
- package/test/model.js +61 -12
- package/test/validate.js +238 -33
package/docs/readme.md
CHANGED
package/docs/schema.md
CHANGED
|
@@ -173,13 +173,13 @@ You are able to define custom validation rules to use. (`this` will refer to the
|
|
|
173
173
|
```js
|
|
174
174
|
schema.rules = {
|
|
175
175
|
// Basic definition
|
|
176
|
-
isGrandMaster: function(value, ruleArgument,
|
|
176
|
+
isGrandMaster: function(value, ruleArgument, path, model) {
|
|
177
177
|
return (value == 'Martin Luther')? true : false
|
|
178
178
|
},
|
|
179
179
|
// Full definition
|
|
180
180
|
isGrandMaster: {
|
|
181
|
-
message: (value, ruleArgument,
|
|
182
|
-
fn: function(value, ruleArgument,
|
|
181
|
+
message: (value, ruleArgument, path, model) => 'Only grand masters are permitted'
|
|
182
|
+
fn: function(value, ruleArgument, path, model) {
|
|
183
183
|
return (value == 'Martin Luther' || this.age > 100)? true : false
|
|
184
184
|
}
|
|
185
185
|
}
|
|
@@ -209,17 +209,39 @@ You are able to define custom error messages for each validation rule.
|
|
|
209
209
|
|
|
210
210
|
```js
|
|
211
211
|
schema.messages = {
|
|
212
|
-
|
|
212
|
+
'name': {
|
|
213
213
|
required: 'Sorry, even a monk cannot be nameless'
|
|
214
|
-
type: 'Sorry, your name needs to be a string
|
|
214
|
+
type: 'Sorry, your name needs to be a string'
|
|
215
215
|
},
|
|
216
|
-
|
|
217
|
-
minLength: (value, ruleArgument,
|
|
216
|
+
'address.city': {
|
|
217
|
+
minLength: (value, ruleArgument, path, model) => {
|
|
218
218
|
return `Is your city of residence really only ${ruleArgument} characters long?`
|
|
219
219
|
}
|
|
220
220
|
},
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
// You can assign custom error messages for all subdocument fields in an array
|
|
222
|
+
// e.g. pets = [{ name: { type: 'string' }}]
|
|
223
|
+
'pets.name': {
|
|
224
|
+
required: `Your pet's name needs to be a string.`
|
|
225
|
+
}
|
|
226
|
+
// To target a specific array item
|
|
227
|
+
'pets.0.name': {
|
|
228
|
+
required: `You first pet needs a name`
|
|
229
|
+
}
|
|
230
|
+
// You can also target any rules set on the array or sub arrays
|
|
231
|
+
// e.g.
|
|
232
|
+
// let arrayWithSchema = (array, schema) => { array.schema = schema; return array }
|
|
233
|
+
// petGroups = arrayWithSchema(
|
|
234
|
+
// [arrayWithSchema(
|
|
235
|
+
// [{ name: { type: 'string' }}],
|
|
236
|
+
// { minLength: 1 }
|
|
237
|
+
// )],
|
|
238
|
+
// { minLength: 1 }
|
|
239
|
+
// )
|
|
240
|
+
'petGroups': {
|
|
241
|
+
minLength: `Please add at least one pet pet group.`
|
|
242
|
+
}
|
|
243
|
+
'petGroups.$': {
|
|
244
|
+
minLength: `Please add at least one pet into your pet group.`
|
|
223
245
|
}
|
|
224
246
|
}
|
|
225
247
|
```
|
|
@@ -265,7 +287,7 @@ let schema = {
|
|
|
265
287
|
|
|
266
288
|
afterFind: [function(data) {// Synchronous
|
|
267
289
|
data = data || {}
|
|
268
|
-
data.name = data.firstName +
|
|
290
|
+
data.name = data.firstName + ' ' + data.lastName
|
|
269
291
|
}]
|
|
270
292
|
}
|
|
271
293
|
|
package/lib/model-validate.js
CHANGED
|
@@ -85,6 +85,34 @@ module.exports = {
|
|
|
85
85
|
})
|
|
86
86
|
},
|
|
87
87
|
|
|
88
|
+
_getMostSpecificKeyMatchingPath: function(object, path) {
|
|
89
|
+
/**
|
|
90
|
+
* Get all possible array variation matches from the object, and return the most specifc key
|
|
91
|
+
* @param {object} object - e.g. { 'pets.1.name', 'pets.$.name', 'pets.name', .. }
|
|
92
|
+
* @path {string} path - must be a specifc path, e.g. 'pets.1.name'
|
|
93
|
+
* @return most specific key in object
|
|
94
|
+
*
|
|
95
|
+
* 1. Get all viable messages keys, e.g. (key)dogs.$ == (path)dogs.1
|
|
96
|
+
* 2. Order array key list by scoring, i.e. [0-9]=2, $=1, ''=0
|
|
97
|
+
* 3. Return first
|
|
98
|
+
*/
|
|
99
|
+
let keys = []
|
|
100
|
+
let pathExpand = path.replace(/\.([0-9]+)/g, '(.$1|.\\$|)').replace(/\./g, '\\.')
|
|
101
|
+
let pathreg = new RegExp(`^${pathExpand}$`)
|
|
102
|
+
|
|
103
|
+
for (let key in object) {
|
|
104
|
+
if (key.match(pathreg)) {
|
|
105
|
+
let score = (key.match(/\.[0-9]+/g)||[]).length * 1001
|
|
106
|
+
score += (key.match(/\.\$/g)||[]).length * 1000
|
|
107
|
+
keys.push({ score: score, key: key })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!keys.length) return
|
|
112
|
+
else if (keys.length == 1) return keys[0].key
|
|
113
|
+
return keys.sort((a, b) => a.score - b.score).reverse()[0].key // descending
|
|
114
|
+
},
|
|
115
|
+
|
|
88
116
|
_validateFields: function(dataRoot, fields, data, opts, path) {
|
|
89
117
|
/**
|
|
90
118
|
* Recurse through and retrieve any errors and valid data
|
|
@@ -223,17 +251,18 @@ module.exports = {
|
|
|
223
251
|
|
|
224
252
|
_validateRule: function(dataRoot, ruleName, field, ruleArg, value, path) {
|
|
225
253
|
//this.debug(path, field, ruleName, ruleArg, value)
|
|
226
|
-
|
|
254
|
+
// Remove [] from the message path, and simply ignore non-numeric children to test for all array items
|
|
227
255
|
ruleArg = ruleArg === true? undefined : ruleArg
|
|
228
256
|
let rule = this.rules[ruleName] || rules[ruleName]
|
|
229
257
|
let fieldName = path.match(/[^\.]+$/)[0]
|
|
230
|
-
let
|
|
231
|
-
let ruleMessage =
|
|
258
|
+
let ruleMessageKey = this._getMostSpecificKeyMatchingPath(this.messages, path)
|
|
259
|
+
let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
|
|
232
260
|
if (!ruleMessage) ruleMessage = rule.message
|
|
233
261
|
|
|
262
|
+
|
|
234
263
|
if (ruleName !== 'required') {
|
|
235
264
|
// Ignore undefined when not testing 'required'
|
|
236
|
-
if (typeof value === 'undefined') return
|
|
265
|
+
if (typeof value === 'undefined') return ////////////////////////////////////////
|
|
237
266
|
|
|
238
267
|
// Ignore null if not testing required
|
|
239
268
|
if (value === null && !field.isObject && !field.isArray) return
|
|
@@ -246,9 +275,9 @@ module.exports = {
|
|
|
246
275
|
if (value === '' && rule.ignoreEmptyString) return
|
|
247
276
|
|
|
248
277
|
// Rule failed
|
|
249
|
-
if (!rule.fn.call(dataRoot, value, ruleArg,
|
|
278
|
+
if (!rule.fn.call(dataRoot, value, ruleArg, path, this)) return {
|
|
250
279
|
detail: util.isFunction(ruleMessage)
|
|
251
|
-
? ruleMessage.call(dataRoot, value, ruleArg,
|
|
280
|
+
? ruleMessage.call(dataRoot, value, ruleArg, path, this)
|
|
252
281
|
: ruleMessage,
|
|
253
282
|
meta: { rule: ruleName, model: this.name, field: fieldName, detailLong: rule.messageLong },
|
|
254
283
|
status: '400',
|
package/lib/model.js
CHANGED
|
@@ -6,10 +6,11 @@ let validate = require('./model-validate')
|
|
|
6
6
|
let Model = module.exports = function(name, opts, manager) {
|
|
7
7
|
/**
|
|
8
8
|
* Setup a model (aka monk collection)
|
|
9
|
-
* Todo: convert into a promise
|
|
10
9
|
* @param {string} name
|
|
11
10
|
* @param {object} opts - see mongodb colleciton documentation
|
|
12
|
-
* @
|
|
11
|
+
* @param {boolean} opts.waitForIndexes
|
|
12
|
+
* @this model
|
|
13
|
+
* @return Promise(model) | this
|
|
13
14
|
*/
|
|
14
15
|
if (!(this instanceof Model)) {
|
|
15
16
|
return new Model(name, opts, this)
|
|
@@ -25,10 +26,6 @@ let Model = module.exports = function(name, opts, manager) {
|
|
|
25
26
|
opts = opts || {}
|
|
26
27
|
Object.assign(this, {
|
|
27
28
|
...(opts.methods || {}),
|
|
28
|
-
name: name,
|
|
29
|
-
manager: manager,
|
|
30
|
-
error: manager.error,
|
|
31
|
-
info: manager.info,
|
|
32
29
|
afterFind: opts.afterFind || [],
|
|
33
30
|
afterInsert: (opts.afterInsert || []).concat(opts.afterInsertUpdate || []),
|
|
34
31
|
afterUpdate: (opts.afterUpdate || []).concat(opts.afterInsertUpdate || []),
|
|
@@ -37,12 +34,16 @@ let Model = module.exports = function(name, opts, manager) {
|
|
|
37
34
|
beforeUpdate: (opts.beforeUpdate || []).concat(opts.beforeInsertUpdate || []),
|
|
38
35
|
beforeRemove: opts.beforeRemove || [],
|
|
39
36
|
beforeValidate: opts.beforeValidate || [],
|
|
40
|
-
|
|
37
|
+
error: manager.error,
|
|
38
|
+
info: manager.info,
|
|
41
39
|
insertBL: opts.insertBL || [],
|
|
42
|
-
updateBL: opts.updateBL || [],
|
|
43
|
-
messages: opts.messages || {},
|
|
44
40
|
fields: { ...(util.deepCopy(opts.fields) || {}) },
|
|
45
|
-
|
|
41
|
+
findBL: opts.findBL || ['password'],
|
|
42
|
+
manager: manager,
|
|
43
|
+
messages: opts.messages || {},
|
|
44
|
+
name: name,
|
|
45
|
+
rules: { ...(opts.rules || {}) },
|
|
46
|
+
updateBL: opts.updateBL || [],
|
|
46
47
|
})
|
|
47
48
|
|
|
48
49
|
// Run before model hooks
|
|
@@ -94,13 +95,13 @@ let Model = module.exports = function(name, opts, manager) {
|
|
|
94
95
|
// Add model to manager.model
|
|
95
96
|
this.manager.model[name] = this
|
|
96
97
|
|
|
97
|
-
// Ensure field indexes exist in
|
|
98
|
+
// Setup/Ensure field indexes exist in MongoDB
|
|
98
99
|
let errHandler = err => {
|
|
99
100
|
if (err.type == 'info') this.info(err.detail)
|
|
100
101
|
else this.error(err)
|
|
101
102
|
}
|
|
102
|
-
if (opts.
|
|
103
|
-
else this._setupIndexes().catch(errHandler)
|
|
103
|
+
if (opts.waitForIndexes) return this._setupIndexes().catch(errHandler).then(() => this)
|
|
104
|
+
else this._setupIndexes().catch(errHandler) // returns this
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
Model.prototype._getFieldlist = function(fields, path) {
|
|
@@ -243,7 +244,7 @@ Model.prototype._setupIndexes = function(fields) {
|
|
|
243
244
|
* @link https://docs.mongodb.com/manual/reference/command/createIndexes/
|
|
244
245
|
* @link https://mongodb.github.io/node-mongodb-native/2.1/api/Collection.html#createIndexes
|
|
245
246
|
* @param {object} <fields>
|
|
246
|
-
* @return Promise( {array} indexes | {string} error )
|
|
247
|
+
* @return Promise( {array} indexes ensured | {string} error )
|
|
247
248
|
*
|
|
248
249
|
* MongoDB index structures = [
|
|
249
250
|
* true = { name: 'name_1', key: { name: 1 } },
|
|
@@ -267,6 +268,7 @@ Model.prototype._setupIndexes = function(fields) {
|
|
|
267
268
|
|
|
268
269
|
// Find all indexes
|
|
269
270
|
recurseFields(fields || model.fields, '')
|
|
271
|
+
// console.log(2, indexes, fields)
|
|
270
272
|
if (hasTextIndex) indexes.push(textIndex)
|
|
271
273
|
if (!indexes.length) return Promise.resolve([]) // No indexes defined
|
|
272
274
|
|
|
@@ -313,6 +315,7 @@ Model.prototype._setupIndexes = function(fields) {
|
|
|
313
315
|
})
|
|
314
316
|
.then(response => {
|
|
315
317
|
model.info('db index(s) created for ' + model.name)
|
|
318
|
+
return indexes
|
|
316
319
|
})
|
|
317
320
|
|
|
318
321
|
function recurseFields(fields, parentPath) {
|
package/lib/rules.js
CHANGED
|
@@ -5,6 +5,7 @@ let validator = require('validator')
|
|
|
5
5
|
module.exports = {
|
|
6
6
|
|
|
7
7
|
required: {
|
|
8
|
+
runOnUndefined: true, // integrate
|
|
8
9
|
message: 'This field is required.',
|
|
9
10
|
fn: function(x) {
|
|
10
11
|
if (util.isArray(x) && !x.length) return false
|
|
@@ -12,7 +13,7 @@ module.exports = {
|
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
+
// Type rules below ignore undefined
|
|
16
17
|
|
|
17
18
|
'isBoolean': {
|
|
18
19
|
message: 'Value was not a boolean.',
|
|
@@ -25,12 +26,6 @@ module.exports = {
|
|
|
25
26
|
return typeof x === 'boolean'
|
|
26
27
|
}
|
|
27
28
|
},
|
|
28
|
-
'isNotEmptyString': {
|
|
29
|
-
message: 'Value was an empty string.',
|
|
30
|
-
fn: function(x) {
|
|
31
|
-
return x !== ''
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
29
|
'isArray': {
|
|
35
30
|
message: 'Value was not an array.',
|
|
36
31
|
tryParse: function(x) {
|
|
@@ -132,6 +127,8 @@ module.exports = {
|
|
|
132
127
|
return util.isObject(x) && ObjectId.isValid(x)/*x.get_inc*/? true : false
|
|
133
128
|
}
|
|
134
129
|
},
|
|
130
|
+
|
|
131
|
+
|
|
135
132
|
'max': {
|
|
136
133
|
message: (x, arg) => 'Value was greater than the configured maximum (' + arg + ')',
|
|
137
134
|
fn: function(x, arg) {
|
|
@@ -146,8 +143,16 @@ module.exports = {
|
|
|
146
143
|
return x >= arg
|
|
147
144
|
}
|
|
148
145
|
},
|
|
146
|
+
'isNotEmptyString': {
|
|
147
|
+
message: 'Value was an empty string.',
|
|
148
|
+
fn: function(x) {
|
|
149
|
+
return x !== ''
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Rules below ignore undefined & empty strings
|
|
154
|
+
// (e.g. an empty email field can be saved that isn't required)
|
|
149
155
|
|
|
150
|
-
// Rules below ignore null & empty strings
|
|
151
156
|
'enum': {
|
|
152
157
|
ignoreEmptyString: true,
|
|
153
158
|
message: (x, arg) => 'Invalid enum value',
|
|
@@ -157,10 +162,10 @@ module.exports = {
|
|
|
157
162
|
}
|
|
158
163
|
}
|
|
159
164
|
},
|
|
160
|
-
'hasAgreed': {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
},
|
|
165
|
+
// 'hasAgreed': {
|
|
166
|
+
// message: (x, arg) => 'Please agree to the terms and conditions.',
|
|
167
|
+
// fn: function(x, arg) { return !x }
|
|
168
|
+
// },
|
|
164
169
|
'isAfter': {
|
|
165
170
|
ignoreEmptyString: true,
|
|
166
171
|
message: (x, arg) => 'Value was before the configured time (' + 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.28.3",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:boycce/monastery",
|
|
8
8
|
"homepage": "https://boycce.github.io/monastery/",
|
package/test/model.js
CHANGED
|
@@ -116,27 +116,76 @@ module.exports = function(monastery, opendb) {
|
|
|
116
116
|
})
|
|
117
117
|
|
|
118
118
|
test('Model indexes', async (done) => {
|
|
119
|
-
// Setup
|
|
120
119
|
// Need to test different types of indexes
|
|
121
120
|
let db = (await opendb(null)).db
|
|
122
|
-
let user = db.model('user', {})
|
|
123
|
-
let user2 = db.model('user2', {})
|
|
124
121
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
// Drop previously tested collections
|
|
123
|
+
if ((await db._db.listCollections().toArray()).find(o => o.name == 'userIndexRaw')) {
|
|
124
|
+
await db._db.collection('userIndexRaw').drop()
|
|
125
|
+
}
|
|
126
|
+
if ((await db._db.listCollections().toArray()).find(o => o.name == 'userIndex')) {
|
|
127
|
+
await db._db.collection('userIndex').drop()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Unique & text index (after model initialisation, in serial)
|
|
131
|
+
let userIndexRawModel = db.model('userIndexRaw', {})
|
|
132
|
+
let setupIndex1 = await userIndexRawModel._setupIndexes({
|
|
133
|
+
email: { type: 'string', index: 'unique' },
|
|
134
|
+
})
|
|
135
|
+
let setupIndex2 = await userIndexRawModel._setupIndexes({
|
|
136
|
+
name: { type: 'string', index: 'text' },
|
|
137
|
+
})
|
|
138
|
+
let indexes = await db._db.collection('userIndexRaw').indexes()
|
|
139
|
+
expect(indexes[0]).toMatchObject({ v: 2, key: { _id: 1 }, name: '_id_' })
|
|
140
|
+
expect(indexes[1]).toMatchObject({ v: 2, unique: true, key: { email: 1 }, name: 'email_1' })
|
|
141
|
+
expect(indexes[2]).toMatchObject({
|
|
142
|
+
v: 2,
|
|
143
|
+
key: { _fts: 'text', _ftsx: 1 },
|
|
144
|
+
name: 'text',
|
|
145
|
+
weights: { name: 1 },
|
|
146
|
+
default_language: 'english',
|
|
147
|
+
language_override: 'language',
|
|
148
|
+
textIndexVersion: 3
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Unique & text index
|
|
152
|
+
let userIndexModel = await db.model('userIndex', {
|
|
153
|
+
waitForIndexes: true,
|
|
154
|
+
fields: {
|
|
155
|
+
email: { type: 'string', index: 'unique' },
|
|
156
|
+
name: { type: 'string', index: 'text' },
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
let indexes2 = await db._db.collection('userIndex').indexes()
|
|
161
|
+
expect(indexes2[0]).toMatchObject({ v: 2, key: { _id: 1 }, name: '_id_' })
|
|
162
|
+
expect(indexes2[1]).toMatchObject({ v: 2, unique: true, key: { email: 1 }, name: 'email_1' })
|
|
163
|
+
expect(indexes2[2]).toMatchObject({
|
|
164
|
+
v: 2,
|
|
165
|
+
key: { _fts: 'text', _ftsx: 1 },
|
|
166
|
+
name: 'text',
|
|
167
|
+
weights: { name: 1 },
|
|
168
|
+
default_language: 'english',
|
|
169
|
+
language_override: 'language',
|
|
170
|
+
textIndexVersion: 3
|
|
128
171
|
})
|
|
129
172
|
|
|
130
173
|
// No text index change error, i.e. new Error("Index with name: text already exists with different options")
|
|
131
|
-
await expect(
|
|
174
|
+
await expect(userIndexModel._setupIndexes({
|
|
132
175
|
name: { type: 'string', index: 'text' },
|
|
133
176
|
name2: { type: 'string', index: 'text' }
|
|
134
|
-
})).resolves.toEqual(
|
|
177
|
+
})).resolves.toEqual([{
|
|
178
|
+
"key": { "name": "text", "name2": "text" },
|
|
179
|
+
"name": "text",
|
|
180
|
+
}])
|
|
135
181
|
|
|
136
182
|
// Text index on a different model
|
|
137
|
-
await expect(
|
|
138
|
-
|
|
139
|
-
})).resolves.toEqual(
|
|
183
|
+
await expect(userIndexRawModel._setupIndexes({
|
|
184
|
+
name2: { type: 'string', index: 'text' }
|
|
185
|
+
})).resolves.toEqual([{
|
|
186
|
+
"key": {"name2": "text"},
|
|
187
|
+
"name": "text",
|
|
188
|
+
}])
|
|
140
189
|
|
|
141
190
|
db.close()
|
|
142
191
|
done()
|
|
@@ -147,7 +196,7 @@ module.exports = function(monastery, opendb) {
|
|
|
147
196
|
// with text indexes are setup at the same time
|
|
148
197
|
let db = (await opendb(null)).db
|
|
149
198
|
await db.model('user3', {
|
|
150
|
-
|
|
199
|
+
waitForIndexes: true,
|
|
151
200
|
fields: {
|
|
152
201
|
location: {
|
|
153
202
|
index: '2dsphere',
|
package/test/validate.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
let validate = require('../lib/model-validate')
|
|
2
|
+
|
|
1
3
|
module.exports = function(monastery, opendb) {
|
|
2
4
|
|
|
3
5
|
test('Validation basic errors', async () => {
|
|
@@ -227,7 +229,40 @@ module.exports = function(monastery, opendb) {
|
|
|
227
229
|
.rejects.toContainEqual(error)
|
|
228
230
|
})
|
|
229
231
|
|
|
230
|
-
test('Validation
|
|
232
|
+
test('Validation getMostSpecificKeyMatchingPath', async () => {
|
|
233
|
+
let fn = validate._getMostSpecificKeyMatchingPath
|
|
234
|
+
let mock = {
|
|
235
|
+
'cats.name': true,
|
|
236
|
+
'cats.name': true,
|
|
237
|
+
|
|
238
|
+
'dogs.name': true,
|
|
239
|
+
'dogs.$.name': true,
|
|
240
|
+
|
|
241
|
+
'pigs.name': true,
|
|
242
|
+
'pigs.$.name': true,
|
|
243
|
+
'pigs.1.name': true,
|
|
244
|
+
'pigs.2.name': true,
|
|
245
|
+
|
|
246
|
+
'gulls.$': true,
|
|
247
|
+
'gulls.$.$': true,
|
|
248
|
+
'gulls.name': true,
|
|
249
|
+
'gulls.$.name': true,
|
|
250
|
+
}
|
|
251
|
+
// subdocument
|
|
252
|
+
expect(fn(mock, 'cats.name')).toEqual('cats.name')
|
|
253
|
+
// array subdocuments
|
|
254
|
+
expect(fn(mock, 'cats.1.name')).toEqual('cats.name')
|
|
255
|
+
expect(fn(mock, 'dogs.1.name')).toEqual('dogs.$.name')
|
|
256
|
+
expect(fn(mock, 'dogs.2.name')).toEqual('dogs.$.name')
|
|
257
|
+
expect(fn(mock, 'pigs.1.name')).toEqual('pigs.1.name')
|
|
258
|
+
expect(fn(mock, 'pigs.2.name')).toEqual('pigs.2.name')
|
|
259
|
+
expect(fn(mock, 'pigs.3.name')).toEqual('pigs.$.name')
|
|
260
|
+
// array
|
|
261
|
+
expect(fn(mock, 'gulls.1.2')).toEqual('gulls.$.$')
|
|
262
|
+
expect(fn(mock, 'gulls.1')).toEqual('gulls.$')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test('Validation default messages', async () => {
|
|
231
266
|
// Setup
|
|
232
267
|
let db = (await opendb(false)).db
|
|
233
268
|
let user = db.model('user', {
|
|
@@ -238,64 +273,234 @@ module.exports = function(monastery, opendb) {
|
|
|
238
273
|
animals: [{
|
|
239
274
|
name: { type: 'string', minLength: 4 }
|
|
240
275
|
}]
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
let mock = {
|
|
280
|
+
status: '400',
|
|
281
|
+
title: 'name',
|
|
282
|
+
detail: 'Value needs to be at least 4 characters long.',
|
|
283
|
+
meta: { rule: 'minLength', model: 'user', field: 'name' }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// basic error
|
|
287
|
+
await expect(user.validate({ name: 'ben' })).rejects.toContainEqual(
|
|
288
|
+
mock
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
// subdocument error
|
|
292
|
+
await expect(user.validate({ dog: { name: 'ben' } })).rejects.toContainEqual({
|
|
293
|
+
...mock,
|
|
294
|
+
title: 'dog.name'
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// array error
|
|
298
|
+
await expect(user.validate({ dogNames: ['ben'] })).rejects.toContainEqual({
|
|
299
|
+
...mock,
|
|
300
|
+
title: 'dogNames.0',
|
|
301
|
+
meta: { ...mock.meta, field: '0' }
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// subdocument in an array error
|
|
305
|
+
await expect(user.validate({ animals: [{ name: 'ben' }] })).rejects.toContainEqual({
|
|
306
|
+
...mock,
|
|
307
|
+
title: 'animals.0.name'
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// subdocument in an array error (different index)
|
|
311
|
+
await expect(user.validate({ animals: [{ name: 'carla' }, { name: 'ben' }] })).rejects.toContainEqual({
|
|
312
|
+
...mock,
|
|
313
|
+
title: 'animals.1.name'
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
test('Validation custom messages', async () => {
|
|
318
|
+
// Setup
|
|
319
|
+
// Todo: Setup testing for array array subdocument field messages
|
|
320
|
+
let db = (await opendb(false)).db
|
|
321
|
+
let arrayWithSchema = (array, schema) => { array.schema = schema; return array }
|
|
322
|
+
let user = db.model('user', {
|
|
323
|
+
fields: {
|
|
324
|
+
name: { type: 'string', minLength: 4 },
|
|
325
|
+
dog: { name: { type: 'string', minLength: 4 }},
|
|
326
|
+
dogNames: [{ type: 'string', minLength: 4 }],
|
|
241
327
|
},
|
|
242
328
|
messages: {
|
|
243
329
|
'name': { minLength: 'Oops min length is 4' },
|
|
244
330
|
'dog.name': { minLength: 'Oops min length is 4' },
|
|
245
|
-
'dogNames
|
|
246
|
-
'animals.[].name': { minLength: 'Oops min length is 4' }
|
|
331
|
+
'dogNames': { minLength: 'Oops min length is 4' },
|
|
247
332
|
}
|
|
248
333
|
})
|
|
249
334
|
|
|
250
|
-
|
|
251
|
-
await expect(user.validate({
|
|
252
|
-
name: 'ben'
|
|
253
|
-
})).rejects.toContainEqual({
|
|
335
|
+
let mock = {
|
|
254
336
|
status: '400',
|
|
255
337
|
title: 'name',
|
|
256
338
|
detail: 'Oops min length is 4',
|
|
257
339
|
meta: { rule: 'minLength', model: 'user', field: 'name' }
|
|
258
|
-
}
|
|
340
|
+
}
|
|
259
341
|
|
|
342
|
+
// basic error
|
|
343
|
+
await expect(user.validate({ name: 'ben' })).rejects.toContainEqual(
|
|
344
|
+
mock
|
|
345
|
+
)
|
|
260
346
|
// subdocument error
|
|
261
|
-
await expect(user.validate({
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
status: '400',
|
|
265
|
-
title: 'dog.name',
|
|
266
|
-
detail: 'Oops min length is 4',
|
|
267
|
-
meta: { rule: 'minLength', model: 'user', field: 'name' }
|
|
347
|
+
await expect(user.validate({ dog: { name: 'ben' } })).rejects.toContainEqual({
|
|
348
|
+
...mock,
|
|
349
|
+
title: 'dog.name'
|
|
268
350
|
})
|
|
269
|
-
|
|
270
351
|
// array error
|
|
271
|
-
await expect(user.validate({
|
|
272
|
-
|
|
273
|
-
|
|
352
|
+
await expect(user.validate({ dogNames: ['ben'] })).rejects.toContainEqual({
|
|
353
|
+
...mock,
|
|
354
|
+
title: 'dogNames.0',
|
|
355
|
+
meta: { ...mock.meta, field: '0' }
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('Validation custom messages for arrays', async () => {
|
|
360
|
+
// Setup
|
|
361
|
+
// Todo: Setup testing for array array subdocument field messages
|
|
362
|
+
let db = (await opendb(false)).db
|
|
363
|
+
let arrayWithSchema = (array, schema) => { array.schema = schema; return array }
|
|
364
|
+
let user = db.model('user', {
|
|
365
|
+
fields: {
|
|
366
|
+
dogNames: arrayWithSchema([
|
|
367
|
+
arrayWithSchema([{ type: 'string' }], { minLength: 1 })
|
|
368
|
+
], { minLength: 1 }),
|
|
369
|
+
catNames: [{
|
|
370
|
+
name: { type: 'string', minLength: 4 }
|
|
371
|
+
}],
|
|
372
|
+
pigNames: [[{
|
|
373
|
+
name: { type: 'string', minLength: 4 },
|
|
374
|
+
}]],
|
|
375
|
+
},
|
|
376
|
+
messages: {
|
|
377
|
+
'dogNames': { minLength: 'add one dog name' },
|
|
378
|
+
'dogNames.$': { minLength: 'add one sub dog name' },
|
|
379
|
+
|
|
380
|
+
'catNames.name': { minLength: 'min length error (name)' },
|
|
381
|
+
'catNames.1.name': { minLength: 'min length error (1)' },
|
|
382
|
+
'catNames.2.name': { minLength: 'min length error (2)' },
|
|
383
|
+
|
|
384
|
+
'pigNames.name': { minLength: 'min length error (name)' },
|
|
385
|
+
'pigNames.$.name': { minLength: 'min length error ($)' },
|
|
386
|
+
'pigNames.1.name': { minLength: 'min length error (1)' },
|
|
387
|
+
'pigNames.2.name': { minLength: 'min length error (2)' },
|
|
388
|
+
'pigNames.0.2.name': { minLength: 'min length error (deep 0 2)' },
|
|
389
|
+
'pigNames.$.2.name': { minLength: 'min length error (deep $ 2)' },
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
// Empty array
|
|
394
|
+
await expect(user.validate({ dogNames: [] })).rejects.toContainEqual({
|
|
395
|
+
status: '400',
|
|
396
|
+
title: 'dogNames',
|
|
397
|
+
detail: 'add one dog name',
|
|
398
|
+
meta: { rule: 'minLength', model: 'user', field: 'dogNames' }
|
|
399
|
+
})
|
|
400
|
+
// Empty sub array
|
|
401
|
+
await expect(user.validate({ dogNames: [['carla']] })).resolves.toEqual({ dogNames: [['carla']] })
|
|
402
|
+
await expect(user.validate({ dogNames: [[]] })).rejects.toContainEqual({
|
|
274
403
|
status: '400',
|
|
275
404
|
title: 'dogNames.0',
|
|
276
|
-
detail: '
|
|
405
|
+
detail: 'add one sub dog name',
|
|
277
406
|
meta: { rule: 'minLength', model: 'user', field: '0' }
|
|
278
407
|
})
|
|
279
408
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
})).rejects.toContainEqual({
|
|
409
|
+
|
|
410
|
+
// array-subdocument-field error (loose match)
|
|
411
|
+
await expect(user.validate({ catNames: [{ name: 'ben' }] })).rejects.toContainEqual({
|
|
284
412
|
status: '400',
|
|
285
|
-
title: '
|
|
286
|
-
detail: '
|
|
413
|
+
title: 'catNames.0.name',
|
|
414
|
+
detail: 'min length error (name)',
|
|
415
|
+
meta: { rule: 'minLength', model: 'user', field: 'name' }
|
|
416
|
+
})
|
|
417
|
+
// array-subdocument-1-field error
|
|
418
|
+
await expect(user.validate({ catNames: [{ name: 'carla' }, { name: 'ben' }] })).rejects.toContainEqual({
|
|
419
|
+
status: '400',
|
|
420
|
+
title: 'catNames.1.name',
|
|
421
|
+
detail: 'min length error (1)',
|
|
422
|
+
meta: { rule: 'minLength', model: 'user', field: 'name' }
|
|
423
|
+
})
|
|
424
|
+
// array-subdocument-2-field error
|
|
425
|
+
await expect(user.validate({ catNames: [{ name: 'carla' }, { name: 'carla' }, { name: 'ben' }] })).rejects.toContainEqual({
|
|
426
|
+
status: '400',
|
|
427
|
+
title: 'catNames.2.name',
|
|
428
|
+
detail: 'min length error (2)',
|
|
287
429
|
meta: { rule: 'minLength', model: 'user', field: 'name' }
|
|
288
430
|
})
|
|
289
431
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
})).rejects.toContainEqual({
|
|
432
|
+
|
|
433
|
+
// array-subdocument-field error (loose $ match)
|
|
434
|
+
await expect(user.validate({ pigNames: [[{ name: 'ben' }]] })).rejects.toContainEqual({
|
|
294
435
|
status: '400',
|
|
295
|
-
title: '
|
|
296
|
-
detail: '
|
|
436
|
+
title: 'pigNames.0.0.name',
|
|
437
|
+
detail: 'min length error ($)',
|
|
297
438
|
meta: { rule: 'minLength', model: 'user', field: 'name' }
|
|
298
439
|
})
|
|
440
|
+
// array-subdocument-1-field error
|
|
441
|
+
await expect(user.validate({ pigNames: [[{ name: 'carla' }, { name: 'ben' }]] })).rejects.toContainEqual({
|
|
442
|
+
status: '400',
|
|
443
|
+
title: 'pigNames.0.1.name',
|
|
444
|
+
detail: 'min length error (1)',
|
|
445
|
+
meta: { rule: 'minLength', model: 'user', field: 'name' }
|
|
446
|
+
})
|
|
447
|
+
// array-subdocument-0-2-field error
|
|
448
|
+
await expect(user.validate({ pigNames: [[{ name: 'carla' }, { name: 'carla' }, { name: 'ben' }]] })).rejects.toContainEqual({
|
|
449
|
+
status: '400',
|
|
450
|
+
title: 'pigNames.0.2.name',
|
|
451
|
+
detail: 'min length error (deep 0 2)',
|
|
452
|
+
meta: { rule: 'minLength', model: 'user', field: 'name' }
|
|
453
|
+
})
|
|
454
|
+
// array-subdocument-2-0-field error (fallback)
|
|
455
|
+
await expect(user.validate({ pigNames: [[],[],[{ name: 'carla' },{ name: 'carla' },{ name: 'ben' }]] })).rejects.toContainEqual({
|
|
456
|
+
status: '400',
|
|
457
|
+
title: 'pigNames.2.2.name',
|
|
458
|
+
detail: 'min length error (deep $ 2)',
|
|
459
|
+
meta: { rule: 'minLength', model: 'user', field: 'name' }
|
|
460
|
+
})
|
|
461
|
+
// array-subdocument-2-0-field error (lower fallback)
|
|
462
|
+
await expect(user.validate({ pigNames: [[],[],[{ name: 'ben' }]] })).rejects.toContainEqual({
|
|
463
|
+
status: '400',
|
|
464
|
+
title: 'pigNames.2.0.name',
|
|
465
|
+
detail: 'min length error (2)',
|
|
466
|
+
meta: { rule: 'minLength', model: 'user', field: 'name' }
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
test('Validation custom rules', async () => {
|
|
471
|
+
// Setup
|
|
472
|
+
let db = (await opendb(false)).db
|
|
473
|
+
let user = db.model('user', {
|
|
474
|
+
fields: {
|
|
475
|
+
name: { type: 'string', bigName: 8 },
|
|
476
|
+
animals: [{
|
|
477
|
+
name: { type: 'string', bigName: 8 }
|
|
478
|
+
}]
|
|
479
|
+
},
|
|
480
|
+
rules: {
|
|
481
|
+
bigName: function(value, ruleArg) {
|
|
482
|
+
return value.length >= ruleArg
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
// Basic field
|
|
488
|
+
await expect(user.validate({ name: 'benjamin' })).resolves.toEqual({ name: 'benjamin' })
|
|
489
|
+
await expect(user.validate({ name: 'ben' })).rejects.toContainEqual({
|
|
490
|
+
status: '400',
|
|
491
|
+
title: 'name',
|
|
492
|
+
detail: 'Invalid data property for rule "bigName".',
|
|
493
|
+
meta: { rule: 'bigName', model: 'user', field: 'name' }
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
// subdocument in an array
|
|
497
|
+
await expect(user.validate({ animals: [{ name: 'benjamin' }] })).resolves.toEqual({ animals: [{ name: 'benjamin' }] })
|
|
498
|
+
await expect(user.validate({ animals: [{ name: 'ben' }] })).rejects.toContainEqual({
|
|
499
|
+
status: '400',
|
|
500
|
+
title: 'animals.0.name',
|
|
501
|
+
detail: 'Invalid data property for rule "bigName".',
|
|
502
|
+
meta: { rule: 'bigName', model: 'user', field: 'name' }
|
|
503
|
+
})
|
|
299
504
|
})
|
|
300
505
|
|
|
301
506
|
test('Validated data', async () => {
|
|
@@ -399,7 +604,7 @@ module.exports = function(monastery, opendb) {
|
|
|
399
604
|
})
|
|
400
605
|
})
|
|
401
606
|
|
|
402
|
-
test('Schema rules', async () => {
|
|
607
|
+
test('Schema default rules', async () => {
|
|
403
608
|
// Setup
|
|
404
609
|
let db = (await opendb(false)).db
|
|
405
610
|
let user = db.model('user', { fields: {
|