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 +4 -0
- package/lib/model-crud.js +84 -53
- package/lib/model-validate.js +91 -67
- package/lib/model.js +90 -50
- package/lib/util.js +24 -13
- package/package.json +1 -1
- package/plugins/images/index.js +9 -9
- package/test/blacklisting.js +2 -1
- package/test/crud.js +185 -6
- 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,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
|
|
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
|
|
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.
|
|
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 ` +
|
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
544
|
-
|
|
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
|
-
|
|
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.
|
|
561
|
+
if (key.indexOf(path + '.') == 0) return false
|
|
553
562
|
}
|
|
554
|
-
}
|
|
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.
|
|
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
|
|
591
|
+
let seriesGroups = []
|
|
580
592
|
let models = this.manager.models
|
|
581
|
-
let
|
|
582
|
-
|
|
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 (
|
|
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,
|
|
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(
|
|
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
|
-
|
|
609
|
-
|
|
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
|
-
|
|
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=''
|
|
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
|
-
*
|
|
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
|
|
649
|
-
if (!
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
696
|
+
dataRefParent: data,
|
|
697
|
+
dataRefKey: key,
|
|
671
698
|
dataPath: dataPath,
|
|
672
699
|
dataFieldName: key,
|
|
673
|
-
modelName: this.fieldsFlattened[
|
|
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
|
-
|
|
707
|
+
dataRefParent: data[key],
|
|
708
|
+
dataRefKey: i,
|
|
681
709
|
dataPath: dataPath + '.' + i,
|
|
682
710
|
dataFieldName: key,
|
|
683
|
-
modelName: this.fieldsFlattened[
|
|
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
|
-
'
|
|
727
|
+
'projectionInclusion', 'projectionKeys', 'query', 'remove', 'req', 'respond', 'skipValidation', 'type', 'update',
|
|
697
728
|
'validateUndefined',
|
|
698
729
|
]
|
package/lib/model-validate.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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}
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
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
|
|
88
|
-
let value =
|
|
89
|
-
let indexOrFieldName =
|
|
90
|
-
let
|
|
91
|
-
let
|
|
92
|
-
|
|
93
|
-
|
|
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)
|
|
128
|
+
if (opts.update && schema.insertOnly) continue
|
|
111
129
|
// Ignore virtual fields
|
|
112
|
-
if (schema.virtual)
|
|
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 &&
|
|
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
|
-
//
|
|
119
|
-
if (
|
|
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,
|
|
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 : (
|
|
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
|
-
//
|
|
141
|
-
} else if (
|
|
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,
|
|
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
|
-
|
|
154
|
-
|
|
173
|
+
// if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
155
176
|
|
|
156
177
|
// Normalise array indexes and return
|
|
157
|
-
if (
|
|
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,
|
|
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}
|
|
167
|
-
* @param {
|
|
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
|
|
218
|
+
for (let ruleName in fieldSchema) {
|
|
197
219
|
if (this._ignoredRules.indexOf(ruleName) > -1) continue
|
|
198
|
-
let error = this._validateRule(dataRoot, ruleName,
|
|
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,
|
|
206
|
-
// this.debug(path,
|
|
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
|
|
211
|
-
|
|
212
|
-
|
|
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 && (
|
|
240
|
+
if (value === null && (fieldSchema.isObject || fieldSchema.isArray) && fieldSchema.nullObject && !rule.validateNull) return
|
|
222
241
|
|
|
223
242
|
// Ignore null
|
|
224
|
-
if (value === null && !(
|
|
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))
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
]
|