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 +2 -0
- package/lib/model-crud.js +39 -36
- package/lib/model-validate.js +90 -65
- package/lib/model.js +90 -50
- package/lib/util.js +12 -2
- package/package.json +1 -1
- package/plugins/images/index.js +9 -9
- package/test/blacklisting.js +2 -1
- package/test/crud.js +62 -0
- package/test/manager.js +1 -1
- package/test/model.js +125 -47
- package/test/plugin-images.js +38 -16
- package/test/virtuals.js +2 -2
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
545
|
-
|
|
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
|
-
|
|
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.
|
|
561
|
+
if (key.indexOf(path + '.') == 0) return false
|
|
554
562
|
}
|
|
555
|
-
}
|
|
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.
|
|
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,
|
|
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=''
|
|
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
|
|
672
|
-
if (!
|
|
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 =
|
|
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
|
|
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
|
|
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[
|
|
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[
|
|
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
|
-
'
|
|
727
|
+
'projectionInclusion', 'projectionKeys', 'query', 'remove', 'req', 'respond', 'skipValidation', 'type', 'update',
|
|
725
728
|
'validateUndefined',
|
|
726
729
|
]
|
package/lib/model-validate.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
29
|
-
else
|
|
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,
|
|
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}
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
87
|
-
let value =
|
|
88
|
-
let indexOrFieldName =
|
|
89
|
-
let
|
|
90
|
-
let
|
|
91
|
-
|
|
92
|
-
|
|
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)
|
|
128
|
+
if (opts.update && schema.insertOnly) continue
|
|
110
129
|
// Ignore virtual fields
|
|
111
|
-
if (schema.virtual)
|
|
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 &&
|
|
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
|
-
//
|
|
118
|
-
if (
|
|
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,
|
|
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 : (
|
|
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
|
-
//
|
|
140
|
-
} else if (
|
|
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,
|
|
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
|
-
|
|
153
|
-
|
|
173
|
+
// if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
154
176
|
|
|
155
177
|
// Normalise array indexes and return
|
|
156
|
-
if (
|
|
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,
|
|
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}
|
|
166
|
-
* @param {
|
|
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
|
|
218
|
+
for (let ruleName in fieldSchema) {
|
|
196
219
|
if (this._ignoredRules.indexOf(ruleName) > -1) continue
|
|
197
|
-
let error = this._validateRule(dataRoot, ruleName,
|
|
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,
|
|
205
|
-
// this.debug(path,
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
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 && (
|
|
240
|
+
if (value === null && (fieldSchema.isObject || fieldSchema.isArray) && fieldSchema.nullObject && !rule.validateNull) return
|
|
221
241
|
|
|
222
242
|
// Ignore null
|
|
223
|
-
if (value === null && !(
|
|
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))
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
]
|