monastery 3.0.21 → 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,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.0.23](https://github.com/boycce/monastery/compare/3.0.22...3.0.23) (2024-05-25)
6
+
7
+ ### [3.0.22](https://github.com/boycce/monastery/compare/3.0.21...3.0.22) (2024-05-08)
8
+
5
9
  ### [3.0.21](https://github.com/boycce/monastery/compare/3.0.20...3.0.21) (2024-05-07)
6
10
 
7
11
  ### [3.0.20](https://github.com/boycce/monastery/compare/3.0.19...3.0.20) (2024-05-05)
package/lib/model-crud.js CHANGED
@@ -48,9 +48,9 @@ Model.prototype.insert = async function (opts) {
48
48
  let data = await this.validate(opts.data || {}, opts) // was { ...opts }
49
49
 
50
50
  // Insert
51
- await util.runSeries.call(this, this.beforeInsert.map(f => f.bind(opts, data)), 'beforeInsert')
51
+ data = await util.runSeries.call(this, this.beforeInsert.map(f => f.bind(opts)), 'beforeInsert', data)
52
52
  let response = await this._insert(data, util.omit(opts, this._queryOptions))
53
- await util.runSeries.call(this, this.afterInsert.map(f => f.bind(opts, response)), 'afterInsert')
53
+ response = await util.runSeries.call(this, this.afterInsert.map(f => f.bind(opts)), 'afterInsert', response)
54
54
 
55
55
  // Success/error
56
56
  if (opts.req && opts.respond) opts.req.res.json(response)
@@ -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 ` +
@@ -283,7 +288,8 @@ Model.prototype.update = async function (opts, type='update') {
283
288
  }
284
289
 
285
290
  // Hook: beforeUpdate (has access to original, non-validated opts.data)
286
- await util.runSeries.call(this, this.beforeUpdate.map(f => f.bind(opts, data||{})), 'beforeUpdate')
291
+ data = data || {}
292
+ data = await util.runSeries.call(this, this.beforeUpdate.map(f => f.bind(opts)), 'beforeUpdate', data)
287
293
 
288
294
  if (data && operators['$set']) {
289
295
  this.info(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
@@ -301,7 +307,7 @@ Model.prototype.update = async function (opts, type='update') {
301
307
  }
302
308
 
303
309
  // Update
304
- 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
305
311
  if (type == 'findOneAndUpdate') {
306
312
  response = update
307
313
  } else if (util.isDefined(update.upsertedId)) {
@@ -314,7 +320,7 @@ Model.prototype.update = async function (opts, type='update') {
314
320
 
315
321
  // Hook: afterUpdate (doesn't have access to validated data)
316
322
  if (response) {
317
- await util.runSeries.call(this, this.afterUpdate.map(f => f.bind(opts, response)), 'afterUpdate')
323
+ response = await util.runSeries.call(this, this.afterUpdate.map(f => f.bind(opts)), 'afterUpdate', response)
318
324
  }
319
325
 
320
326
  // Hook: afterFind if findOneAndUpdate
@@ -392,6 +398,9 @@ Model.prototype._getProjectionFromBlacklist = function (type, customBlacklist) {
392
398
  if (type == 'find') {
393
399
  util.forEach(this.fieldsFlattened, (schema, path) => {
394
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
+ }
395
404
  let deepBL = manager.models[schema.model][`${type}BL`] || []
396
405
  let pathWithoutArrays = path.replace(/\.0(\.|$)/, '$1')
397
406
  list = list.concat(deepBL.map(o => {
@@ -531,37 +540,40 @@ Model.prototype._queryObject = async function (opts, type, _one) {
531
540
  return opts
532
541
  }
533
542
 
534
- Model.prototype._pathBlacklisted = function (path, projection, matchDeepWhitelistedKeys=true) {
543
+ Model.prototype._pathBlacklisted = function (path, projectionInclusion, projectionKeys, matchDeepWhitelistedKeys=true) {
535
544
  /**
536
545
  * Checks if the path is blacklisted within a inclusion/exclusion projection
537
546
  * @param {string} path - path without array brackets e.g. '.[]'
538
- * @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
539
549
  * @param {boolean} matchDeepWhitelistedKeys - match deep whitelisted keys containing path
540
550
  * E.g. pets.color == pets.color.age
541
551
  * @return {boolean}
542
552
  */
543
- for (let key in projection) {
544
- if (projection[key]) {
553
+ if (projectionInclusion) {
554
+ for (let key of projectionKeys) {
545
555
  // Inclusion (whitelisted)
546
556
  // E.g. pets.color.age == pets.color.age (exact match)
547
557
  // E.g. pets.color.age == pets.color (path contains key)
548
- var inclusion = true
549
- if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return false
558
+ if (path.indexOf(key + '.') == 0 || path == key) return false
550
559
  if (matchDeepWhitelistedKeys) {
551
560
  // E.g. pets.color == pets.color.age (key contains path)
552
- if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '\\.'))) return false
561
+ if (key.indexOf(path + '.') == 0) return false
553
562
  }
554
- } else {
563
+ }
564
+ return true
565
+ } else {
566
+ for (let key of projectionKeys) {
555
567
  // Exclusion (blacklisted)
556
568
  // E.g. pets.color.age == pets.color.age (exact match)
557
569
  // E.g. pets.color.age == pets.color (path contains key)
558
- if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
570
+ if (path.indexOf(key + '.') == 0 || path == key) return true
559
571
  }
572
+ return false
560
573
  }
561
- return inclusion? true : false
562
574
  }
563
575
 
564
- Model.prototype._processAfterFind = function (data, projection={}, afterFindContext={}) {
576
+ Model.prototype._processAfterFind = async function (data, projection={}, afterFindContext={}) {
565
577
  /**
566
578
  * Todo: Maybe make this method public?
567
579
  * Recurses through fields that are models and populates missing default-fields and calls model.afterFind([fn,..])
@@ -576,16 +588,28 @@ Model.prototype._processAfterFind = function (data, projection={}, afterFindCont
576
588
  */
577
589
  // Recurse down from the parent model, ending with the parent model as the parent afterFind hook may
578
590
  // want to manipulate any populated models
579
- let callbackSeries = []
591
+ let seriesGroups = []
580
592
  let models = this.manager.models
581
- let parent = util.toArray(data).map(o => ({ dataRef: o, dataPath: '', dataFieldName: '', modelName: this.name }))
582
- let modelFields = this._recurseAndFindModels(data).concat(parent)
593
+ let isArray = util.isArray(data)
594
+ const projectionKeys = Object.keys(projection)
595
+ const projectionInclusion = projection[projectionKeys[0]] ? true : false // default false
596
+ if (!isArray) data = [data]
597
+ let modelFields = this
598
+ ._recurseAndFindModels(data)
599
+ .concat(data.map((o, i) => ({
600
+ dataRefParent: data,
601
+ dataRefKey: i,
602
+ dataPath: '',
603
+ dataFieldName: '',
604
+ modelName: this.name,
605
+ })))
583
606
 
584
607
  // Loop found model/deep-model data objects, and populate missing default-fields and call afterFind on each
585
608
  for (let item of modelFields) {
609
+ const dataRef = item.dataRefParent[item.dataRefKey]
586
610
  // Populate missing default fields if data !== null
587
611
  // NOTE: maybe only call functions if default is being set.. fine for now
588
- if (item.dataRef) {
612
+ if (dataRef) {
589
613
  for (const localSchemaFieldPath in models[item.modelName].fieldsFlattened) {
590
614
  const schema = models[item.modelName].fieldsFlattened[localSchemaFieldPath]
591
615
  if (!util.isDefined(schema.default) || localSchemaFieldPath.match(/^\.?(createdAt|updatedAt)$/)) continue
@@ -597,29 +621,41 @@ Model.prototype._processAfterFind = function (data, projection={}, afterFindCont
597
621
  .replace(/\.[0-9]+(\.|$)/g, '$1')
598
622
 
599
623
  // Ignore default fields that are blacklisted
600
- if (this._pathBlacklisted(fullPathWithoutArrays, projection)) continue
624
+ if (this._pathBlacklisted(fullPathWithoutArrays, projectionInclusion, projectionKeys)) continue
601
625
 
602
626
  // Set default value
603
627
  const value = util.isFunction(schema.default) ? schema.default(this.manager) : schema.default
604
- util.setDeepValue(item.dataRef, localSchemaFieldPath.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
628
+ util.setDeepValue(dataRef, localSchemaFieldPath.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
605
629
  }
606
630
  }
607
631
  // Collect all of the model's afterFind hooks
608
- for (let fn of models[item.modelName].afterFind) {
609
- callbackSeries.push(fn.bind(afterFindContext, item.dataRef))
632
+ if (models[item.modelName].afterFind.length) {
633
+ seriesGroups.push(
634
+ (async (_item) => {
635
+ const _modelName = _item.modelName
636
+ const _model = models[_modelName]
637
+ const _opts = { ...afterFindContext, afterFindName: _modelName }
638
+ const _dataRef = _item.dataRefParent[_item.dataRefKey]
639
+ _item.dataRefParent[_item.dataRefKey] = (
640
+ await util.runSeries.call(_model, _model.afterFind.map(f => f.bind(_opts)), 'afterFind', _dataRef)
641
+ )
642
+ }).bind(null, item)
643
+ )
610
644
  }
611
645
  }
612
- return util.runSeries.call(this, callbackSeries, 'afterFind').then(() => data)
646
+ for (let item of seriesGroups) await item()
647
+ return isArray ? data : data[0]
613
648
  }
614
649
 
615
- Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', modelPaths) {
650
+ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='') {
616
651
  /**
617
- * Returns a flattened list of models fields
652
+ * Returns a flattened list of models fields, sorted by depth
618
653
  * @param {object|array} dataArr
619
654
  * @param {string} <dataParentPath>
620
655
  * @this Model
621
656
  * @return [{
622
- * dataRef: { *fields here* },
657
+ * dataRefParent: { *fields here* },
658
+ * dataRefKey: 'fieldName',
623
659
  * dataPath: 'usersNewCompany',
624
660
  * dataFieldName: usersNewCompany,
625
661
  * modelName: company
@@ -627,16 +663,6 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', mo
627
663
  */
628
664
  let out = []
629
665
  let dataParentPath2 = dataParentPath.replace(/\.[0-9]+(\.|$)/g, '$1')
630
-
631
- // Get all model fields
632
- if (!modelPaths) {
633
- modelPaths = Object.keys(this.fieldsFlattened).reduce((acc, k) => {
634
- if (this.fieldsFlattened[k].model) {
635
- acc.push({ k: k, k2: k.replace(/\.[0-9]+(\.|$)/g, '$1') }) // e.g. [{k: 'pets.0.dog', 'k2': 'pets.dog'}, ...]
636
- }
637
- return acc
638
- }, [])
639
- }
640
666
 
641
667
  for (let data of util.toArray(dataArr)) {
642
668
  for (let key in data) {
@@ -645,54 +671,59 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', mo
645
671
  const dataPath2 = dataParentPath2 ? `${dataParentPath2}.${key}` : key
646
672
 
647
673
  // modelPath has to contain data path
648
- const modelPath = modelPaths.find(o => o.k2.match(new RegExp('^' + dataPath2 + '(\\.|$)')))
649
- if (!modelPath) continue
674
+ const modelField = this.modelFieldsArray.find(o => o.path2.match(new RegExp('^' + dataPath2 + '(\\.|$)')))
675
+ if (!modelField) continue
650
676
 
651
677
  // Exact path match
652
- const pathMatch = modelPath.k2 == dataPath2
678
+ const pathMatch = modelField.path2 == dataPath2
653
679
 
654
680
  // Ignore id values
655
681
  if (util.isId(data[key])) continue
656
682
 
657
683
  // Recurse through sub-document fields
658
- if (!pathMatch && util.isSubdocument(data[key])) {
659
- 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)]
660
686
 
661
687
  // Recurse through array of sub-documents
662
688
  } else if (!pathMatch && util.isSubdocument((data[key]||{})[0])) {
663
689
  for (let i=0, l=data[key].length; i<l; i++) {
664
- out = [...out, ...this._recurseAndFindModels(data[key][i], dataPath + '.' + i, modelPaths)]
690
+ out = [...out, ...this._recurseAndFindModels(data[key][i], dataPath + '.' + i)]
665
691
  }
666
692
 
667
693
  // Array of data models (schema field can be either a single or array of models, due to custom $lookup's)
668
694
  } else if (pathMatch && util.isObject(data[key])) {
669
695
  out.push({
670
- dataRef: data[key],
696
+ dataRefParent: data,
697
+ dataRefKey: key,
671
698
  dataPath: dataPath,
672
699
  dataFieldName: key,
673
- modelName: this.fieldsFlattened[modelPath.k].model,
700
+ modelName: this.fieldsFlattened[modelField.path].model,
674
701
  })
675
702
 
676
703
  // Array of data models (schema field can be either a single or array of models, due to custom $lookup's)
677
704
  } else if (pathMatch && util.isObject(data[key][0])) {
678
705
  for (let i=0, l=data[key].length; i<l; i++) {
679
706
  out.push({
680
- dataRef: data[key][i],
707
+ dataRefParent: data[key],
708
+ dataRefKey: i,
681
709
  dataPath: dataPath + '.' + i,
682
710
  dataFieldName: key,
683
- modelName: this.fieldsFlattened[modelPath.k].model,
711
+ modelName: this.fieldsFlattened[modelField.path].model,
684
712
  })
685
713
  }
686
714
  }
687
715
  }
688
716
  }
689
717
 
718
+ // Sort by dataPath length so that nested models are processed first
719
+ // note: this shouldn't matter anyway since we can't currently have nested populated models
720
+ out = out.sort((a, b) => b.dataPath.split('.').length - a.dataPath.split('.').length)
690
721
  return out
691
722
  }
692
723
 
693
724
  Model.prototype._queryOptions = [
694
725
  // todo: remove type properties
695
726
  'blacklist', 'data', 'find', 'findOneAndUpdate', 'insert', 'model', '_one', 'populate', 'project',
696
- 'projectionValidate', 'query', 'remove', 'req', 'respond', 'skipValidation', 'type', 'update',
727
+ 'projectionInclusion', 'projectionKeys', 'query', 'remove', 'req', 'respond', 'skipValidation', 'type', 'update',
697
728
  'validateUndefined',
698
729
  ]
@@ -18,26 +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
-
33
- await util.runSeries.call(this, this.beforeValidate.map(f => f.bind(opts, data)), 'beforeValidate')
36
+ data = await util.runSeries.call(this, this.beforeValidate.map(f => f.bind(opts)), 'beforeValidate', data)
34
37
 
35
38
  // Recurse and validate fields
39
+ // console.time('_validateFields')
36
40
  let response = util.toArray(data).map(item => {
37
- let validated = this._validateFields(item, this.fields, item, opts, '')
41
+ let validated = this._validateFields(item, this.fields, item, opts, '', '')
38
42
  if (validated[0].length) throw validated[0]
39
43
  else return validated[1]
40
44
  })
45
+ // console.timeEnd('_validateFields')
41
46
 
42
47
  // Single document?
43
48
  response = util.isArray(data)? response : response[0]
@@ -63,14 +68,17 @@ Model.prototype._getMostSpecificKeyMatchingPath = function (object, path) {
63
68
  return key
64
69
  }
65
70
 
66
- Model.prototype._validateFields = function (dataRoot, fields, data, opts, path) {
71
+ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parentPath, parentPath2) {
67
72
  /**
68
- * 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
+ *
69
76
  * @param {any} dataRoot
70
77
  * @param {object|array} fields
71
78
  * @param {any} data
72
79
  * @param {object} opts
73
- * @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
74
82
  * @return [errors, valid-data]
75
83
  * @this model
76
84
  *
@@ -78,19 +86,31 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, path)
78
86
  * Fields second recursion = [0]: { name: {}, color: {} }
79
87
  */
80
88
  let errors = []
81
- let data2 = util.isArray(fields)? [] : {}
89
+ let fieldsIsArray = util.isArray(fields)
90
+ let fieldsArray = fieldsIsArray ? fields : Object.keys(fields)
82
91
  let timestamps = util.isDefined(opts.timestamps) ? opts.timestamps : this.manager.opts.timestamps
92
+ let dataArray = util.forceArray(data)
93
+ let data2 = fieldsIsArray ? [] : {}
94
+
95
+ for (let i=0, l=dataArray.length; i<l; i++) {
96
+ const item = dataArray[i]
83
97
 
84
- util.forEach(util.forceArray(data), function(data, i) {
85
- util.forEach(fields, function(field, fieldName) {
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)
86
105
  let verrors = []
87
- let schema = field.schema || field
88
- let value = util.isArray(fields)? data : (data||{})[fieldName]
89
- let indexOrFieldName = util.isArray(fields)? i : fieldName
90
- let path2 = `${path}.${indexOrFieldName}`.replace(/^\./, '')
91
- let path3 = path2.replace(/(^|\.)[0-9]+(\.|$)/, '$2') // no numerical keys, e.g. pets.1.name = pets.name
92
- let isType = 'is' + util.ucFirst(schema.type)
93
- 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]
94
114
 
95
115
  // Timestamp overrides
96
116
  if (schema.timestampField) {
@@ -104,68 +124,70 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, path)
104
124
  }
105
125
  }
106
126
 
107
- // Ignore blacklisted
108
- if (this._pathBlacklisted(path3, opts.projectionValidate) && !schema.defaultOverride) return
109
127
  // Ignore insert only
110
- if (opts.update && schema.insertOnly) return
128
+ if (opts.update && schema.insertOnly) continue
111
129
  // Ignore virtual fields
112
- if (schema.virtual) return
130
+ if (schema.virtual) continue
131
+ // Ignore blacklisted
132
+ if (!schema.defaultOverride && this._pathBlacklisted(path2, opts.projectionInclusion, opts.projectionKeys)) continue
113
133
  // Type cast the value if tryParse is available, .e.g. isInteger.tryParse
114
- if (isTypeRule && util.isFunction(isTypeRule.tryParse)) {
115
- 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
116
136
  }
117
-
118
- // Schema field (ignore object/array schemas)
119
- if (util.isSchema(field) && fieldName !== 'schema') {
120
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
121
- if (util.isDefined(value) && !verrors.length) data2[indexOrFieldName] = value
122
-
123
- // Fields can be a subdocument
124
- } else if (util.isSubdocument(field)) {
137
+
138
+ // Field is a subdocument
139
+ if (schema.isObject) {
125
140
  // Object schema errors
126
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
141
+ errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
127
142
  // Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
128
143
  if (
129
144
  opts.insert ||
130
145
  util.isObject(value) ||
131
- (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path2||'').match(/\./))
146
+ (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path||'').indexOf('.') !== -1)
132
147
  ) {
133
- var res = this._validateFields(dataRoot, field, value, opts, path2)
148
+ var res = this._validateFields(dataRoot, field, value, opts, path, path2)
134
149
  errors.push(...res[0])
135
150
  }
136
151
  if (util.isDefined(value) && !verrors.length) {
137
152
  data2[indexOrFieldName] = res? res[1] : value
138
153
  }
139
154
 
140
- // Fields can be an array
141
- } else if (util.isArray(field)) {
155
+ // Field is an array
156
+ } else if (schema.isArray) {
142
157
  // Array schema errors
143
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
158
+ errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
144
159
  // Data value is array too
145
160
  if (util.isArray(value)) {
146
- var res2 = this._validateFields(dataRoot, field, value, opts, path2)
161
+ var res2 = this._validateFields(dataRoot, field, value, opts, path, path2)
147
162
  errors.push(...res2[0])
148
163
  }
149
164
  if (util.isDefined(value) && !verrors.length) {
150
165
  data2[indexOrFieldName] = res2? res2[1] : value
151
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
152
172
  }
153
- }, this)
154
- }, this)
173
+ // if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
174
+ }
175
+ }
155
176
 
156
177
  // Normalise array indexes and return
157
- if (util.isArray(fields)) data2 = data2.filter(() => true)
178
+ if (fieldsIsArray) data2 = data2.filter(() => true) //todo: remove???
158
179
  if (data === null) data2 = null
159
180
  return [errors, data2]
160
181
  }
161
182
 
162
- Model.prototype._validateRules = function (dataRoot, field, value, opts, path) {
183
+ Model.prototype._validateRules = function (dataRoot, fieldSchema, value, opts, path) {
163
184
  /**
164
185
  * Validate all the field's rules
165
186
  * @param {object} dataRoot - data
166
- * @param {object} field - field schema
167
- * @param {string} path - full field path
187
+ * @param {object} fieldSchema - field schema
188
+ * @param {any} value - data value
168
189
  * @param {object} opts - original validate() options
190
+ * @param {string} path - full field path, e.g. pets.1.name
169
191
  * @return {array} errors
170
192
  * @this model
171
193
  */
@@ -193,47 +215,49 @@ Model.prototype._validateRules = function (dataRoot, field, value, opts, path) {
193
215
  }
194
216
  }
195
217
 
196
- for (let ruleName in field) {
218
+ for (let ruleName in fieldSchema) {
197
219
  if (this._ignoredRules.indexOf(ruleName) > -1) continue
198
- 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)
199
221
  if (error && ruleName == 'required') return [error] // only show the required error
200
222
  if (error) errors.push(error)
201
223
  }
202
224
  return errors
203
225
  }
204
226
 
205
- Model.prototype._validateRule = function (dataRoot, ruleName, field, ruleArg, value, opts, path) {
206
- // 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)
207
229
  // Remove [] from the message path, and simply ignore non-numeric children to test for all array items
208
- ruleArg = ruleArg === true? undefined : ruleArg
230
+ ruleArg = ruleArg === true ? undefined : ruleArg
209
231
  let rule = this.rules[ruleName] || rules[ruleName]
210
- let fieldName = path.match(/[^.]+$/)[0]
211
- let isDeepProp = path.match(/\./) // todo: not dot-notation
212
- let ruleMessageKey = this.messagesLen && this._getMostSpecificKeyMatchingPath(this.messages, path)
213
- let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
214
- let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : opts.insert || isDeepProp
215
- if (!ruleMessage) ruleMessage = rule.message
232
+ let validateUndefined = typeof opts.validateUndefined != 'undefined'
233
+ ? opts.validateUndefined
234
+ : opts.insert || path.includes('.') // is a deep property
216
235
 
217
236
  // Undefined value
218
237
  if (typeof value === 'undefined' && (!validateUndefined || !rule.validateUndefined)) return
219
238
 
220
239
  // Ignore null (if nullObject is set on objects or arrays)
221
- 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
222
241
 
223
242
  // Ignore null
224
- if (value === null && !(field.isObject || field.isArray) && !rule.validateNull) return
243
+ if (value === null && !(fieldSchema.isObject || fieldSchema.isArray) && !rule.validateNull) return
225
244
 
226
245
  // Ignore empty strings
227
246
  if (value === '' && !rule.validateEmptyString) return
228
247
 
229
248
  // Rule failed
230
- if (!rule.fn.call(dataRoot, value, ruleArg, path, this)) return {
231
- detail: util.isFunction(ruleMessage)
232
- ? ruleMessage.call(dataRoot, value, ruleArg, path, this)
233
- : ruleMessage,
234
- meta: { rule: ruleName, model: this.name, field: fieldName, detailLong: rule.messageLong },
235
- status: '400',
236
- 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
+ }
237
261
  }
238
262
  }
239
263
 
@@ -242,5 +266,5 @@ Model.prototype._ignoredRules = [
242
266
  // todo: need to remove filesize and formats..
243
267
  'awsAcl', 'awsBucket', 'default', 'defaultOverride', 'filename', 'filesize', 'fileSize', 'formats',
244
268
  'image', 'index', 'insertOnly', 'model', 'nullObject', 'params', 'path', 'getSignedUrl', 'timestampField',
245
- 'type', 'virtual',
269
+ 'type', 'isType', 'isSchema', 'virtual',
246
270
  ]