monastery 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +1 -1
- package/changelog.md +2 -0
- package/docs/readme.md +4 -2
- package/lib/index.js +14 -2
- package/lib/model-crud.js +125 -88
- package/lib/model-validate.js +131 -97
- package/lib/model.js +15 -0
- package/lib/util.js +101 -72
- package/package.json +1 -1
- package/test/crud.js +128 -1
- package/test/util.js +237 -22
- package/test/validate.js +87 -3
package/lib/model-validate.js
CHANGED
|
@@ -9,8 +9,8 @@ Model.prototype.validate = async function (data, opts) {
|
|
|
9
9
|
* @param {object} <opts>
|
|
10
10
|
* @param {array|string|false} <opts.blacklist> - augment insertBL/updateBL, `false` will remove blacklisting
|
|
11
11
|
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
12
|
-
* @param {
|
|
13
|
-
*
|
|
12
|
+
* @param {boolran} <opts.skipHooks> - skip hooks
|
|
13
|
+
* @param {array|string} <opts.skipValidation> - skip validation for these fields
|
|
14
14
|
* @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
|
|
15
15
|
* updated, depending on the `options.update` value
|
|
16
16
|
* @param {boolean(false)} <opts.update> - are we validating for insert or update? todo: change to `type`
|
|
@@ -25,8 +25,8 @@ Model.prototype.validate = async function (data, opts) {
|
|
|
25
25
|
opts = opts || {}
|
|
26
26
|
opts.update = opts.update || opts.findOneAndUpdate
|
|
27
27
|
opts.insert = !opts.update
|
|
28
|
-
opts.skipValidation = opts.skipValidation === true ? true : util.toArray(opts.skipValidation||[])
|
|
29
|
-
|
|
28
|
+
opts.skipValidation = opts.skipValidation === true ? true : util.toArray(opts.skipValidation || [])
|
|
29
|
+
opts.timestamps = util.isDefined(opts.timestamps) ? opts.timestamps : this.manager.opts.timestamps
|
|
30
30
|
|
|
31
31
|
// Get projection
|
|
32
32
|
if (opts.project) var projectionValidate = this._getProjectionFromProject(opts.project)
|
|
@@ -35,14 +35,14 @@ Model.prototype.validate = async function (data, opts) {
|
|
|
35
35
|
opts.projectionInclusion = (projectionValidate || {})[opts.projectionKeys[0]] ? true : false // default false
|
|
36
36
|
|
|
37
37
|
// Hook: beforeValidate
|
|
38
|
-
data = await
|
|
38
|
+
data = await this._callHooks('beforeValidate', data, opts)
|
|
39
39
|
|
|
40
40
|
// Recurse and validate fields
|
|
41
41
|
// console.time('_validateFields')
|
|
42
42
|
let response = util.toArray(data).map(item => {
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
else return validated
|
|
43
|
+
const [errors, validated] = this._validateFields(item, this.fields, item, opts, '', '')
|
|
44
|
+
if (errors.length) throw errors // todo: maybe add trace to this object?
|
|
45
|
+
else return validated
|
|
46
46
|
})
|
|
47
47
|
// console.timeEnd('_validateFields')
|
|
48
48
|
|
|
@@ -76,11 +76,11 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
|
|
|
76
76
|
* Note: This is now super fast, it can validate 100k possible fields in 235ms
|
|
77
77
|
*
|
|
78
78
|
* @param {any} dataRoot
|
|
79
|
-
* @param {object|array} fields
|
|
79
|
+
* @param {object|array} fields (from definition)
|
|
80
80
|
* @param {any} data
|
|
81
81
|
* @param {object} opts
|
|
82
|
-
* @param {string} parentPath - data
|
|
83
|
-
* @param {string} parentPath2 - no numerical keys, e.g. pets.name
|
|
82
|
+
* @param {string} parentPath - parent data path, e.g. pets.1.name
|
|
83
|
+
* @param {string} parentPath2 - parent field path, no numerical keys, e.g. pets.name
|
|
84
84
|
* @return [errors, valid-data]
|
|
85
85
|
* @this model
|
|
86
86
|
*
|
|
@@ -90,112 +90,146 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
|
|
|
90
90
|
let errors = []
|
|
91
91
|
let fieldsIsArray = util.isArray(fields)
|
|
92
92
|
let fieldsArray = fieldsIsArray ? fields : Object.keys(fields)
|
|
93
|
-
let timestamps = util.isDefined(opts.timestamps) ? opts.timestamps : this.manager.opts.timestamps
|
|
94
93
|
let dataArray = util.forceArray(data)
|
|
95
94
|
let data2 = fieldsIsArray ? [] : {}
|
|
96
95
|
let notStrict = fields.schema.strict === false
|
|
97
96
|
|
|
98
97
|
for (let i=0, l=dataArray.length; i<l; i++) {
|
|
99
|
-
const
|
|
98
|
+
const dataItem = dataArray[i]
|
|
99
|
+
const dataKeys = Object.keys(dataItem || {}) // may be false when inserting, e.g. mode.insert({ data: false })
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (fieldName == 'schema') continue
|
|
106
|
-
// if (!parentPath && fieldName == 'categories') console.time(fieldName)
|
|
107
|
-
// if (!parentPath && fieldName == 'categories') console.time(fieldName + 1)
|
|
108
|
-
let schema = field.schema
|
|
109
|
-
let value = fieldsIsArray ? item : (item||{})[fieldName]
|
|
110
|
-
let indexOrFieldName = fieldsIsArray ? i : fieldName
|
|
111
|
-
let path = `${parentPath}.${indexOrFieldName}`
|
|
112
|
-
let path2 = fieldsIsArray ? parentPath2 : `${parentPath2}.${fieldName}`
|
|
113
|
-
if (path[0] == '.') path = path.slice(1) // remove leading dot, e.g. .pets.1.name
|
|
114
|
-
if (path2[0] == '.') path2 = path2.slice(1) // remove leading dot, e.g. .pets.1.name
|
|
115
|
-
let isTypeRule = this.rules[schema.isType] || rules[schema.isType]
|
|
116
|
-
|
|
117
|
-
// Timestamp overrides
|
|
118
|
-
if (schema.timestampField) {
|
|
119
|
-
if (timestamps && ((fieldName == 'createdAt' && opts.insert) || fieldName == 'updatedAt')) {
|
|
120
|
-
value = schema.default.call(dataRoot, fieldName, this)
|
|
121
|
-
}
|
|
122
|
-
// Use the default if available
|
|
123
|
-
} else if (util.isDefined(schema.default)) {
|
|
124
|
-
if ((!util.isDefined(value) && opts.insert) || schema.defaultOverride) {
|
|
125
|
-
value = util.isFunction(schema.default)? schema.default.call(dataRoot, fieldName, this) : schema.default
|
|
126
|
-
}
|
|
101
|
+
// Add any non-schema properties, excluding array properties
|
|
102
|
+
if (notStrict && !fieldsIsArray) {
|
|
103
|
+
for (let m=0, n=dataKeys.length; m<n; m++) {
|
|
104
|
+
if (!fieldsArray.includes(dataKeys[m])) data2[dataKeys[m]] = dataItem[dataKeys[m]]
|
|
127
105
|
}
|
|
106
|
+
}
|
|
128
107
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// Type cast the value if tryParse is available, .e.g. isInteger.tryParse
|
|
136
|
-
if (isTypeRule && typeof isTypeRule.tryParse == 'function') {
|
|
137
|
-
value = isTypeRule.tryParse.call(dataRoot, value, fieldName, this) // 80ms // DISABLE
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Field is a subdocument
|
|
141
|
-
if (schema.isObject) {
|
|
142
|
-
// Object schema errors
|
|
143
|
-
let res
|
|
144
|
-
const verrors = this._validateRules(dataRoot, schema, value, opts, path)
|
|
145
|
-
if (verrors.length) errors.push(...verrors)
|
|
146
|
-
// Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
|
|
147
|
-
if (
|
|
148
|
-
opts.insert ||
|
|
149
|
-
util.isObject(value) ||
|
|
150
|
-
(util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path||'').indexOf('.') !== -1)
|
|
151
|
-
) {
|
|
152
|
-
res = this._validateFields(dataRoot, field, value, opts, path, path2)
|
|
153
|
-
if (res[0].length) errors.push(...res[0])
|
|
154
|
-
}
|
|
155
|
-
if (util.isDefined(value) && !verrors.length) {
|
|
156
|
-
data2[indexOrFieldName] = res ? res[1] : value
|
|
157
|
-
}
|
|
108
|
+
// Loop through each schema field
|
|
109
|
+
for (let m=0, n=fieldsArray.length; m<n; m++) {
|
|
110
|
+
const fieldName = fieldsIsArray ? m : fieldsArray[m] // array|object key
|
|
111
|
+
const dataFieldName = fieldsIsArray ? i : fieldName
|
|
112
|
+
const value = fieldsIsArray ? dataItem : (dataItem||{})[fieldName]
|
|
113
|
+
const field = fields[fieldName] // schema field
|
|
158
114
|
|
|
159
|
-
// Field
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
let res2
|
|
163
|
-
const verrors = this._validateRules(dataRoot, schema, value, opts, path)
|
|
164
|
-
if (verrors.length) errors.push(...verrors)
|
|
165
|
-
// Data value is array too
|
|
166
|
-
if (util.isArray(value)) {
|
|
167
|
-
res2 = this._validateFields(dataRoot, field, value, opts, path, path2)
|
|
168
|
-
if (res2[0].length) errors.push(...res2[0])
|
|
169
|
-
}
|
|
170
|
-
if (util.isDefined(value) && !verrors.length) {
|
|
171
|
-
data2[indexOrFieldName] = res2? res2[1] : value
|
|
172
|
-
}
|
|
115
|
+
// Field paths
|
|
116
|
+
const path = `${parentPath ? parentPath + '.' : ''}${dataFieldName}` // e.g. pets.1.name
|
|
117
|
+
const path2 = fieldsIsArray ? parentPath2 : (`${parentPath2 ? parentPath2 + '.' : ''}${fieldName}`) // e.g. pets.name
|
|
173
118
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (verrors.length) errors.push(...verrors)
|
|
178
|
-
if (util.isDefined(value) && !verrors.length) data2[indexOrFieldName] = value
|
|
179
|
-
}
|
|
180
|
-
// if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
|
|
119
|
+
const [errors2, value2] = this._validateField(dataRoot, field, fieldName, value, opts, path, path2)
|
|
120
|
+
if (errors2.length) errors = errors.concat(errors2)
|
|
121
|
+
else if (typeof value2 !== 'undefined') data2[dataFieldName] = value2
|
|
181
122
|
}
|
|
182
123
|
|
|
183
|
-
//
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
124
|
+
// Validate dot-notation fields on the dataRoot, e.g. pets.1.name
|
|
125
|
+
if (!parentPath) {
|
|
126
|
+
for (let j=0, k=dataKeys.length; j<k; j++) {
|
|
127
|
+
if (dataKeys[j].includes('.')) {
|
|
128
|
+
const path = dataKeys[j]
|
|
129
|
+
const path2 = path.replace(/\.(\d|\$)+/g, '')
|
|
130
|
+
const pathWithZeroIndexes = path.replace(/\.(\d|\$)+/g, '.0')
|
|
131
|
+
const fieldName = pathWithZeroIndexes.split('.')[0]
|
|
132
|
+
const field = util.deepFind(fields, pathWithZeroIndexes)
|
|
133
|
+
if (!field) continue
|
|
134
|
+
|
|
135
|
+
const [errors2, value2] = this._validateField(dataRoot, field, fieldName, dataItem[path], opts, path, path2)
|
|
136
|
+
if (errors2.length) errors = errors.concat(errors2)
|
|
137
|
+
else if (typeof value2 !== 'undefined') data2[path] = value2
|
|
138
|
+
}
|
|
189
139
|
}
|
|
190
140
|
}
|
|
191
141
|
}
|
|
192
142
|
|
|
193
143
|
// Normalise array indexes and return
|
|
194
|
-
if (fieldsIsArray) data2 = data2.filter(() => true) //todo: remove???
|
|
195
144
|
if (data === null) data2 = null
|
|
196
145
|
return [errors, data2]
|
|
197
146
|
}
|
|
198
147
|
|
|
148
|
+
Model.prototype._validateField = function (dataRoot, field, fieldName, value, opts, path, path2) {
|
|
149
|
+
/**
|
|
150
|
+
* Validate a field
|
|
151
|
+
*
|
|
152
|
+
* @param {object} dataRoot - data
|
|
153
|
+
* @param {object} field - field (from definition)
|
|
154
|
+
* @param {string} fieldName
|
|
155
|
+
* @param {any} value
|
|
156
|
+
* @param {object} opts - original validate() options
|
|
157
|
+
* @param {string} path - full data path, e.g. pets.1.name
|
|
158
|
+
* @param {string} path2 - full field path, without numerical keys, e.g. pets.name
|
|
159
|
+
* @return [errors[], valid-value]
|
|
160
|
+
* @this model
|
|
161
|
+
*/
|
|
162
|
+
// iterations++
|
|
163
|
+
const schema = field.schema
|
|
164
|
+
if (fieldName == 'schema') return [[]]
|
|
165
|
+
// if (!parentPath && fieldName == 'categories') console.time(fieldName)
|
|
166
|
+
// if (!parentPath && fieldName == 'categories') console.time(fieldName + 1)
|
|
167
|
+
|
|
168
|
+
const isTypeRule = this.rules[schema.isType] || rules[schema.isType]
|
|
169
|
+
|
|
170
|
+
// Timestamp overrides
|
|
171
|
+
if (schema.timestampField) {
|
|
172
|
+
if (opts.timestamps && ((fieldName == 'createdAt' && opts.insert) || fieldName == 'updatedAt')) {
|
|
173
|
+
value = schema.default.call(dataRoot, fieldName, this)
|
|
174
|
+
}
|
|
175
|
+
// Use the default if available
|
|
176
|
+
} else if (util.isDefined(schema.default)) {
|
|
177
|
+
if ((!util.isDefined(value) && opts.insert) || schema.defaultOverride) {
|
|
178
|
+
value = util.isFunction(schema.default)? schema.default.call(dataRoot, fieldName, this) : schema.default
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Ignore insert only
|
|
183
|
+
if (opts.update && schema.insertOnly) return [[]]
|
|
184
|
+
// Ignore virtual fields
|
|
185
|
+
if (schema.virtual) return [[]]
|
|
186
|
+
// Ignore blacklisted
|
|
187
|
+
if (!schema.defaultOverride && this._pathBlacklisted(path2, opts.projectionInclusion, opts.projectionKeys)) return [[]]
|
|
188
|
+
// Type cast the value if tryParse is available, .e.g. isInteger.tryParse
|
|
189
|
+
if (isTypeRule && typeof isTypeRule.tryParse == 'function') {
|
|
190
|
+
value = isTypeRule.tryParse.call(dataRoot, value, fieldName, this) // 80ms // DISABLE
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Field is a subdocument
|
|
194
|
+
if (schema.isObject) {
|
|
195
|
+
// Object schema errors
|
|
196
|
+
let verrors2, value2
|
|
197
|
+
let verrors = this._validateRules(dataRoot, schema, value, opts, path)
|
|
198
|
+
// Recurse if inserting, value is a subdocument, or we're within a subdocument (todo: not dot-notation)
|
|
199
|
+
const parentIsSubdocument = (path||'').indexOf('.') !== -1
|
|
200
|
+
if (
|
|
201
|
+
opts.insert ||
|
|
202
|
+
util.isObject(value) ||
|
|
203
|
+
(util.isDefined(opts.validateUndefined) ? opts.validateUndefined : parentIsSubdocument)
|
|
204
|
+
) {
|
|
205
|
+
[verrors2, value2] = this._validateFields(dataRoot, field, value, opts, path, path2)
|
|
206
|
+
if (verrors2.length) verrors = verrors.concat(verrors2)
|
|
207
|
+
}
|
|
208
|
+
if (verrors.length) return [verrors]
|
|
209
|
+
else return [[], typeof value2 !== 'undefined' && typeof value !== 'undefined' ? value2 : value]
|
|
210
|
+
|
|
211
|
+
// Field is an array
|
|
212
|
+
} else if (schema.isArray) {
|
|
213
|
+
// Array schema errors
|
|
214
|
+
let verrors2, value2
|
|
215
|
+
let verrors = this._validateRules(dataRoot, schema, value, opts, path)
|
|
216
|
+
// Data value is array too
|
|
217
|
+
if (util.isArray(value)) {
|
|
218
|
+
[verrors2, value2] = this._validateFields(dataRoot, field, value, opts, path, path2)
|
|
219
|
+
if (verrors2.length) verrors = verrors.concat(verrors2)
|
|
220
|
+
}
|
|
221
|
+
if (verrors.length) return [verrors]
|
|
222
|
+
else return [[], typeof value2 !== 'undefined' && typeof value !== 'undefined' ? value2 : value]
|
|
223
|
+
|
|
224
|
+
// Field is a field-type/field-schema
|
|
225
|
+
} else {
|
|
226
|
+
const verrors = this._validateRules(dataRoot, schema, value, opts, path)
|
|
227
|
+
if (verrors.length) return [verrors]
|
|
228
|
+
else return [[], value]
|
|
229
|
+
}
|
|
230
|
+
// if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
|
|
231
|
+
}
|
|
232
|
+
|
|
199
233
|
Model.prototype._validateRules = function (dataRoot, fieldSchema, value, opts, path) {
|
|
200
234
|
/**
|
|
201
235
|
* Validate all the field's rules
|
|
@@ -203,7 +237,7 @@ Model.prototype._validateRules = function (dataRoot, fieldSchema, value, opts, p
|
|
|
203
237
|
* @param {object} fieldSchema - field schema
|
|
204
238
|
* @param {any} value - data value
|
|
205
239
|
* @param {object} opts - original validate() options
|
|
206
|
-
* @param {string} path - full
|
|
240
|
+
* @param {string} path - full data path, e.g. pets.1.name
|
|
207
241
|
* @return {array} errors
|
|
208
242
|
* @this model
|
|
209
243
|
*/
|
package/lib/model.js
CHANGED
|
@@ -405,6 +405,21 @@ Model.prototype._setupIndexes = async function(fields, opts={}) {
|
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
Model.prototype._callHooks = async function(hookName, data, opts) {
|
|
409
|
+
/**
|
|
410
|
+
* Calls hooks in series
|
|
411
|
+
*
|
|
412
|
+
* @param {string} hookName - e.g. 'beforeValidate'
|
|
413
|
+
* @param {object} opts - operation options, e.g. { data, skipValidation, ... }
|
|
414
|
+
* @param {any} arg - data to pass to the first function
|
|
415
|
+
*
|
|
416
|
+
* @return {any} - the result of the last function
|
|
417
|
+
* @this model
|
|
418
|
+
*/
|
|
419
|
+
if (opts.skipHooks) return data
|
|
420
|
+
return await util.runSeries.call(this, this[hookName].map(f => f.bind(opts)), hookName, data)
|
|
421
|
+
}
|
|
422
|
+
|
|
408
423
|
Model.prototype._defaultFields = {
|
|
409
424
|
_id: {
|
|
410
425
|
insertOnly: true,
|
package/lib/util.js
CHANGED
|
@@ -40,6 +40,20 @@ module.exports = {
|
|
|
40
40
|
return obj2
|
|
41
41
|
},
|
|
42
42
|
|
|
43
|
+
deepFind: (obj, path) => {
|
|
44
|
+
// Returns a nested value from a path URI e.g. user.books.1.title
|
|
45
|
+
if (!obj) return undefined
|
|
46
|
+
let last
|
|
47
|
+
let chunks = (path || '').split('.')
|
|
48
|
+
let target = obj
|
|
49
|
+
for (let i = 0, l = chunks.length; i < l; i++) {
|
|
50
|
+
last = l === i + 1
|
|
51
|
+
if (!last && !target[chunks[i]]) break
|
|
52
|
+
else target = target[chunks[i]]
|
|
53
|
+
}
|
|
54
|
+
return last ? target : undefined
|
|
55
|
+
},
|
|
56
|
+
|
|
43
57
|
forEach: function(obj, iteratee, context) {
|
|
44
58
|
if (this.isArrayLike(obj)) {
|
|
45
59
|
for (let i=0, l=obj.length; i<l; i++) {
|
|
@@ -205,97 +219,111 @@ module.exports = {
|
|
|
205
219
|
return shallowCopy
|
|
206
220
|
},
|
|
207
221
|
|
|
208
|
-
parseData: function(obj) {
|
|
222
|
+
parseData: function(obj, parseBracketToDotNotation, parseDotNotation) {
|
|
209
223
|
/**
|
|
210
|
-
*
|
|
211
|
-
* @param {object}
|
|
212
|
-
* @
|
|
224
|
+
* Coverts dot notation objects, and then bracket notation objects (form data) into deep objects
|
|
225
|
+
* @param {object}
|
|
226
|
+
* @param {boolean} parseBracketToDotNotation - converts bracket notation to dot notation, instead of deep objects
|
|
227
|
+
* @param {boolean} parseDotNotation - converts dot notation to deep objects
|
|
228
|
+
* @return data
|
|
213
229
|
*/
|
|
214
|
-
|
|
215
|
-
|
|
230
|
+
obj = parseBracketToDotNotation ? this.parseBracketToDotNotation(obj) : this.parseBracketNotation(obj)
|
|
231
|
+
if (parseDotNotation) obj = this.parseDotNotation(obj)
|
|
232
|
+
return obj
|
|
216
233
|
},
|
|
217
234
|
|
|
218
235
|
parseDotNotation: function(obj) {
|
|
219
236
|
/**
|
|
220
|
-
*
|
|
221
|
-
* @param {object}
|
|
237
|
+
* Converts dot notation field paths into deep objects
|
|
238
|
+
* @param {object} obj - e.g. { 'deep.companyLogos2.1.logo': '' } (not mutated)
|
|
239
|
+
* @return {object} - e.g. { deep: { companyLogos2: [{ logo: '' }] }}
|
|
222
240
|
*/
|
|
223
|
-
if (!Object.keys(obj).
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
241
|
+
if (!Object.keys(obj).some(key => key.includes('.'))) return obj
|
|
242
|
+
|
|
243
|
+
const result = {}
|
|
244
|
+
|
|
245
|
+
for (const key in obj) {
|
|
246
|
+
if (key.includes('.')) {
|
|
247
|
+
setValue(result, key.split('.'), obj[key])
|
|
229
248
|
} else {
|
|
230
|
-
|
|
231
|
-
// reassigning, trying to preserve the order of keys (not always guaranteed in for loops)
|
|
232
|
-
obj[key] = objCopy[key]
|
|
249
|
+
result[key] = obj[key] // keep non-dot notation values
|
|
233
250
|
}
|
|
234
251
|
}
|
|
235
|
-
|
|
236
|
-
function
|
|
237
|
-
let
|
|
238
|
-
let
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
let key = keys[i]
|
|
243
|
-
// If denoting an array, make sure parent is an array
|
|
244
|
-
if (key.match(/^[0-9]+$/) && !Array.isArray(parentObj)) {
|
|
245
|
-
parentObj = grandparentObj[keys[i-1]] = []
|
|
252
|
+
|
|
253
|
+
function setValue(target, keys, value) {
|
|
254
|
+
let current = target
|
|
255
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
256
|
+
const key = keys[i]
|
|
257
|
+
if (!current[key]) {
|
|
258
|
+
current[key] = /^\d+$/.test(keys[i + 1]) ? [] : {}
|
|
246
259
|
}
|
|
247
|
-
|
|
248
|
-
parentObj = parentObj[key] = parentObj[key] || {}
|
|
260
|
+
current = current[key]
|
|
249
261
|
}
|
|
250
|
-
|
|
251
|
-
parentObj[keys[i]] = val
|
|
252
|
-
delete obj[str]
|
|
262
|
+
current[keys[keys.length - 1]] = value
|
|
253
263
|
}
|
|
264
|
+
|
|
265
|
+
return result
|
|
254
266
|
},
|
|
255
267
|
|
|
256
|
-
|
|
268
|
+
parseBracketNotation: function(obj) {
|
|
257
269
|
/**
|
|
258
|
-
*
|
|
259
|
-
* @param {object}
|
|
260
|
-
* @return
|
|
261
|
-
* E.g. ['user']['name']
|
|
262
|
-
* E.g. ['user']['petnames'][0]
|
|
263
|
-
* E.g. ['users'][0]['name']
|
|
270
|
+
* Converts bracket notation field paths (form data) into deep objects
|
|
271
|
+
* @param {object} obj - e.g. { 'users[0][first]': 'Martin' } (not mutated)
|
|
272
|
+
* @return {object} - e.g. { users: [{ first: 'Martin' }] }
|
|
264
273
|
*/
|
|
265
|
-
if (!Object.keys(obj).
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
274
|
+
if (!Object.keys(obj).some(key => key.includes('['))) return obj
|
|
275
|
+
|
|
276
|
+
const result = {}
|
|
277
|
+
|
|
278
|
+
for (const key in obj) {
|
|
279
|
+
if (key.includes('[][')) {
|
|
270
280
|
throw new Error(`Monastery: Array items in bracket notation need array indexes "${key}", e.g. users[0][name]`)
|
|
271
281
|
}
|
|
272
|
-
if (key.
|
|
273
|
-
|
|
282
|
+
if (key.includes('[')) {
|
|
283
|
+
// console.log(key, key.split(/[[\]]/).filter(Boolean), key.split(/[[\]]/))
|
|
284
|
+
setValue(result, key.split(/[[\]]/).filter(Boolean), obj[key])
|
|
274
285
|
} else {
|
|
275
|
-
|
|
276
|
-
// reassigning, trying to preserve the order of keys (not always guaranteed in for loops)
|
|
277
|
-
obj[key] = objCopy[key]
|
|
286
|
+
result[key] = obj[key] // keep non-dot notation values
|
|
278
287
|
}
|
|
279
288
|
}
|
|
280
|
-
|
|
281
|
-
function
|
|
282
|
-
let
|
|
283
|
-
let
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (chunks[i].match(/^$|^[0-9]+$/) && !Array.isArray(parent)) {
|
|
288
|
-
parent = grandparent[chunks[i-1]] = []
|
|
289
|
-
}
|
|
290
|
-
if (i !== l-1) {
|
|
291
|
-
grandparent = parent
|
|
292
|
-
parent = parent[chunks[i]] = parent[chunks[i]] || {}
|
|
293
|
-
} else {
|
|
294
|
-
parent[chunks[i]] = obj[path]
|
|
289
|
+
|
|
290
|
+
function setValue(target, keys, value) {
|
|
291
|
+
let current = target
|
|
292
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
293
|
+
const key = keys[i]
|
|
294
|
+
if (!current[key]) {
|
|
295
|
+
current[key] = /^\d+$/.test(keys[i + 1]) ? [] : {}
|
|
295
296
|
}
|
|
297
|
+
current = current[key]
|
|
298
|
+
}
|
|
299
|
+
current[keys[keys.length - 1]] = value
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return result
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
parseBracketToDotNotation: function(obj) {
|
|
306
|
+
/**
|
|
307
|
+
* Converts bracket notation field paths (form data) into dot notation paths
|
|
308
|
+
* @param {object} obj - e.g. { 'users[0][first]': 'Martin' } (not mutated)
|
|
309
|
+
* @return {object} - e.g. { 'users.0.first': 'Martin' }
|
|
310
|
+
*/
|
|
311
|
+
const result = {}
|
|
312
|
+
|
|
313
|
+
for (const key in obj) {
|
|
314
|
+
if (key.includes('[][')) {
|
|
315
|
+
throw new Error(`Monastery: Array items in bracket notation need array indexes "${key}", e.g. users[0][name]`)
|
|
316
|
+
}
|
|
317
|
+
if (key.includes('[')) {
|
|
318
|
+
const keys = key.split(/[[\]]/).filter(Boolean)
|
|
319
|
+
const newKey = keys.shift() + keys.map(k => `.${k}`).join('')
|
|
320
|
+
result[newKey] = obj[key]
|
|
321
|
+
} else {
|
|
322
|
+
result[key] = obj[key] // keep non-bracket notation values
|
|
296
323
|
}
|
|
297
|
-
delete obj[path]
|
|
298
324
|
}
|
|
325
|
+
|
|
326
|
+
return result
|
|
299
327
|
},
|
|
300
328
|
|
|
301
329
|
pick: function(obj, keys) {
|
|
@@ -330,16 +358,17 @@ module.exports = {
|
|
|
330
358
|
},
|
|
331
359
|
|
|
332
360
|
runSeries: function(tasks, hookName, data) {
|
|
333
|
-
|
|
361
|
+
/**
|
|
334
362
|
* Runs functions in series
|
|
335
|
-
*
|
|
336
|
-
* @param {
|
|
337
|
-
* @param {
|
|
338
|
-
* @param {
|
|
363
|
+
*
|
|
364
|
+
* @param {function(err, result)[]} tasks - array of functions
|
|
365
|
+
* @param {string} hookName - e.g. 'afterFind'
|
|
366
|
+
* @param {any} data - data to pass to the first function
|
|
367
|
+
*
|
|
339
368
|
* @return promise
|
|
340
369
|
* @this Model
|
|
341
370
|
* @source https://github.com/feross/run-series
|
|
342
|
-
|
|
371
|
+
**/
|
|
343
372
|
let current = 0
|
|
344
373
|
let isSync = true
|
|
345
374
|
let caller = (this.afterFindName || this.name) + '.' + hookName
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "monastery",
|
|
3
3
|
"description": "⛪ A simple, straightforward MongoDB ODM",
|
|
4
4
|
"author": "Ricky Boyce",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.4.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:boycce/monastery",
|
|
8
8
|
"homepage": "https://boycce.github.io/monastery/",
|