monastery 3.0.22 → 3.0.23

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,8 @@
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
+ ### [3.0.23](https://github.com/boycce/monastery/compare/3.0.22...3.0.23) (2024-05-25)
6
+
5
7
  ### [3.0.22](https://github.com/boycce/monastery/compare/3.0.21...3.0.22) (2024-05-08)
6
8
 
7
9
  ### [3.0.21](https://github.com/boycce/monastery/compare/3.0.20...3.0.21) (2024-05-07)
package/lib/model-crud.js CHANGED
@@ -83,6 +83,8 @@ Model.prototype.find = async function (opts, _one) {
83
83
  // Get projection
84
84
  if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
85
85
  else opts.projection = this._getProjectionFromBlacklist(opts.type, opts.blacklist)
86
+ opts.projectionKeys = Object.keys(opts.projection || {})
87
+ opts.projectionInclusion = (opts.projection || {})[opts.projectionKeys[0]] ? true : false // default false
86
88
 
87
89
  // Has text search?
88
90
  // if (opts.query.$text) {
@@ -95,17 +97,19 @@ Model.prototype.find = async function (opts, _one) {
95
97
  var response = await this[`_find${opts._one? 'One' : ''}`](opts.query, util.omit(opts, this._queryOptions))
96
98
  } else {
97
99
  loop: for (let item of opts.populate) {
98
- let path = util.isObject(item)? item.as : item
100
+ let path = util.isObject(item) ? item.as : item
99
101
  // Blacklisted?
100
- if (this._pathBlacklisted(path, opts.projection)) continue loop
102
+ if (this._pathBlacklisted(path, opts.projectionInclusion, opts.projectionKeys)) continue loop
101
103
  // Custom $lookup definition
102
104
  // https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-lookup-expr
103
105
  if (util.isObject(item)) {
104
106
  lookups.push({ $lookup: item })
107
+ // String populate, e.g. 'comments.author'
105
108
  } else {
106
109
  let arrayTarget
107
110
  let arrayCount = 0
108
- let schema = path.split('.').reduce((o, i) => {
111
+ // Find the field type for the path
112
+ let field = path.split('.').reduce((o, i) => {
109
113
  if (util.isArray(o[i])) {
110
114
  arrayCount++
111
115
  arrayTarget = true
@@ -115,7 +119,8 @@ Model.prototype.find = async function (opts, _one) {
115
119
  return o[i]
116
120
  }
117
121
  }, this.fields)
118
- let modelName = (schema||{}).model
122
+
123
+ let modelName = ((field||{}).schema||{}).model
119
124
  if (!modelName) {
120
125
  this.error(
121
126
  `The field "${path}" passed to populate is not of type model. You would ` +
@@ -302,7 +307,7 @@ Model.prototype.update = async function (opts, type='update') {
302
307
  }
303
308
 
304
309
  // Update
305
- let update = await this['_' + type](opts.query, operators, util.omit(opts, this._queryOptions))
310
+ let update = await this['_' + type](opts.query, operators, util.omit(opts, this._queryOptions)) // 50ms for 100k
306
311
  if (type == 'findOneAndUpdate') {
307
312
  response = update
308
313
  } else if (util.isDefined(update.upsertedId)) {
@@ -393,6 +398,9 @@ Model.prototype._getProjectionFromBlacklist = function (type, customBlacklist) {
393
398
  if (type == 'find') {
394
399
  util.forEach(this.fieldsFlattened, (schema, path) => {
395
400
  if (!schema.model) return
401
+ if (!manager.models[schema.model]) {
402
+ throw new Error(`The model "${schema.model}" referenced from the model "${this.name}" doesn't exist.`)
403
+ }
396
404
  let deepBL = manager.models[schema.model][`${type}BL`] || []
397
405
  let pathWithoutArrays = path.replace(/\.0(\.|$)/, '$1')
398
406
  list = list.concat(deepBL.map(o => {
@@ -532,34 +540,37 @@ Model.prototype._queryObject = async function (opts, type, _one) {
532
540
  return opts
533
541
  }
534
542
 
535
- Model.prototype._pathBlacklisted = function (path, projection, matchDeepWhitelistedKeys=true) {
543
+ Model.prototype._pathBlacklisted = function (path, projectionInclusion, projectionKeys, matchDeepWhitelistedKeys=true) {
536
544
  /**
537
545
  * Checks if the path is blacklisted within a inclusion/exclusion projection
538
546
  * @param {string} path - path without array brackets e.g. '.[]'
539
- * @param {object} projection - inclusion/exclusion projection, not mixed
547
+ * @param {boolean} projectionInclusion - is a inclusion or exclusion projection (default is exclusion)
548
+ * @param {array} projectionKeys - inclusion/exclusion projection keys, not mixed
540
549
  * @param {boolean} matchDeepWhitelistedKeys - match deep whitelisted keys containing path
541
550
  * E.g. pets.color == pets.color.age
542
551
  * @return {boolean}
543
552
  */
544
- for (let key in projection) {
545
- if (projection[key]) {
553
+ if (projectionInclusion) {
554
+ for (let key of projectionKeys) {
546
555
  // Inclusion (whitelisted)
547
556
  // E.g. pets.color.age == pets.color.age (exact match)
548
557
  // E.g. pets.color.age == pets.color (path contains key)
549
- var inclusion = true
550
- if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return false
558
+ if (path.indexOf(key + '.') == 0 || path == key) return false
551
559
  if (matchDeepWhitelistedKeys) {
552
560
  // E.g. pets.color == pets.color.age (key contains path)
553
- if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '\\.'))) return false
561
+ if (key.indexOf(path + '.') == 0) return false
554
562
  }
555
- } else {
563
+ }
564
+ return true
565
+ } else {
566
+ for (let key of projectionKeys) {
556
567
  // Exclusion (blacklisted)
557
568
  // E.g. pets.color.age == pets.color.age (exact match)
558
569
  // E.g. pets.color.age == pets.color (path contains key)
559
- if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
570
+ if (path.indexOf(key + '.') == 0 || path == key) return true
560
571
  }
572
+ return false
561
573
  }
562
- return inclusion? true : false
563
574
  }
564
575
 
565
576
  Model.prototype._processAfterFind = async function (data, projection={}, afterFindContext={}) {
@@ -580,6 +591,8 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
580
591
  let seriesGroups = []
581
592
  let models = this.manager.models
582
593
  let isArray = util.isArray(data)
594
+ const projectionKeys = Object.keys(projection)
595
+ const projectionInclusion = projection[projectionKeys[0]] ? true : false // default false
583
596
  if (!isArray) data = [data]
584
597
  let modelFields = this
585
598
  ._recurseAndFindModels(data)
@@ -608,7 +621,7 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
608
621
  .replace(/\.[0-9]+(\.|$)/g, '$1')
609
622
 
610
623
  // Ignore default fields that are blacklisted
611
- if (this._pathBlacklisted(fullPathWithoutArrays, projection)) continue
624
+ if (this._pathBlacklisted(fullPathWithoutArrays, projectionInclusion, projectionKeys)) continue
612
625
 
613
626
  // Set default value
614
627
  const value = util.isFunction(schema.default) ? schema.default(this.manager) : schema.default
@@ -634,7 +647,7 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
634
647
  return isArray ? data : data[0]
635
648
  }
636
649
 
637
- Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', modelPaths) {
650
+ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='') {
638
651
  /**
639
652
  * Returns a flattened list of models fields, sorted by depth
640
653
  * @param {object|array} dataArr
@@ -650,16 +663,6 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', mo
650
663
  */
651
664
  let out = []
652
665
  let dataParentPath2 = dataParentPath.replace(/\.[0-9]+(\.|$)/g, '$1')
653
-
654
- // Get all model fields
655
- if (!modelPaths) {
656
- modelPaths = Object.keys(this.fieldsFlattened).reduce((acc, k) => {
657
- if (this.fieldsFlattened[k].model) {
658
- acc.push({ k: k, k2: k.replace(/\.[0-9]+(\.|$)/g, '$1') }) // e.g. [{k: 'pets.0.dog', 'k2': 'pets.dog'}, ...]
659
- }
660
- return acc
661
- }, [])
662
- }
663
666
 
664
667
  for (let data of util.toArray(dataArr)) {
665
668
  for (let key in data) {
@@ -668,23 +671,23 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', mo
668
671
  const dataPath2 = dataParentPath2 ? `${dataParentPath2}.${key}` : key
669
672
 
670
673
  // modelPath has to contain data path
671
- const modelPath = modelPaths.find(o => o.k2.match(new RegExp('^' + dataPath2 + '(\\.|$)')))
672
- if (!modelPath) continue
674
+ const modelField = this.modelFieldsArray.find(o => o.path2.match(new RegExp('^' + dataPath2 + '(\\.|$)')))
675
+ if (!modelField) continue
673
676
 
674
677
  // Exact path match
675
- const pathMatch = modelPath.k2 == dataPath2
678
+ const pathMatch = modelField.path2 == dataPath2
676
679
 
677
680
  // Ignore id values
678
681
  if (util.isId(data[key])) continue
679
682
 
680
683
  // Recurse through sub-document fields
681
- if (!pathMatch && util.isSubdocument(data[key])) {
682
- out = [...out, ...this._recurseAndFindModels(data[key], dataPath, modelPaths)]
684
+ if (!pathMatch && util.isSubdocument(data[key])) { /// subdocument? wouldn't isObject be better?
685
+ out = [...out, ...this._recurseAndFindModels(data[key], dataPath)]
683
686
 
684
687
  // Recurse through array of sub-documents
685
688
  } else if (!pathMatch && util.isSubdocument((data[key]||{})[0])) {
686
689
  for (let i=0, l=data[key].length; i<l; i++) {
687
- out = [...out, ...this._recurseAndFindModels(data[key][i], dataPath + '.' + i, modelPaths)]
690
+ out = [...out, ...this._recurseAndFindModels(data[key][i], dataPath + '.' + i)]
688
691
  }
689
692
 
690
693
  // Array of data models (schema field can be either a single or array of models, due to custom $lookup's)
@@ -694,7 +697,7 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', mo
694
697
  dataRefKey: key,
695
698
  dataPath: dataPath,
696
699
  dataFieldName: key,
697
- modelName: this.fieldsFlattened[modelPath.k].model,
700
+ modelName: this.fieldsFlattened[modelField.path].model,
698
701
  })
699
702
 
700
703
  // Array of data models (schema field can be either a single or array of models, due to custom $lookup's)
@@ -705,7 +708,7 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', mo
705
708
  dataRefKey: i,
706
709
  dataPath: dataPath + '.' + i,
707
710
  dataFieldName: key,
708
- modelName: this.fieldsFlattened[modelPath.k].model,
711
+ modelName: this.fieldsFlattened[modelField.path].model,
709
712
  })
710
713
  }
711
714
  }
@@ -721,6 +724,6 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', mo
721
724
  Model.prototype._queryOptions = [
722
725
  // todo: remove type properties
723
726
  'blacklist', 'data', 'find', 'findOneAndUpdate', 'insert', 'model', '_one', 'populate', 'project',
724
- 'projectionValidate', 'query', 'remove', 'req', 'respond', 'skipValidation', 'type', 'update',
727
+ 'projectionInclusion', 'projectionKeys', 'query', 'remove', 'req', 'respond', 'skipValidation', 'type', 'update',
725
728
  'validateUndefined',
726
729
  ]
@@ -18,25 +18,31 @@ Model.prototype.validate = async function (data, opts) {
18
18
  * @return promise(errors[] || pruned data{})
19
19
  * @this model
20
20
  */
21
- data = util.deepCopy(data)
21
+ // console.time('1')
22
+ data = util.deepCopy(data) // 30ms for 100k fields
23
+ // console.timeEnd('1')
22
24
  opts = opts || {}
23
25
  opts.update = opts.update || opts.findOneAndUpdate
24
26
  opts.insert = !opts.update
25
27
  opts.skipValidation = opts.skipValidation === true ? true : util.toArray(opts.skipValidation||[])
26
28
 
27
29
  // Get projection
28
- if (opts.project) opts.projectionValidate = this._getProjectionFromProject(opts.project)
29
- else opts.projectionValidate = this._getProjectionFromBlacklist(opts.update ? 'update' : 'insert', opts.blacklist)
30
+ if (opts.project) var projectionValidate = this._getProjectionFromProject(opts.project)
31
+ else projectionValidate = this._getProjectionFromBlacklist(opts.update ? 'update' : 'insert', opts.blacklist)
32
+ opts.projectionKeys = Object.keys(projectionValidate || {})
33
+ opts.projectionInclusion = (projectionValidate || {})[opts.projectionKeys[0]] ? true : false // default false
30
34
 
31
35
  // Hook: beforeValidate
32
36
  data = await util.runSeries.call(this, this.beforeValidate.map(f => f.bind(opts)), 'beforeValidate', data)
33
37
 
34
38
  // Recurse and validate fields
39
+ // console.time('_validateFields')
35
40
  let response = util.toArray(data).map(item => {
36
- let validated = this._validateFields(item, this.fields, item, opts, '')
41
+ let validated = this._validateFields(item, this.fields, item, opts, '', '')
37
42
  if (validated[0].length) throw validated[0]
38
43
  else return validated[1]
39
44
  })
45
+ // console.timeEnd('_validateFields')
40
46
 
41
47
  // Single document?
42
48
  response = util.isArray(data)? response : response[0]
@@ -62,14 +68,17 @@ Model.prototype._getMostSpecificKeyMatchingPath = function (object, path) {
62
68
  return key
63
69
  }
64
70
 
65
- Model.prototype._validateFields = function (dataRoot, fields, data, opts, path) {
71
+ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parentPath, parentPath2) {
66
72
  /**
67
- * Recurse through and retrieve any errors and valid data
73
+ * Recurse through and retrieve any errors and valid data (this needs to be perfomant)
74
+ * Note: This is now super fast, it can validate 100k possible fields in 235ms
75
+ *
68
76
  * @param {any} dataRoot
69
77
  * @param {object|array} fields
70
78
  * @param {any} data
71
79
  * @param {object} opts
72
- * @param {string} path
80
+ * @param {string} parentPath - data localised parent, e.g. pets.1.name
81
+ * @param {string} parentPath2 - no numerical keys, e.g. pets.name
73
82
  * @return [errors, valid-data]
74
83
  * @this model
75
84
  *
@@ -77,19 +86,31 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, path)
77
86
  * Fields second recursion = [0]: { name: {}, color: {} }
78
87
  */
79
88
  let errors = []
80
- let data2 = util.isArray(fields)? [] : {}
89
+ let fieldsIsArray = util.isArray(fields)
90
+ let fieldsArray = fieldsIsArray ? fields : Object.keys(fields)
81
91
  let timestamps = util.isDefined(opts.timestamps) ? opts.timestamps : this.manager.opts.timestamps
92
+ let dataArray = util.forceArray(data)
93
+ let data2 = fieldsIsArray ? [] : {}
82
94
 
83
- util.forEach(util.forceArray(data), function(data, i) {
84
- util.forEach(fields, function(field, fieldName) {
95
+ for (let i=0, l=dataArray.length; i<l; i++) {
96
+ const item = dataArray[i]
97
+
98
+ for (let m=0, n=fieldsArray.length; m<n; m++) {
99
+ // iterations++
100
+ const fieldName = fieldsIsArray ? m : fieldsArray[m] // array|object key
101
+ const field = fields[fieldName]
102
+ if (fieldName == 'schema') continue
103
+ // if (!parentPath && fieldName == 'categories') console.time(fieldName)
104
+ // if (!parentPath && fieldName == 'categories') console.time(fieldName + 1)
85
105
  let verrors = []
86
- let schema = field.schema || field
87
- let value = util.isArray(fields)? data : (data||{})[fieldName]
88
- let indexOrFieldName = util.isArray(fields)? i : fieldName
89
- let path2 = `${path}.${indexOrFieldName}`.replace(/^\./, '')
90
- let path3 = path2.replace(/(^|\.)[0-9]+(\.|$)/, '$2') // no numerical keys, e.g. pets.1.name = pets.name
91
- let isType = 'is' + util.ucFirst(schema.type)
92
- let isTypeRule = this.rules[isType] || rules[isType]
106
+ let schema = field.schema
107
+ let value = fieldsIsArray ? item : (item||{})[fieldName]
108
+ let indexOrFieldName = fieldsIsArray ? i : fieldName
109
+ let path = `${parentPath}.${indexOrFieldName}`
110
+ let path2 = fieldsIsArray ? parentPath2 : `${parentPath2}.${fieldName}`
111
+ if (path[0] == '.') path = path.slice(1) // remove leading dot, e.g. .pets.1.name
112
+ if (path2[0] == '.') path2 = path2.slice(1) // remove leading dot, e.g. .pets.1.name
113
+ let isTypeRule = this.rules[schema.isType] || rules[schema.isType]
93
114
 
94
115
  // Timestamp overrides
95
116
  if (schema.timestampField) {
@@ -103,68 +124,70 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, path)
103
124
  }
104
125
  }
105
126
 
106
- // Ignore blacklisted
107
- if (this._pathBlacklisted(path3, opts.projectionValidate) && !schema.defaultOverride) return
108
127
  // Ignore insert only
109
- if (opts.update && schema.insertOnly) return
128
+ if (opts.update && schema.insertOnly) continue
110
129
  // Ignore virtual fields
111
- if (schema.virtual) return
130
+ if (schema.virtual) continue
131
+ // Ignore blacklisted
132
+ if (!schema.defaultOverride && this._pathBlacklisted(path2, opts.projectionInclusion, opts.projectionKeys)) continue
112
133
  // Type cast the value if tryParse is available, .e.g. isInteger.tryParse
113
- if (isTypeRule && util.isFunction(isTypeRule.tryParse)) {
114
- value = isTypeRule.tryParse.call(dataRoot, value, fieldName, this)
134
+ if (isTypeRule && typeof isTypeRule.tryParse == 'function') {
135
+ value = isTypeRule.tryParse.call(dataRoot, value, fieldName, this) // 80ms // DISABLE
115
136
  }
116
-
117
- // Schema field (ignore object/array schemas)
118
- if (util.isSchema(field) && fieldName !== 'schema') {
119
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
120
- if (util.isDefined(value) && !verrors.length) data2[indexOrFieldName] = value
121
-
122
- // Fields can be a subdocument
123
- } else if (util.isSubdocument(field)) {
137
+
138
+ // Field is a subdocument
139
+ if (schema.isObject) {
124
140
  // Object schema errors
125
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
141
+ errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
126
142
  // Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
127
143
  if (
128
144
  opts.insert ||
129
145
  util.isObject(value) ||
130
- (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path2||'').match(/\./))
146
+ (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path||'').indexOf('.') !== -1)
131
147
  ) {
132
- var res = this._validateFields(dataRoot, field, value, opts, path2)
148
+ var res = this._validateFields(dataRoot, field, value, opts, path, path2)
133
149
  errors.push(...res[0])
134
150
  }
135
151
  if (util.isDefined(value) && !verrors.length) {
136
152
  data2[indexOrFieldName] = res? res[1] : value
137
153
  }
138
154
 
139
- // Fields can be an array
140
- } else if (util.isArray(field)) {
155
+ // Field is an array
156
+ } else if (schema.isArray) {
141
157
  // Array schema errors
142
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
158
+ errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
143
159
  // Data value is array too
144
160
  if (util.isArray(value)) {
145
- var res2 = this._validateFields(dataRoot, field, value, opts, path2)
161
+ var res2 = this._validateFields(dataRoot, field, value, opts, path, path2)
146
162
  errors.push(...res2[0])
147
163
  }
148
164
  if (util.isDefined(value) && !verrors.length) {
149
165
  data2[indexOrFieldName] = res2? res2[1] : value
150
166
  }
167
+
168
+ // Field is a field-type/field-schema
169
+ } else {
170
+ errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
171
+ if (util.isDefined(value) && !verrors.length) data2[indexOrFieldName] = value
151
172
  }
152
- }, this)
153
- }, this)
173
+ // if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
174
+ }
175
+ }
154
176
 
155
177
  // Normalise array indexes and return
156
- if (util.isArray(fields)) data2 = data2.filter(() => true)
178
+ if (fieldsIsArray) data2 = data2.filter(() => true) //todo: remove???
157
179
  if (data === null) data2 = null
158
180
  return [errors, data2]
159
181
  }
160
182
 
161
- Model.prototype._validateRules = function (dataRoot, field, value, opts, path) {
183
+ Model.prototype._validateRules = function (dataRoot, fieldSchema, value, opts, path) {
162
184
  /**
163
185
  * Validate all the field's rules
164
186
  * @param {object} dataRoot - data
165
- * @param {object} field - field schema
166
- * @param {string} path - full field path
187
+ * @param {object} fieldSchema - field schema
188
+ * @param {any} value - data value
167
189
  * @param {object} opts - original validate() options
190
+ * @param {string} path - full field path, e.g. pets.1.name
168
191
  * @return {array} errors
169
192
  * @this model
170
193
  */
@@ -192,47 +215,49 @@ Model.prototype._validateRules = function (dataRoot, field, value, opts, path) {
192
215
  }
193
216
  }
194
217
 
195
- for (let ruleName in field) {
218
+ for (let ruleName in fieldSchema) {
196
219
  if (this._ignoredRules.indexOf(ruleName) > -1) continue
197
- let error = this._validateRule(dataRoot, ruleName, field, field[ruleName], value, opts, path)
220
+ let error = this._validateRule(dataRoot, ruleName, fieldSchema, fieldSchema[ruleName], value, opts, path)
198
221
  if (error && ruleName == 'required') return [error] // only show the required error
199
222
  if (error) errors.push(error)
200
223
  }
201
224
  return errors
202
225
  }
203
226
 
204
- Model.prototype._validateRule = function (dataRoot, ruleName, field, ruleArg, value, opts, path) {
205
- // this.debug(path, field, ruleName, ruleArg, value)
227
+ Model.prototype._validateRule = function (dataRoot, ruleName, fieldSchema, ruleArg, value, opts, path) {
228
+ // this.debug(path, fieldSchema, ruleName, ruleArg, value)
206
229
  // Remove [] from the message path, and simply ignore non-numeric children to test for all array items
207
- ruleArg = ruleArg === true? undefined : ruleArg
230
+ ruleArg = ruleArg === true ? undefined : ruleArg
208
231
  let rule = this.rules[ruleName] || rules[ruleName]
209
- let fieldName = path.match(/[^.]+$/)[0]
210
- let isDeepProp = path.match(/\./) // todo: not dot-notation
211
- let ruleMessageKey = this.messagesLen && this._getMostSpecificKeyMatchingPath(this.messages, path)
212
- let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
213
- let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : opts.insert || isDeepProp
214
- if (!ruleMessage) ruleMessage = rule.message
232
+ let validateUndefined = typeof opts.validateUndefined != 'undefined'
233
+ ? opts.validateUndefined
234
+ : opts.insert || path.includes('.') // is a deep property
215
235
 
216
236
  // Undefined value
217
237
  if (typeof value === 'undefined' && (!validateUndefined || !rule.validateUndefined)) return
218
238
 
219
239
  // Ignore null (if nullObject is set on objects or arrays)
220
- if (value === null && (field.isObject || field.isArray) && field.nullObject && !rule.validateNull) return
240
+ if (value === null && (fieldSchema.isObject || fieldSchema.isArray) && fieldSchema.nullObject && !rule.validateNull) return
221
241
 
222
242
  // Ignore null
223
- if (value === null && !(field.isObject || field.isArray) && !rule.validateNull) return
243
+ if (value === null && !(fieldSchema.isObject || fieldSchema.isArray) && !rule.validateNull) return
224
244
 
225
245
  // Ignore empty strings
226
246
  if (value === '' && !rule.validateEmptyString) return
227
247
 
228
248
  // Rule failed
229
- if (!rule.fn.call(dataRoot, value, ruleArg, path, this)) return {
230
- detail: util.isFunction(ruleMessage)
231
- ? ruleMessage.call(dataRoot, value, ruleArg, path, this)
232
- : ruleMessage,
233
- meta: { rule: ruleName, model: this.name, field: fieldName, detailLong: rule.messageLong },
234
- status: '400',
235
- title: path,
249
+ if (!rule.fn.call(dataRoot, value, ruleArg, path, this)) {
250
+ let ruleMessageKey = this.messagesLen && this._getMostSpecificKeyMatchingPath(this.messages, path)
251
+ let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
252
+ if (!ruleMessage) ruleMessage = rule.message
253
+ return {
254
+ detail: util.isFunction(ruleMessage)
255
+ ? ruleMessage.call(dataRoot, value, ruleArg, path, this)
256
+ : ruleMessage,
257
+ meta: { rule: ruleName, model: this.name, field: path.match(/[^.]+$/)[0], detailLong: rule.messageLong },
258
+ status: '400',
259
+ title: path,
260
+ }
236
261
  }
237
262
  }
238
263
 
@@ -241,5 +266,5 @@ Model.prototype._ignoredRules = [
241
266
  // todo: need to remove filesize and formats..
242
267
  'awsAcl', 'awsBucket', 'default', 'defaultOverride', 'filename', 'filesize', 'fileSize', 'formats',
243
268
  'image', 'index', 'insertOnly', 'model', 'nullObject', 'params', 'path', 'getSignedUrl', 'timestampField',
244
- 'type', 'virtual',
269
+ 'type', 'isType', 'isSchema', 'virtual',
245
270
  ]