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.
@@ -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 {array|string|true} <opts.skipValidation> - skip validation for these fields, or pass `true` to skip
13
- * all fields and hooks
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
- if (opts.skipValidation === true) return data
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 util.runSeries.call(this, this.beforeValidate.map(f => f.bind(opts)), 'beforeValidate', data)
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
- let validated = this._validateFields(item, this.fields, item, opts, '', '')
44
- if (validated[0].length) throw validated[0]
45
- else return validated[1]
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 localised parent, e.g. pets.1.name
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 item = dataArray[i]
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
- for (let m=0, n=fieldsArray.length; m<n; m++) {
102
- // iterations++
103
- const fieldName = fieldsIsArray ? m : fieldsArray[m] // array|object key
104
- const field = fields[fieldName]
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
- // Ignore insert only
130
- if (opts.update && schema.insertOnly) continue
131
- // Ignore virtual fields
132
- if (schema.virtual) continue
133
- // Ignore blacklisted
134
- if (!schema.defaultOverride && this._pathBlacklisted(path2, opts.projectionInclusion, opts.projectionKeys)) continue
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 is an array
160
- } else if (schema.isArray) {
161
- // Array schema errors
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
- // Field is a field-type/field-schema
175
- } else {
176
- const verrors = this._validateRules(dataRoot, schema, value, opts, path)
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
- // Add any extra fields that are not in the schema. Item maybe false when inserting (from recursing above)
184
- if (notStrict && !fieldsIsArray && item) {
185
- const allDataKeys = Object.keys(item)
186
- for (let m=0, n=allDataKeys.length; m<n; m++) {
187
- const key = allDataKeys[m]
188
- if (!fieldsArray.includes(key)) data2[key] = item[key]
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 field path, e.g. pets.1.name
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
- * Mutates dot notation objects, and then FormData (bracket notation) objects into a deep object
211
- * @param {object}
212
- * @return promise(data)
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
- let data = this.parseDotNotation(obj)
215
- return this.parseFormData(data)
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
- * Mutates dot notation objects into a deep object
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).find(o => o.indexOf('.') !== -1)) return obj
224
- let objCopy = this.deepCopy(obj) // maybe convert to JSON.parse(JSON.stringify(obj))
225
-
226
- for (let key in obj) {
227
- if (key.indexOf('.') !== -1) {
228
- setup(key, obj[key], obj)
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
- // Ordinary values may of been updated by the bracket notation values, we are
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
- return obj
236
- function setup(str, val, obj) {
237
- let parentObj = obj
238
- let grandparentObj = obj
239
- let keys = str.split(/\./)
240
-
241
- for (var i=0, l=Math.max(1, keys.length-1); i<l; ++i) {
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
- grandparentObj = parentObj
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
- parseFormData: function(obj) {
268
+ parseBracketNotation: function(obj) {
257
269
  /**
258
- * Mutates FormData (bracket notation) objects into a deep object
259
- * @param {object}
260
- * @return promise(data)
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).find(o => o.indexOf('[') !== -1)) return obj
266
- let objCopy = this.deepCopy(obj) // maybe convert to JSON.parse(JSON.stringify(obj))
267
-
268
- for (let key in obj) {
269
- if (key.match(/\[\]\[/i)) {
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.indexOf('[') !== -1) {
273
- setup(key)
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
- // Ordinary values may of been updated by the bracket notation values, we are
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
- return obj
281
- function setup(path) {
282
- let parent = obj
283
- let grandparent = obj
284
- let chunks = path.replace(/]/g, '').split('[')
285
- for (var i=0, l=chunks.length; i<l; ++i) {
286
- // If denoting an array, make sure parent is an array
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
- * @param {function(err, result)[]} tasks - array of functions
336
- * @param {string} <hookName> - e.g. 'afterFind'
337
- * @param {any} data - data to pass to the first function
338
- * @param {function(err, results[])} <cb>
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.3.0",
5
+ "version": "3.4.0",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",