monastery 2.2.3 → 3.0.1

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.
@@ -1,263 +1,245 @@
1
- let util = require('./util.js')
2
- let rules = require('./rules.js')
3
-
4
- module.exports = {
5
-
6
- validate: async function(data, opts, cb) {
7
- /**
8
- * Validates a model
9
- * @param {object} data
10
- * @param {object} <opts>
11
- * @param {array|string|false} <opts.blacklist> - augment insertBL/updateBL, `false` will remove blacklisting
12
- * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
13
- * @param {array|string|true} <opts.skipValidation> - skip validation on these fields
14
- * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
15
- * updated, depending on the `options.update` value
16
- * @param {boolean(false)} <opts.update> - are we validating for insert or update? todo: change to `type`
17
- * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
18
- * default, but false on update
19
- * @param {function} <cb> - instead of returning a promise
20
- * @return promise(errors[] || pruned data{})
21
- * @this model
22
- */
23
-
24
- // Optional cb and opts
25
- if (util.isFunction(opts)) {
26
- cb = opts; opts = undefined
27
- }
28
- if (cb && !util.isFunction(cb)) {
29
- throw new Error(`The callback passed to ${this.name}.validate() is not a function`)
30
- }
31
- try {
32
- data = util.deepCopy(data)
33
- opts = opts || {}
34
- opts.update = opts.update || opts.findOneAndUpdate
35
- opts.insert = !opts.update
36
- opts.skipValidation = opts.skipValidation === true ? true : util.toArray(opts.skipValidation||[])
37
-
38
- // Get projection
39
- if (opts.project) opts.projectionValidate = this._getProjectionFromProject(opts.project)
40
- else opts.projectionValidate = this._getProjectionFromBlacklist(opts.update ? 'update' : 'insert', opts.blacklist)
41
-
42
- // Hook: beforeValidate
43
- await util.runSeries(this.beforeValidate.map(f => f.bind(opts, data)))
44
-
45
- // Recurse and validate fields
46
- let response = util.toArray(data).map(item => {
47
- let validated = this._validateFields(item, this.fields, item, opts, '')
48
- if (validated[0].length) throw validated[0]
49
- else return validated[1]
50
- })
51
-
52
- // Single document?
53
- response = util.isArray(data)? response : response[0]
54
-
55
- // Success/error
56
- if (cb) cb(null, response)
57
- else return Promise.resolve(response)
1
+ const util = require('./util.js')
2
+ const rules = require('./rules.js')
3
+ const Model = require('./model.js')
4
+
5
+ Model.prototype.validate = async function (data, opts) {
6
+ /**
7
+ * Validates a model
8
+ * @param {object} data
9
+ * @param {object} <opts>
10
+ * @param {array|string|false} <opts.blacklist> - augment insertBL/updateBL, `false` will remove blacklisting
11
+ * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
12
+ * @param {array|string|true} <opts.skipValidation> - skip validation on these fields
13
+ * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
14
+ * updated, depending on the `options.update` value
15
+ * @param {boolean(false)} <opts.update> - are we validating for insert or update? todo: change to `type`
16
+ * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
17
+ * default, but false on update
18
+ * @return promise(errors[] || pruned data{})
19
+ * @this model
20
+ */
21
+ data = util.deepCopy(data)
22
+ opts = opts || {}
23
+ opts.update = opts.update || opts.findOneAndUpdate
24
+ opts.insert = !opts.update
25
+ opts.skipValidation = opts.skipValidation === true ? true : util.toArray(opts.skipValidation||[])
26
+
27
+ // 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
+
31
+ // Hook: beforeValidate
32
+ await util.runSeries(this.beforeValidate.map(f => f.bind(opts, data)))
33
+
34
+ // Recurse and validate fields
35
+ let response = util.toArray(data).map(item => {
36
+ let validated = this._validateFields(item, this.fields, item, opts, '')
37
+ if (validated[0].length) throw validated[0]
38
+ else return validated[1]
39
+ })
40
+
41
+ // Single document?
42
+ response = util.isArray(data)? response : response[0]
43
+
44
+ // Success/error
45
+ return Promise.resolve(response)
46
+ }
58
47
 
59
- } catch (e) {
60
- if (cb) cb(e)
61
- else throw e
48
+ Model.prototype._getMostSpecificKeyMatchingPath = function (object, path) {
49
+ /**
50
+ * Get all possible array variation matches from the object, and return the most specifc key
51
+ * @param {object} object - messages, e.g. { 'pets.1.name', 'pets.$.name', 'pets.name', .. }
52
+ * @path {string} path - must be a specifc path, e.g. 'pets.1.name'
53
+ * @return most specific key in object
54
+ */
55
+ let key
56
+ for (let k in object) {
57
+ if (path.match(object[k].regex)) {
58
+ key = k
59
+ break
62
60
  }
63
- },
61
+ }
62
+ return key
63
+ }
64
64
 
65
- _getMostSpecificKeyMatchingPath: function(object, path) {
66
- /**
67
- * Get all possible array variation matches from the object, and return the most specifc key
68
- * @param {object} object - messages, e.g. { 'pets.1.name', 'pets.$.name', 'pets.name', .. }
69
- * @path {string} path - must be a specifc path, e.g. 'pets.1.name'
70
- * @return most specific key in object
71
- */
72
- let key
73
- for (let k in object) {
74
- if (path.match(object[k].regex)) {
75
- key = k
76
- break
65
+ Model.prototype._validateFields = function (dataRoot, fields, data, opts, path) {
66
+ /**
67
+ * Recurse through and retrieve any errors and valid data
68
+ * @param {any} dataRoot
69
+ * @param {object|array} fields
70
+ * @param {any} data
71
+ * @param {object} opts
72
+ * @param {string} path
73
+ * @return [errors, valid-data]
74
+ * @this model
75
+ *
76
+ * Fields first recursion = { pets: [{ name: {}, color: {} }] }
77
+ * Fields second recursion = [0]: { name: {}, color: {} }
78
+ */
79
+ let errors = []
80
+ let data2 = util.isArray(fields)? [] : {}
81
+ let timestamps = util.isDefined(opts.timestamps) ? opts.timestamps : this.manager.opts.timestamps
82
+
83
+ util.forEach(util.forceArray(data), function(data, i) {
84
+ util.forEach(fields, function(field, fieldName) {
85
+ 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]
93
+
94
+ // Timestamp overrides
95
+ if (schema.timestampField) {
96
+ if (timestamps && ((fieldName == 'createdAt' && opts.insert) || fieldName == 'updatedAt')) {
97
+ value = schema.default.call(dataRoot, fieldName, this)
98
+ }
99
+ // Use the default if available
100
+ } else if (util.isDefined(schema.default)) {
101
+ if ((!util.isDefined(value) && opts.insert) || schema.defaultOverride) {
102
+ value = util.isFunction(schema.default)? schema.default.call(dataRoot, fieldName, this) : schema.default
103
+ }
77
104
  }
78
- }
79
- return key
80
- },
81
105
 
82
- _validateFields: function(dataRoot, fields, data, opts, path) {
83
- /**
84
- * Recurse through and retrieve any errors and valid data
85
- * @param {any} dataRoot
86
- * @param {object|array} fields
87
- * @param {any} data
88
- * @param {object} opts
89
- * @param {string} path
90
- * @return [errors, valid-data]
91
- * @this model
92
- *
93
- * Fields first recursion = { pets: [{ name: {}, color: {} }] }
94
- * Fields second recursion = [0]: { name: {}, color: {} }
95
- */
96
- let errors = []
97
- let data2 = util.isArray(fields)? [] : {}
98
- let timestamps = util.isDefined(opts.timestamps)? opts.timestamps : this.manager.timestamps
99
-
100
- util.forEach(util.forceArray(data), function(data, i) {
101
- util.forEach(fields, function(field, fieldName) {
102
- let verrors = []
103
- let schema = field.schema || field
104
- let value = util.isArray(fields)? data : (data||{})[fieldName]
105
- let indexOrFieldName = util.isArray(fields)? i : fieldName
106
- let path2 = `${path}.${indexOrFieldName}`.replace(/^\./, '')
107
- let path3 = path2.replace(/(^|\.)[0-9]+(\.|$)/, '$2') // no numerical keys, e.g. pets.1.name = pets.name
108
- let isType = 'is' + util.ucFirst(schema.type)
109
- let isTypeRule = this.rules[isType] || rules[isType]
106
+ // Ignore blacklisted
107
+ if (this._pathBlacklisted(path3, opts.projectionValidate) && !schema.defaultOverride) return
108
+ // Ignore insert only
109
+ if (opts.update && schema.insertOnly) return
110
+ // Ignore virtual fields
111
+ if (schema.virtual) return
112
+ // 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)
115
+ }
110
116
 
111
- // Timestamp overrides
112
- if (schema.timestampField) {
113
- if (timestamps && ((fieldName == 'createdAt' && opts.insert) || fieldName == 'updatedAt')) {
114
- value = schema.default.call(dataRoot, fieldName, this)
115
- }
116
- // Use the default if available
117
- } else if (util.isDefined(schema.default)) {
118
- if ((!util.isDefined(value) && opts.insert) || schema.defaultOverride) {
119
- value = util.isFunction(schema.default)? schema.default.call(dataRoot, fieldName, this) : schema.default
120
- }
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)) {
124
+ // Object schema errors
125
+ errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
126
+ // Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
127
+ if (
128
+ opts.insert ||
129
+ util.isObject(value) ||
130
+ (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path2||'').match(/\./))
131
+ ) {
132
+ var res = this._validateFields(dataRoot, field, value, opts, path2)
133
+ errors.push(...res[0])
121
134
  }
122
-
123
- // Ignore blacklisted
124
- if (this._pathBlacklisted(path3, opts.projectionValidate) && !schema.defaultOverride) return
125
- // Ignore insert only
126
- if (opts.update && schema.insertOnly) return
127
- // Ignore virtual fields
128
- if (schema.virtual) return
129
- // Type cast the value if tryParse is available, .e.g. isInteger.tryParse
130
- if (isTypeRule && util.isFunction(isTypeRule.tryParse)) {
131
- value = isTypeRule.tryParse.call(dataRoot, value, fieldName, this)
135
+ if (util.isDefined(value) && !verrors.length) {
136
+ data2[indexOrFieldName] = res? res[1] : value
132
137
  }
133
138
 
134
- // Schema field (ignore object/array schemas)
135
- if (util.isSchema(field) && fieldName !== 'schema') {
136
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
137
- if (util.isDefined(value) && !verrors.length) data2[indexOrFieldName] = value
138
-
139
- // Fields can be a subdocument
140
- } else if (util.isSubdocument(field)) {
141
- // Object schema errors
142
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
143
- // Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
144
- if (
145
- opts.insert ||
146
- util.isObject(value) ||
147
- (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path2||'').match(/\./))
148
- ) {
149
- var res = this._validateFields(dataRoot, field, value, opts, path2)
150
- errors.push(...res[0])
151
- }
152
- if (util.isDefined(value) && !verrors.length) {
153
- data2[indexOrFieldName] = res? res[1] : value
154
- }
155
-
156
- // Fields can be an array
157
- } else if (util.isArray(field)) {
158
- // Array schema errors
159
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
160
- // Data value is array too
161
- if (util.isArray(value)) {
162
- var res2 = this._validateFields(dataRoot, field, value, opts, path2)
163
- errors.push(...res2[0])
164
- }
165
- if (util.isDefined(value) && !verrors.length) {
166
- data2[indexOrFieldName] = res2? res2[1] : value
167
- }
139
+ // Fields can be an array
140
+ } else if (util.isArray(field)) {
141
+ // Array schema errors
142
+ errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
143
+ // Data value is array too
144
+ if (util.isArray(value)) {
145
+ var res2 = this._validateFields(dataRoot, field, value, opts, path2)
146
+ errors.push(...res2[0])
147
+ }
148
+ if (util.isDefined(value) && !verrors.length) {
149
+ data2[indexOrFieldName] = res2? res2[1] : value
168
150
  }
169
- }, this)
151
+ }
170
152
  }, this)
153
+ }, this)
171
154
 
172
- // Normalise array indexes and return
173
- if (util.isArray(fields)) data2 = data2.filter(() => true)
174
- if (data === null) data2 = null
175
- return [errors, data2]
176
- },
177
-
178
- _validateRules: function(dataRoot, field, value, opts, path) {
179
- /**
180
- * Validate all the field's rules
181
- * @param {object} dataRoot - data
182
- * @param {object} field - field schema
183
- * @param {string} path - full field path
184
- * @param {object} opts - original validate() options
185
- * @return {array} errors
186
- * @this model
187
- */
188
- let errors = []
189
- if (opts.skipValidation === true) return []
155
+ // Normalise array indexes and return
156
+ if (util.isArray(fields)) data2 = data2.filter(() => true)
157
+ if (data === null) data2 = null
158
+ return [errors, data2]
159
+ }
190
160
 
191
- // Skip validation for a field, takes in to account if a parent has been skipped.
192
- if (opts.skipValidation.length) {
193
- //console.log(path, field, opts)
194
- let pathChunks = path.split('.')
195
- for (let skippedField of opts.skipValidation) {
196
- // Make sure there is numerical character representing arrays
197
- let skippedFieldChunks = skippedField.split('.')
198
- for (let i=0, l=pathChunks.length; i<l; i++) {
199
- if (pathChunks[i].match(/^[0-9]+$/)
200
- && skippedFieldChunks[i]
201
- && !skippedFieldChunks[i].match(/^\$$|^[0-9]+$/)) {
202
- skippedFieldChunks.splice(i, 0, '$')
203
- }
161
+ Model.prototype._validateRules = function (dataRoot, field, value, opts, path) {
162
+ /**
163
+ * Validate all the field's rules
164
+ * @param {object} dataRoot - data
165
+ * @param {object} field - field schema
166
+ * @param {string} path - full field path
167
+ * @param {object} opts - original validate() options
168
+ * @return {array} errors
169
+ * @this model
170
+ */
171
+ let errors = []
172
+ if (opts.skipValidation === true) return []
173
+
174
+ // Skip validation for a field, takes in to account if a parent has been skipped.
175
+ if (opts.skipValidation.length) {
176
+ //console.log(path, field, opts)
177
+ let pathChunks = path.split('.')
178
+ for (let skippedField of opts.skipValidation) {
179
+ // Make sure there is numerical character representing arrays
180
+ let skippedFieldChunks = skippedField.split('.')
181
+ for (let i=0, l=pathChunks.length; i<l; i++) {
182
+ if (pathChunks[i].match(/^[0-9]+$/)
183
+ && skippedFieldChunks[i]
184
+ && !skippedFieldChunks[i].match(/^\$$|^[0-9]+$/)) {
185
+ skippedFieldChunks.splice(i, 0, '$')
204
186
  }
205
- for (let i=0, l=skippedFieldChunks.length; i<l; i++) {
206
- if (skippedFieldChunks[i] == '$') skippedFieldChunks[i] = '[0-9]+'
207
- }
208
- if (path.match(new RegExp('^' + skippedFieldChunks.join('.') + '(.|$)'))) return []
209
187
  }
188
+ for (let i=0, l=skippedFieldChunks.length; i<l; i++) {
189
+ if (skippedFieldChunks[i] == '$') skippedFieldChunks[i] = '[0-9]+'
190
+ }
191
+ if (path.match(new RegExp('^' + skippedFieldChunks.join('.') + '(.|$)'))) return []
210
192
  }
193
+ }
194
+
195
+ for (let ruleName in field) {
196
+ if (this._ignoredRules.indexOf(ruleName) > -1) continue
197
+ let error = this._validateRule(dataRoot, ruleName, field, field[ruleName], value, opts, path)
198
+ if (error && ruleName == 'required') return [error] // only show the required error
199
+ if (error) errors.push(error)
200
+ }
201
+ return errors
202
+ }
211
203
 
212
- for (let ruleName in field) {
213
- if (this._ignoredRules.indexOf(ruleName) > -1) continue
214
- let error = this._validateRule(dataRoot, ruleName, field, field[ruleName], value, opts, path)
215
- if (error && ruleName == 'required') return [error] // only show the required error
216
- if (error) errors.push(error)
217
- }
218
- return errors
219
- },
220
-
221
- _validateRule: function(dataRoot, ruleName, field, ruleArg, value, opts, path) {
222
- // this.debug(path, field, ruleName, ruleArg, value)
223
- // Remove [] from the message path, and simply ignore non-numeric children to test for all array items
224
- ruleArg = ruleArg === true? undefined : ruleArg
225
- let rule = this.rules[ruleName] || rules[ruleName]
226
- let fieldName = path.match(/[^.]+$/)[0]
227
- let isDeepProp = path.match(/\./) // todo: not dot-notation
228
- let ruleMessageKey = this.messagesLen && this._getMostSpecificKeyMatchingPath(this.messages, path)
229
- let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
230
- let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : opts.insert || isDeepProp
231
- if (!ruleMessage) ruleMessage = rule.message
232
-
233
- // Undefined value
234
- if (typeof value === 'undefined' && (!validateUndefined || !rule.validateUndefined)) return
235
-
236
- // Ignore null (if nullObject is set on objects or arrays)
237
- if (value === null && (field.isObject || field.isArray) && field.nullObject && !rule.validateNull) return
238
-
239
- // Ignore null
240
- if (value === null && !(field.isObject || field.isArray) && !rule.validateNull) return
241
-
242
- // Ignore empty strings
243
- if (value === '' && !rule.validateEmptyString) return
244
-
245
- // Rule failed
246
- if (!rule.fn.call(dataRoot, value, ruleArg, path, this)) return {
247
- detail: util.isFunction(ruleMessage)
248
- ? ruleMessage.call(dataRoot, value, ruleArg, path, this)
249
- : ruleMessage,
250
- meta: { rule: ruleName, model: this.name, field: fieldName, detailLong: rule.messageLong },
251
- status: '400',
252
- title: path
253
- }
254
- },
255
-
256
- _ignoredRules: [ // todo: change name? i.e. 'specialFields'
257
- // Need to remove filesize and formats..
258
- 'awsAcl', 'awsBucket', 'default', 'defaultOverride', 'filename', 'filesize', 'fileSize', 'formats',
259
- 'image', 'index', 'insertOnly', 'model', 'nullObject', 'params', 'path', 'getSignedUrl', 'timestampField',
260
- 'type', 'virtual',
261
- ]
262
-
204
+ Model.prototype._validateRule = function (dataRoot, ruleName, field, ruleArg, value, opts, path) {
205
+ // this.debug(path, field, ruleName, ruleArg, value)
206
+ // Remove [] from the message path, and simply ignore non-numeric children to test for all array items
207
+ ruleArg = ruleArg === true? undefined : ruleArg
208
+ 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
215
+
216
+ // Undefined value
217
+ if (typeof value === 'undefined' && (!validateUndefined || !rule.validateUndefined)) return
218
+
219
+ // Ignore null (if nullObject is set on objects or arrays)
220
+ if (value === null && (field.isObject || field.isArray) && field.nullObject && !rule.validateNull) return
221
+
222
+ // Ignore null
223
+ if (value === null && !(field.isObject || field.isArray) && !rule.validateNull) return
224
+
225
+ // Ignore empty strings
226
+ if (value === '' && !rule.validateEmptyString) return
227
+
228
+ // 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,
236
+ }
263
237
  }
238
+
239
+ Model.prototype._ignoredRules = [
240
+ // todo: change name? i.e. 'specialFields'
241
+ // todo: need to remove filesize and formats..
242
+ 'awsAcl', 'awsBucket', 'default', 'defaultOverride', 'filename', 'filesize', 'fileSize', 'formats',
243
+ 'image', 'index', 'insertOnly', 'model', 'nullObject', 'params', 'path', 'getSignedUrl', 'timestampField',
244
+ 'type', 'virtual',
245
+ ]