monastery 3.0.22 → 3.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
+ ## [3.1.0](https://github.com/boycce/monastery/compare/3.0.23...3.1.0) (2024-05-27)
6
+
7
+ ### [3.0.23](https://github.com/boycce/monastery/compare/3.0.22...3.0.23) (2024-05-25)
8
+
5
9
  ### [3.0.22](https://github.com/boycce/monastery/compare/3.0.21...3.0.22) (2024-05-08)
6
10
 
7
11
  ### [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,32 @@ 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 ? [] : {}
94
+ let notStrict = fields.schema.strict === false
82
95
 
83
- util.forEach(util.forceArray(data), function(data, i) {
84
- util.forEach(fields, function(field, fieldName) {
96
+ for (let i=0, l=dataArray.length; i<l; i++) {
97
+ const item = dataArray[i]
98
+
99
+ for (let m=0, n=fieldsArray.length; m<n; m++) {
100
+ // iterations++
101
+ const fieldName = fieldsIsArray ? m : fieldsArray[m] // array|object key
102
+ const field = fields[fieldName]
103
+ if (fieldName == 'schema') continue
104
+ // if (!parentPath && fieldName == 'categories') console.time(fieldName)
105
+ // if (!parentPath && fieldName == 'categories') console.time(fieldName + 1)
85
106
  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]
107
+ let schema = field.schema
108
+ let value = fieldsIsArray ? item : (item||{})[fieldName]
109
+ let indexOrFieldName = fieldsIsArray ? i : fieldName
110
+ let path = `${parentPath}.${indexOrFieldName}`
111
+ let path2 = fieldsIsArray ? parentPath2 : `${parentPath2}.${fieldName}`
112
+ if (path[0] == '.') path = path.slice(1) // remove leading dot, e.g. .pets.1.name
113
+ if (path2[0] == '.') path2 = path2.slice(1) // remove leading dot, e.g. .pets.1.name
114
+ let isTypeRule = this.rules[schema.isType] || rules[schema.isType]
93
115
 
94
116
  // Timestamp overrides
95
117
  if (schema.timestampField) {
@@ -103,68 +125,79 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, path)
103
125
  }
104
126
  }
105
127
 
106
- // Ignore blacklisted
107
- if (this._pathBlacklisted(path3, opts.projectionValidate) && !schema.defaultOverride) return
108
128
  // Ignore insert only
109
- if (opts.update && schema.insertOnly) return
129
+ if (opts.update && schema.insertOnly) continue
110
130
  // Ignore virtual fields
111
- if (schema.virtual) return
131
+ if (schema.virtual) continue
132
+ // Ignore blacklisted
133
+ if (!schema.defaultOverride && this._pathBlacklisted(path2, opts.projectionInclusion, opts.projectionKeys)) continue
112
134
  // 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)
135
+ if (isTypeRule && typeof isTypeRule.tryParse == 'function') {
136
+ value = isTypeRule.tryParse.call(dataRoot, value, fieldName, this) // 80ms // DISABLE
115
137
  }
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)) {
138
+
139
+ // Field is a subdocument
140
+ if (schema.isObject) {
124
141
  // Object schema errors
125
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
142
+ errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
126
143
  // Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
127
144
  if (
128
145
  opts.insert ||
129
146
  util.isObject(value) ||
130
- (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path2||'').match(/\./))
147
+ (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path||'').indexOf('.') !== -1)
131
148
  ) {
132
- var res = this._validateFields(dataRoot, field, value, opts, path2)
149
+ var res = this._validateFields(dataRoot, field, value, opts, path, path2)
133
150
  errors.push(...res[0])
134
151
  }
135
152
  if (util.isDefined(value) && !verrors.length) {
136
153
  data2[indexOrFieldName] = res? res[1] : value
137
154
  }
138
155
 
139
- // Fields can be an array
140
- } else if (util.isArray(field)) {
156
+ // Field is an array
157
+ } else if (schema.isArray) {
141
158
  // Array schema errors
142
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
159
+ errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
143
160
  // Data value is array too
144
161
  if (util.isArray(value)) {
145
- var res2 = this._validateFields(dataRoot, field, value, opts, path2)
162
+ var res2 = this._validateFields(dataRoot, field, value, opts, path, path2)
146
163
  errors.push(...res2[0])
147
164
  }
148
165
  if (util.isDefined(value) && !verrors.length) {
149
166
  data2[indexOrFieldName] = res2? res2[1] : value
150
167
  }
168
+
169
+ // Field is a field-type/field-schema
170
+ } else {
171
+ errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
172
+ if (util.isDefined(value) && !verrors.length) data2[indexOrFieldName] = value
151
173
  }
152
- }, this)
153
- }, this)
174
+ // if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
175
+ }
176
+
177
+ // Add any extra fields that are not in the schema. Item maybe false when inserting (from recursing above)
178
+ if (notStrict && !fieldsIsArray && item) {
179
+ const allDataKeys = Object.keys(item)
180
+ for (let m=0, n=allDataKeys.length; m<n; m++) {
181
+ const key = allDataKeys[m]
182
+ if (!fieldsArray.includes(key)) data2[key] = item[key]
183
+ }
184
+ }
185
+ }
154
186
 
