monastery 1.42.2 → 2.1.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/changelog.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [2.1.0](https://github.com/boycce/monastery/compare/1.42.2...2.1.0) (2024-04-05)
6
+
7
+ ## [2.0.0](https://github.com/boycce/monastery/compare/1.42.2...2.0.0) (2023-12-06)
8
+
5
9
  ### [1.42.2](https://github.com/boycce/monastery/compare/1.42.1...1.42.2) (2023-10-31)
6
10
 
7
11
  ### [1.42.1](https://github.com/boycce/monastery/compare/1.42.0...1.42.1) (2023-10-09)
@@ -283,12 +283,12 @@ You are able to define custom error messages for each field rule.
283
283
  }
284
284
  },
285
285
  // Assign custom error messages for arrays
286
+ // e.g. pets = [{ name: { type: 'string' }}]
286
287
  'pets': {
287
288
  minLength: `Please add at least one pet pet group.`
288
289
  },
289
290
  // You can assign custom error messages for all fields on embedded documents in an array
290
- // e.g. pets = [{ name: { type: 'string' }}]
291
- 'pets.name': {
291
+ 'pets.$.name': {
292
292
  required: `Your pet's name needs to be a string.`
293
293
  },
294
294
  // To target a specific array item
@@ -12,6 +12,7 @@ To use the default image plugin shipped with monastery, you need to use the opti
12
12
  imagePlugin: {
13
13
  awsAcl: 'public-read', // default
14
14
  awsBucket: 'your-bucket-name',
15
+ awsRegion: undefined, // required when using getSignedUrl (e.g. 's3-ap-southeast-2')
15
16
  awsAccessKeyId: 'your-key-here',
16
17
  awsSecretAccessKey: 'your-key-here',
17
18
  filesize: undefined, // default (max filesize in bytes)
package/docs/readme.md CHANGED
@@ -105,6 +105,7 @@ Coming soon...
105
105
  - Remove leading forward slashes from custom image paths (AWS adds this as a seperate folder)
106
106
  - double check await db.model.remove({ query: idfromparam }) doesnt cause issues for null, undefined or '', but continue to allow {}
107
107
  - ~~can't insert/update model id (maybe we can allow this and add _id to default insert/update blacklists)~~
108
+ - timstamps are blacklisted by default (instead of the `timestamps` opt), and can be switched off via blacklisting
108
109
 
109
110
  ## Versions
110
111
 
@@ -65,29 +65,18 @@ module.exports = {
65
65
  _getMostSpecificKeyMatchingPath: function(object, path) {
66
66
  /**
67
67
  * Get all possible array variation matches from the object, and return the most specifc key
68
- * @param {object} object - e.g. { 'pets.1.name', 'pets.$.name', 'pets.name', .. }
68
+ * @param {object} object - messages, e.g. { 'pets.1.name', 'pets.$.name', 'pets.name', .. }
69
69
  * @path {string} path - must be a specifc path, e.g. 'pets.1.name'
70
70
  * @return most specific key in object
71
- *
72
- * 1. Get all viable messages keys, e.g. (key)dogs.$ == (path)dogs.1
73
- * 2. Order array key list by scoring, i.e. [0-9]=2, $=1, ''=0
74
- * 3. Return first
75
71
  */
76
- let keys = []
77
- let pathExpand = path.replace(/\.([0-9]+)/g, '(.$1|.\\$|)').replace(/\./g, '\\.')
78
- let pathreg = new RegExp(`^${pathExpand}$`)
79
-
80
- for (let key in object) {
81
- if (key.match(pathreg)) {
82
- let score = (key.match(/\.[0-9]+/g)||[]).length * 1001
83
- score += (key.match(/\.\$/g)||[]).length * 1000
84
- keys.push({ score: score, key: key })
72
+ let key
73
+ for (let k in object) {
74
+ if (path.match(object[k].regex)) {
75
+ key = k
76
+ break
85
77
  }
86
78
  }
87
-
88
- if (!keys.length) return
89
- else if (keys.length == 1) return keys[0].key
90
- return keys.sort((a, b) => a.score - b.score).reverse()[0].key // descending
79
+ return key
91
80
  },
92
81
 
93
82
  _validateFields: function(dataRoot, fields, data, opts, path) {
@@ -236,7 +225,7 @@ module.exports = {
236
225
  let rule = this.rules[ruleName] || rules[ruleName]
237
226
  let fieldName = path.match(/[^.]+$/)[0]
238
227
  let isDeepProp = path.match(/\./) // todo: not dot-notation
239
- let ruleMessageKey = this._getMostSpecificKeyMatchingPath(this.messages, path)
228
+ let ruleMessageKey = this.messagesLen && this._getMostSpecificKeyMatchingPath(this.messages, path)
240
229
  let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
241
230
  let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : opts.insert || isDeepProp
242
231
  if (!ruleMessage) ruleMessage = rule.message
package/lib/model.js CHANGED
@@ -43,14 +43,50 @@ let Model = module.exports = function(name, opts, manager) {
43
43
  ? !opts.insertBL.includes('_id') && !opts.insertBL.includes('-_id') ? ['_id'].concat(opts.insertBL) : opts.insertBL
44
44
  : ['_id'],
45
45
  fields: { ...(util.deepCopy(opts.fields) || {}) },
46
- findBL: opts.findBL || ['password'],
46
+ findBL: opts.findBL || ['password'], // todo: password should be removed
47
47
  manager: manager,
48
48
  messages: opts.messages || {},
49
+ messagesLen: Object.keys(opts.messages || {}).length > 0,
49
50
  name: name,
50
51
  rules: { ...(opts.rules || {}) },
51
52
  updateBL: opts.updateBL || [],
52
53
  })
53
54
 
55
+ // Sort messages by specifity first, then we can just return the first match
56
+ this.messages = Object
57
+ .keys(this.messages)
58
+ .sort((a, b) => {
59
+ function getScore(key) {
60
+ // Make sure the keys are sorted by specifity, e.g. the most specific keys are at the top
61
+ // That means the variable indexes need to be sorted last,
62
+ // e.g. 'gulls.1.name' is more specific than 'gulls.$.name'
63
+ // e.g. 'gulls.1.name' is more specific than 'gulls.1.$'
64
+ // e.g. 'gulls.1.$' is more specific than 'gulls.$.1'
65
+ // e.g. 'gulls.1.1.$' is more specific than 'gulls.$.1.1'
66
+ if (!key.match(/\.\$/)) return 0
67
+ let score = 0
68
+ let parts = key.split('.')
69
+ for (let i = 0; i < parts.length; i++) {
70
+ if (parts[i] == '$') score += 100 * (100 - i) // higher score is less specific
71
+ }
72
+ return score
73
+ }
74
+ const scoreA = getScore(a)
75
+ const scoreB = getScore(b)
76
+ // this.messages[a].score = scoreA
77
+ // this.messages[b].score = scoreB
78
+ return scoreA > scoreB ? 1 : (scoreA < scoreB ? -1 : 0)
79
+ })
80
+ .reduce((acc, key) => {
81
+ // Now covert the path to a regex
82
+ // e.g. pets.$.names.4.first => pets\.[0-9]+\.names\.4\.first
83
+ this.messages[key].regex = new RegExp(`^${key.replace(/\./g, '\\.').replace(/\.\$/g, '.[0-9]+')}$`)
84
+ // this.messages[key].regex = new RegExp(`^${key.replace(/\.\$/g, '(.[0-9]+|.)').replace(/\./g, '\\.')}$`)
85
+ // return an ordered object
86
+ acc[key] = this.messages[key]
87
+ return acc
88
+ }, {})
89
+
54
90
  // Run before model hooks
55
91
  for (let hook of this.manager.beforeModel) {
56
92
  hook(this)
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.42.2",
5
+ "version": "2.1.0",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -21,30 +21,36 @@
21
21
  "docs": "cd docs && bundle exec jekyll serve --livereload --livereload-port 4001",
22
22
  "lint": "eslint ./lib ./plugins ./test",
23
23
  "mong": "nodemon resources/mong.js",
24
+ "major": "standard-version --release-as major && npm publish",
24
25
  "minor": "standard-version --release-as minor && npm publish",
25
26
  "patch": "standard-version --release-as patch && npm publish",
26
27
  "release": "standard-version && npm publish && git push --tags",
27
28
  "test": "npm run lint && jest",
28
- "test-one-example": "jest -t images"
29
+ "test-one-example": "jest -t \"images addImages\" --watchAll"
29
30
  },
30
31
  "dependencies": {
31
- "aws-sdk": "2.1062.0",
32
- "debug": "4.1.1",
33
- "file-type": "15.0.0",
32
+ "@aws-sdk/client-s3": "^3.549.0",
33
+ "@aws-sdk/s3-request-presigner": "^3.549.0",
34
+ "debug": "^4.3.4",
35
+ "file-type": "^16.5.4",
34
36
  "monk": "7.3.4",
35
37
  "nanoid": "3.2.0",
36
38
  "validator": "13.7.0"
37
39
  },
38
40
  "devDependencies": {
39
- "body-parser": "1.19.0",
41
+ "body-parser": "^1.20.2",
40
42
  "eslint": "8.7.0",
41
- "express": "4.17.1",
42
- "express-fileupload": "1.2.0",
43
+ "express": "^4.19.2",
44
+ "express-fileupload": "^1.5.0",
43
45
  "jest": "27.4.7",
44
- "nodemon": "2.0.15",
46
+ "nodemon": "^3.1.0",
45
47
  "standard-version": "9.3.2",
46
48
  "supertest": "4.0.2"
47
49
  },
50
+ "engines": {
51
+ "node": ">=14",
52
+ "npm": ">=6"
53
+ },
48
54
  "standard-version": {
49
55
  "infile": "changelog.md",
50
56
  "releaseCommitMessageFormat": "{{currentTag}}",
@@ -1,5 +1,6 @@
1
1
  // requiring: nanoid, file-type, aws-sdk/clients/s3
2
- let util = require('../../lib/util')
2
+ const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
3
+ const util = require('../../lib/util')
3
4
 
4
5
  let plugin = module.exports = {
5
6
 
@@ -26,6 +27,7 @@ let plugin = module.exports = {
26
27
  this.awsBucket = options.awsBucket
27
28
  this.awsAccessKeyId = options.awsAccessKeyId
28
29
  this.awsSecretAccessKey = options.awsSecretAccessKey
30
+ this.awsRegion = options.awsRegion
29
31
  this.bucketDir = options.bucketDir || 'full' // depreciated > 1.36.2
30
32
  this.filesize = options.filesize
31
33
  this.formats = options.formats || ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'tiff']
@@ -42,10 +44,14 @@ let plugin = module.exports = {
42
44
  }
43
45
 
44
46
  // Create s3 'service' instance (defer require since it takes 120ms to load)
45
- // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property
47
+ // v2: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property
48
+ // v3: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3/
49
+ // v3 examples: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/javascript_s3_code_examples.html
46
50
  manager._getSignedUrl = this._getSignedUrl
47
- this.s3 = () => {
48
- return this._s3 || (this._s3 = new (require('aws-sdk/clients/s3'))({
51
+ this.getS3Client = () => {
52
+ const { S3 } = require('@aws-sdk/client-s3')
53
+ return this._s3Client || (this._s3Client = new S3({
54
+ region: this.awsRegion,
49
55
  credentials: {
50
56
  accessKeyId: this.awsAccessKeyId,
51
57
  secretAccessKey: this.awsSecretAccessKey
@@ -168,7 +174,7 @@ let plugin = module.exports = {
168
174
  plugin._addImageObjectsToData(filesArr.inputPath, data, image)
169
175
  resolve(s3Options)
170
176
  } else {
171
- plugin.s3().upload(s3Options, (err, response) => {
177
+ plugin.getS3Client().upload(s3Options, (err, response) => {
172
178
  if (err) return reject(err)
173
179
  plugin._addImageObjectsToData(filesArr.inputPath, data, image)
174
180
  resolve(s3Options)
@@ -203,7 +209,7 @@ let plugin = module.exports = {
203
209
  * Get signed urls for all image objects in data
204
210
  * @param {object} options - monastery operation options {model, query, files, ..}
205
211
  * @param {object} data
206
- * @return promise(data)
212
+ * @return promise() - mutates data
207
213
  * @this model
208
214
  */
209
215
  // Not wanting signed urls for this operation?
@@ -215,8 +221,9 @@ let plugin = module.exports = {
215
221
  if (options.getSignedUrls
216
222
  || (util.isDefined(imageField.getSignedUrl) ? imageField.getSignedUrl : plugin.getSignedUrl)) {
217
223
  let images = plugin._findImagesInData(doc, imageField, 0, '').filter(o => o.image)
224
+ // todo: we could do this in parallel
218
225
  for (let image of images) {
219
- image.image.signedUrl = plugin._getSignedUrl(image.image.path, 3600, imageField.awsBucket)
226
+ image.image.signedUrl = await plugin._getSignedUrl(image.image.path, 3600, imageField.awsBucket)
220
227
  }
221
228
  }
222
229
  }
@@ -365,7 +372,7 @@ let plugin = module.exports = {
365
372
  // the file doesnt get deleted, we only delete from plugin.awsBucket.
366
373
  if (!unused.length) return
367
374
  await new Promise((resolve, reject) => {
368
- plugin.s3().deleteObjects({
375
+ plugin.getS3Client().deleteObjects({
369
376
  Bucket: plugin.awsBucket,
370
377
  Delete: { Objects: unused }
371
378
  }, (err, data) => {
@@ -571,18 +578,23 @@ let plugin = module.exports = {
571
578
  return list
572
579
  },
573
580
 
574
- _getSignedUrl: (path, expires=3600, bucket) => {
581
+ _getSignedUrl: async (path, expires=3600, bucket) => {
575
582
  /**
576
583
  * @param {string} path - aws file path
577
584
  * @param {number} <expires> - seconds
578
585
  * @param {number} <bucket>
579
- * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
586
+ * @return {promise} signedUrl
587
+ * @see v2: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
588
+ * @see v3: https://github.com/aws/aws-sdk-js-v3/blob/main/UPGRADING.md#s3-presigned-url
580
589
  */
581
- let signedUrl = plugin.s3().getSignedUrl('getObject', {
582
- Bucket: bucket || plugin.awsBucket,
583
- Expires: expires,
584
- Key: path,
585
- })
590
+ if (!plugin.awsRegion) {
591
+ throw 'Monastery requires config.awsRegion to be defined when using getSignedUrl\'s'
592
+ }
593
+ const { GetObjectCommand } = require('@aws-sdk/client-s3')
594
+ const params = { Bucket: bucket || plugin.awsBucket, Key: path }
595
+ const command = new GetObjectCommand(params)
596
+ let signedUrl = await getSignedUrl(plugin.getS3Client(), command, { expiresIn: expires })
597
+ // console.log(signedUrl)
586
598
  return signedUrl
587
599
  },
588
600
 
package/test/model.js CHANGED
@@ -170,6 +170,134 @@ module.exports = function(monastery, opendb) {
170
170
  })
171
171
  })
172
172
 
173
+ test('model setup with messages', async () => {
174
+ // Setup
175
+ let db = (await opendb(false)).db
176
+ let user = db.model('user', {
177
+ fields: {
178
+ name: { type: 'string' },
179
+ },
180
+ messages: {
181
+ // these are sorted when trhe model's initialised
182
+ 'cats.name': {},
183
+
184
+ 'dogs.name': {},
185
+ 'dogs.$.name': {},
186
+ 'dogs.1.name': {},
187
+ 'dogs.$': {},
188
+ 'dogs.1': {},
189
+
190
+ 'pigs.name': {},
191
+ 'pigs.$.name': {},
192
+ 'pigs.1.name': {},
193
+ 'pigs.2.name': {},
194
+
195
+ 'gulls.$.1.$': {},
196
+ 'gulls.1.$.1': {},
197
+ 'gulls.$': {},
198
+ 'gulls.$.$': {},
199
+ 'gulls.$.$.1': {},
200
+ 'gulls.$.1': {},
201
+ 'gulls.1.$': {},
202
+ 'gulls.1.1': {},
203
+ 'gulls.1.1.$': {},
204
+ 'gulls.name': {},
205
+ 'gulls.$.name': {},
206
+ },
207
+ })
208
+ // Object with schema
209
+ // console.log(user.messages)
210
+ expect(Object.keys(user.messages)).toEqual([
211
+ 'cats.name',
212
+ 'dogs.name',
213
+ 'dogs.1.name',
214
+ 'dogs.1',
215
+ 'pigs.name',
216
+ 'pigs.1.name',
217
+ 'pigs.2.name',
218
+ 'gulls.1.1',
219
+ 'gulls.name',
220
+ 'gulls.1.1.$',
221
+ 'gulls.1.$.1',
222
+ 'gulls.1.$',
223
+ 'dogs.$.name',
224
+ 'dogs.$',
225
+ 'pigs.$.name',
226
+ 'gulls.$',
227
+ 'gulls.$.1',
228
+ 'gulls.$.name',
229
+ 'gulls.$.1.$',
230
+ 'gulls.$.$',
231
+ 'gulls.$.$.1',
232
+ ])
233
+
234
+ expect(user.messages).toEqual({
235
+ // these are sorted in model initialisation
236
+ 'cats.name': {
237
+ 'regex': /^cats\.name$/,
238
+ },
239
+ 'dogs.$': {
240
+ 'regex': /^dogs\.[0-9]+$/,
241
+ },
242
+ 'dogs.$.name': {
243
+ 'regex': /^dogs\.[0-9]+\.name$/,
244
+ },
245
+ 'dogs.1': {
246
+ 'regex': /^dogs\.1$/,
247
+ },
248
+ 'dogs.1.name': {
249
+ 'regex': /^dogs\.1\.name$/,
250
+ },
251
+ 'dogs.name': {
252
+ 'regex': /^dogs\.name$/,
253
+ },
254
+ 'gulls.$': {
255
+ 'regex': /^gulls\.[0-9]+$/,
256
+ },
257
+ 'gulls.$.$': {
258
+ 'regex': /^gulls\.[0-9]+\.[0-9]+$/,
259
+ },
260
+ 'gulls.$.$.1': {
261
+ 'regex': /^gulls\.[0-9]+\.[0-9]+\.1$/,
262
+ },
263
+ 'gulls.$.1': {
264
+ 'regex': /^gulls\.[0-9]+\.1$/,
265
+ },
266
+ 'gulls.$.1.$': {
267
+ 'regex': /^gulls\.[0-9]+\.1\.[0-9]+$/,
268
+ },
269
+ 'gulls.$.name': {
270
+ 'regex': /^gulls\.[0-9]+\.name$/,
271
+ },
272
+ 'gulls.1.$': {
273
+ 'regex': /^gulls\.1\.[0-9]+$/,
274
+ },
275
+ 'gulls.1.$.1': {
276
+ 'regex': /^gulls\.1\.[0-9]+\.1$/,
277
+ },
278
+ 'gulls.1.1': {
279
+ 'regex': /^gulls\.1\.1$/,
280
+ },
281
+ 'gulls.1.1.$': {
282
+ 'regex': /^gulls\.1\.1\.[0-9]+$/,
283
+ },
284
+ 'gulls.name': {
285
+ 'regex': /^gulls\.name$/,
286
+ },
287
+ 'pigs.$.name': {
288
+ 'regex': /^pigs\.[0-9]+\.name$/,
289
+ },
290
+ 'pigs.1.name': {
291
+ 'regex': /^pigs\.1\.name$/,
292
+ },
293
+ 'pigs.2.name': {
294
+ 'regex': /^pigs\.2\.name$/,
295
+ },
296
+ 'pigs.name': {
297
+ 'regex': /^pigs\.name$/,
298
+ },
299
+ })
300
+ }),
173
301
 
174
302
  test('model reserved rules', async () => {
175
303
  // Setup
@@ -803,6 +803,7 @@ module.exports = function(monastery, opendb) {
803
803
  awsBucket: 'fake',
804
804
  awsAccessKeyId: 'fake',
805
805
  awsSecretAccessKey: 'fake',
806
+ awsRegion: 's3-ap-southeast-2',
806
807
  getSignedUrl: true,
807
808
  },
808
809
  })).db
package/test/validate.js CHANGED
@@ -381,35 +381,43 @@ module.exports = function(monastery, opendb) {
381
381
  })
382
382
 
383
383
  test('validation getMostSpecificKeyMatchingPath', async () => {
384
- let fn = validate._getMostSpecificKeyMatchingPath
385
- let mock = {
386
- 'cats.name': true,
387
-
388
- 'dogs.name': true,
389
- 'dogs.$.name': true,
390
-
391
- 'pigs.name': true,
392
- 'pigs.$.name': true,
393
- 'pigs.1.name': true,
394
- 'pigs.2.name': true,
384
+ let db = (await opendb(false)).db
385
+ let user = db.model('user', {
386
+ fields: {
387
+ name: { type: 'string' },
388
+ },
389
+ messages: {
390
+ // these are sorted when trhe model's initialised
391
+ 'cats.name': {},
392
+
393
+ 'dogs.name': {},
394
+ 'dogs.$.name': {},
395
+
396
+ 'pigs.name': {},
397
+ 'pigs.$.name': {},
398
+ 'pigs.1.name': {},
399
+ 'pigs.2.name': {},
400
+
401
+ 'gulls.$': {},
402
+ 'gulls.$.$': {},
403
+ 'gulls.name': {},
404
+ 'gulls.$.name': {},
405
+ },
406
+ })
395
407
 
396
- 'gulls.$': true,
397
- 'gulls.$.$': true,
398
- 'gulls.name': true,
399
- 'gulls.$.name': true,
400
- }
408
+ let fn = validate._getMostSpecificKeyMatchingPath
401
409
  // subdocument
402
- expect(fn(mock, 'cats.name')).toEqual('cats.name')
410
+ expect(fn(user.messages, 'cats.name')).toEqual('cats.name')
403
411
  // array subdocuments
404
- expect(fn(mock, 'cats.1.name')).toEqual('cats.name')
405
- expect(fn(mock, 'dogs.1.name')).toEqual('dogs.$.name')
406
- expect(fn(mock, 'dogs.2.name')).toEqual('dogs.$.name')
407
- expect(fn(mock, 'pigs.1.name')).toEqual('pigs.1.name')
408
- expect(fn(mock, 'pigs.2.name')).toEqual('pigs.2.name')
409
- expect(fn(mock, 'pigs.3.name')).toEqual('pigs.$.name')
412
+ // expect(fn(user.messages, 'cats.1.name')).toEqual('cats.name') // no longer matches
413
+ expect(fn(user.messages, 'dogs.1.name')).toEqual('dogs.$.name')
414
+ expect(fn(user.messages, 'dogs.2.name')).toEqual('dogs.$.name')
415
+ expect(fn(user.messages, 'pigs.1.name')).toEqual('pigs.1.name')
416
+ expect(fn(user.messages, 'pigs.2.name')).toEqual('pigs.2.name')
417
+ expect(fn(user.messages, 'pigs.3.name')).toEqual('pigs.$.name')
410
418
  // array
411
- expect(fn(mock, 'gulls.1.2')).toEqual('gulls.$.$')
412
- expect(fn(mock, 'gulls.1')).toEqual('gulls.$')
419
+ expect(fn(user.messages, 'gulls.1.2')).toEqual('gulls.$.$')
420
+ expect(fn(user.messages, 'gulls.1')).toEqual('gulls.$')
413
421
  })
414
422
 
415
423
  test('validation default messages', async () => {
@@ -478,7 +486,7 @@ module.exports = function(monastery, opendb) {
478
486
  messages: {
479
487
  'name': { minLength: 'Oops min length is 4' },
480
488
  'dog.name': { minLength: 'Oops min length is 4' },
481
- 'dogNames': { minLength: 'Oops min length is 4' },
489
+ 'dogNames.$': { minLength: 'Oops min length is 4' },
482
490
  }
483
491
  })
484
492
 
@@ -519,24 +527,26 @@ module.exports = function(monastery, opendb) {
519
527
  catNames: [{
520
528
  name: { type: 'string', minLength: 4 }
521
529
  }],
522
- pigNames: [[{
523
- name: { type: 'string', minLength: 4 },
524
- }]],
530
+ pigNames: [
531
+ [{
532
+ name: { type: 'string', minLength: 4 },
533
+ }]
534
+ ],
525
535
  },
526
536
  messages: {
527
537
  'dogNames': { minLength: 'add one dog name' },
528
538
  'dogNames.$': { minLength: 'add one sub dog name' },
529
539
 
530
- 'catNames.name': { minLength: 'min length error (name)' },
540
+ 'catNames.$.name': { minLength: 'min length error (name)' },
531
541
  'catNames.1.name': { minLength: 'min length error (1)' },
532
542
  'catNames.2.name': { minLength: 'min length error (2)' },
533
543
 
534
- 'pigNames.name': { minLength: 'min length error (name)' },
535
- 'pigNames.$.name': { minLength: 'min length error ($)' },
536
- 'pigNames.1.name': { minLength: 'min length error (1)' },
537
- 'pigNames.2.name': { minLength: 'min length error (2)' },
538
- 'pigNames.0.2.name': { minLength: 'min length error (deep 0 2)' },
539
- 'pigNames.$.2.name': { minLength: 'min length error (deep $ 2)' },
544
+ // 'pigNames.$.$.name': { minLength: 'min length error (name)' },
545
+ 'pigNames.$.$.name': { minLength: 'min length error ($ $)' }, // catches
546
+ 'pigNames.$.1.name': { minLength: 'min length error ($ 1)' },
547
+ 'pigNames.2.$.name': { minLength: 'min length error (2 $)' },
548
+ 'pigNames.0.2.name': { minLength: 'min length error (0 2)' },
549
+ 'pigNames.$.2.name': { minLength: 'min length error ($ 2)' },
540
550
  }
541
551
  })
542
552
 
@@ -587,7 +597,7 @@ module.exports = function(monastery, opendb) {
587
597
  .rejects.toContainEqual({
588
598
  status: '400',
589
599
  title: 'pigNames.0.0.name',
590
- detail: 'min length error ($)',
600
+ detail: 'min length error ($ $)',
591
601
  meta: { rule: 'minLength', model: 'user', field: 'name' }
592
602
  })
593
603
  // array-subdocument-1-field error
@@ -595,7 +605,14 @@ module.exports = function(monastery, opendb) {
595
605
  .rejects.toContainEqual({
596
606
  status: '400',
597
607
  title: 'pigNames.0.1.name',
598
- detail: 'min length error (1)',
608
+ detail: 'min length error ($ 1)',
609
+ meta: { rule: 'minLength', model: 'user', field: 'name' }
610
+ })
611
+ // array-subdocument-2-0-field error (lower fallback)
612
+ await expect(user.validate({ pigNames: [[],[],[{ name: 'ben' }]] })).rejects.toContainEqual({
613
+ status: '400',
614
+ title: 'pigNames.2.0.name',
615
+ detail: 'min length error (2 $)',
599
616
  meta: { rule: 'minLength', model: 'user', field: 'name' }
600
617
  })
601
618
  // array-subdocument-0-2-field error
@@ -603,22 +620,15 @@ module.exports = function(monastery, opendb) {
603
620
  .rejects.toContainEqual({
604
621
  status: '400',
605
622
  title: 'pigNames.0.2.name',
606
- detail: 'min length error (deep 0 2)',
623
+ detail: 'min length error (0 2)',
607
624
  meta: { rule: 'minLength', model: 'user', field: 'name' }
608
625
  })
609
626
  // array-subdocument-2-0-field error (fallback)
610
- await expect(user.validate({ pigNames: [[],[],[{ name: 'carla' },{ name: 'carla' },{ name: 'ben' }]] }))
627
+ await expect(user.validate({ pigNames: [[], [{ name: 'carla' },{ name: 'carla' },{ name: 'ben' }], []] }))
611
628
  .rejects.toContainEqual({
612
629
  status: '400',
613
- title: 'pigNames.2.2.name',
614
- detail: 'min length error (deep $ 2)',
615
- meta: { rule: 'minLength', model: 'user', field: 'name' }
616
- })
617
- // array-subdocument-2-0-field error (lower fallback)
618
- await expect(user.validate({ pigNames: [[],[],[{ name: 'ben' }]] })).rejects.toContainEqual({
619
- status: '400',
620
- title: 'pigNames.2.0.name',
621
- detail: 'min length error (2)',
630
+ title: 'pigNames.1.2.name',
631
+ detail: 'min length error ($ 2)',
622
632
  meta: { rule: 'minLength', model: 'user', field: 'name' }
623
633
  })
624
634
  })