155
187
  // Normalise array indexes and return
156
- if (util.isArray(fields)) data2 = data2.filter(() => true)
188
+ if (fieldsIsArray) data2 = data2.filter(() => true) //todo: remove???
157
189
  if (data === null) data2 = null
158
190
  return [errors, data2]
159
191
  }
160
192
 
161
- Model.prototype._validateRules = function (dataRoot, field, value, opts, path) {
193
+ Model.prototype._validateRules = function (dataRoot, fieldSchema, value, opts, path) {
162
194
  /**
163
195
  * Validate all the field's rules
164
196
  * @param {object} dataRoot - data
165
- * @param {object} field - field schema
166
- * @param {string} path - full field path
197
+ * @param {object} fieldSchema - field schema
198
+ * @param {any} value - data value
167
199
  * @param {object} opts - original validate() options
200
+ * @param {string} path - full field path, e.g. pets.1.name
168
201
  * @return {array} errors
169
202
  * @this model
170
203
  */
@@ -192,47 +225,49 @@ Model.prototype._validateRules = function (dataRoot, field, value, opts, path) {
192
225
  }
193
226
  }
194
227
 
195
- for (let ruleName in field) {
228
+ for (let ruleName in fieldSchema) {
196
229
  if (this._ignoredRules.indexOf(ruleName) > -1) continue
197
- let error = this._validateRule(dataRoot, ruleName, field, field[ruleName], value, opts, path)
230
+ let error = this._validateRule(dataRoot, ruleName, fieldSchema, fieldSchema[ruleName], value, opts, path)
198
231
  if (error && ruleName == 'required') return [error] // only show the required error
199
232
  if (error) errors.push(error)
200
233
  }
201
234
  return errors
202
235
  }
203
236
 
204
- Model.prototype._validateRule = function (dataRoot, ruleName, field, ruleArg, value, opts, path) {
205
- // this.debug(path, field, ruleName, ruleArg, value)
237
+ Model.prototype._validateRule = function (dataRoot, ruleName, fieldSchema, ruleArg, value, opts, path) {
238
+ // this.debug(path, fieldSchema, ruleName, ruleArg, value)
206
239
  // Remove [] from the message path, and simply ignore non-numeric children to test for all array items
207
- ruleArg = ruleArg === true? undefined : ruleArg
240
+ ruleArg = ruleArg === true ? undefined : ruleArg
208
241
  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
242
+ let validateUndefined = typeof opts.validateUndefined != 'undefined'
243
+ ? opts.validateUndefined
244
+ : opts.insert || path.includes('.') // is a deep property
215
245
 
216
246
  // Undefined value
217
247
  if (typeof value === 'undefined' && (!validateUndefined || !rule.validateUndefined)) return
218
248
 
219
249
  // Ignore null (if nullObject is set on objects or arrays)
220
- if (value === null && (field.isObject || field.isArray) && field.nullObject && !rule.validateNull) return
250
+ if (value === null && (fieldSchema.isObject || fieldSchema.isArray) && fieldSchema.nullObject && !rule.validateNull) return
221
251
 
222
252
  // Ignore null
223
- if (value === null && !(field.isObject || field.isArray) && !rule.validateNull) return
253
+ if (value === null && !(fieldSchema.isObject || fieldSchema.isArray) && !rule.validateNull) return
224
254
 
225
255
  // Ignore empty strings
226
256
  if (value === '' && !rule.validateEmptyString) return
227
257
 
228
258
  // 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,
259
+ if (!rule.fn.call(dataRoot, value, ruleArg, path, this)) {
260
+ let ruleMessageKey = this.messagesLen && this._getMostSpecificKeyMatchingPath(this.messages, path)
261
+ let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
262
+ if (!ruleMessage) ruleMessage = rule.message
263
+ return {
264
+ detail: util.isFunction(ruleMessage)
265
+ ? ruleMessage.call(dataRoot, value, ruleArg, path, this)
266
+ : ruleMessage,
267
+ meta: { rule: ruleName, model: this.name, field: path.match(/[^.]+$/)[0], detailLong: rule.messageLong },
268
+ status: '400',
269
+ title: path,
270
+ }
236
271
  }
237
272
  }
238
273
 
@@ -241,5 +276,5 @@ Model.prototype._ignoredRules = [
241
276
  // todo: need to remove filesize and formats..
242
277
  'awsAcl', 'awsBucket', 'default', 'defaultOverride', 'filename', 'filesize', 'fileSize', 'formats',
243
278
  'image', 'index', 'insertOnly', 'model', 'nullObject', 'params', 'path', 'getSignedUrl', 'timestampField',
244
- 'type', 'virtual',
279
+ 'type', 'isType', 'isSchema', 'virtual', 'strict',
245
280
  ]