monastery 3.2.1 → 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 CHANGED
@@ -32,7 +32,7 @@
32
32
  "exports": "always-multiline",
33
33
  "functions": "never"
34
34
  }],
35
- "max-len": ["error", { "code": 125, "ignorePattern": "^\\s*<(rect|path|line)\\s" }],
35
+ "max-len": ["error", { "code": 130, "ignorePattern": "^\\s*<(rect|path|line)\\s" }],
36
36
  "no-prototype-builtins": "off",
37
37
  "no-unused-vars": ["error", { "args": "none" }],
38
38
  "object-shorthand": ["error", "consistent"],
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.4.0](https://github.com/boycce/monastery/compare/3.3.0...3.4.0) (2024-08-09)
6
+
7
+ ## [3.3.0](https://github.com/boycce/monastery/compare/3.2.1...3.3.0) (2024-08-07)
8
+
5
9
  ### [3.2.1](https://github.com/boycce/monastery/compare/3.2.0...3.2.1) (2024-08-05)
6
10
 
7
11
  ## [3.2.0](https://github.com/boycce/monastery/compare/3.1.0...3.2.0) (2024-07-17)
@@ -13,11 +13,12 @@ Monastery manager constructor.
13
13
  `uri` *(string\|array)*: A [mongo connection string URI](https://www.mongodb.com/docs/v5.0/reference/connection-string/). Replica sets can be an array or comma separated.
14
14
 
15
15
  [`options`] *(object)*:
16
- - [`defaultObjects=false`] *(boolean)*: when [inserting](../model/insert.html#defaults-example), undefined embedded documents and arrays are defined
17
- - [`logLevel=2`] *(number)*: 1=errors, 2=warnings, 3=info. You can also use the debug environment variable `DEBUG=monastery:info`
16
+ - [`defaultObjects=false`] *(boolean)*: when [inserting](../model/insert.html#defaults-example), undefined embedded documents and arrays are defined.
17
+ - [`logLevel=2`] *(number)*: 1=errors, 2=warnings, 3=info. You can also use the debug environment variable `DEBUG=monastery:info`.
18
+ - [`noDefaults`] *(boolean\|string\|array)*: after find operations, don't add defaults for any matching paths, e.g. ['pet.name']. You can override this per operation.
18
19
  - [`nullObjects=false`] *(boolean)*: embedded documents and arrays can be set to null or an empty string (which gets converted to null). You can override this per field via `nullObject: true`.
19
- - [`promise=false`] *(boolean)*: return a promise instead of the manager instance
20
- - [`timestamps=true`] *(boolean)*: whether to use [`createdAt` and `updatedAt`](../definition), this can be overridden per operation
20
+ - [`promise=false`] *(boolean)*: return a promise instead of the manager instance.
21
+ - [`timestamps=true`] *(boolean)*: whether to use [`createdAt` and `updatedAt`](../definition), this can be overridden per operation.
21
22
  - [`useMilliseconds=false`] *(boolean)*: by default the `createdAt` and `updatedAt` fields that get created automatically use unix timestamps in seconds, set this to true to use milliseconds instead.
22
23
  - [`mongo options`](https://mongodb.github.io/node-mongodb-native/5.9/interfaces/MongoClientOptions.html)...
23
24
 
package/docs/readme.md CHANGED
@@ -86,7 +86,7 @@ You can view MongoDB's [compatibility table here](https://www.mongodb.com/docs/d
86
86
  ## v3 Breaking Changes
87
87
 
88
88
  - Removed callback functions on all model methods, you can use the returned promise instead
89
- - model.update() now returns the following _update property: `{ acknowledged: true, matchedCount: 1, modifiedCount: 1, upsertedCount: 0, upsertedId: null }` instead of `{ n: 1, nModified: 1, ok: 1 }`
89
+ - model.update() now returns the following res._output property: `{ acknowledged: true, matchedCount: 1, modifiedCount: 1, upsertedCount: 0, upsertedId: null }` instead of `{ n: 1, nModified: 1, ok: 1 }`
90
90
  - model.remove() now returns `{ acknowledged: true, deletedCount: 1 }`, instead of `{ results: {n:1, ok:1} }`
91
91
  - model._indexes() now returns collection._indexes() not collection._indexInformation()
92
92
  - db.model.* moved to db.models.*
@@ -95,7 +95,7 @@ You can view MongoDB's [compatibility table here](https://www.mongodb.com/docs/d
95
95
  - db._db moved to db.db
96
96
  - db.catch/then() moved to db.onError/db.onOpen()
97
97
  - next() is now redundant when returning promises from hooks, e.g. `afterFind: [async (data) => {...}]`
98
- - option `skipValidation: true` now skips validation hooks
98
+ - deep paths in data, e.g. `books[].title` are now validated, and don't replace the whole object, e.g. `books`
99
99
 
100
100
  ## v2 Breaking Changes
101
101
 
@@ -134,6 +134,8 @@ You can view MongoDB's [compatibility table here](https://www.mongodb.com/docs/d
134
134
  - ~~added `model.count()`~~
135
135
  - Typescript support
136
136
  - Add soft remove plugin
137
+ - ~~Added deep path validation support for updates~~
138
+ - ~~Added option skipHooks~~
137
139
 
138
140
  ## Debugging
139
141
 
package/lib/index.js CHANGED
@@ -38,7 +38,7 @@ function Manager(uri, opts) {
38
38
  const mongoOpts = Object.keys(opts||{}).reduce((acc, key) => {
39
39
  if (
40
40
  ![
41
- 'databaseName', 'defaultObjects', 'logLevel', 'imagePlugin', 'limit', 'nullObjects',
41
+ 'databaseName', 'defaultObjects', 'logLevel', 'imagePlugin', 'limit', 'noDefaults', 'nullObjects',
42
42
  'promise', 'timestamps', 'useMilliseconds',
43
43
  ].includes(key)) {
44
44
  acc[key] = opts[key]
@@ -277,8 +277,20 @@ Manager.prototype.open = async function() {
277
277
  }
278
278
  }
279
279
 
280
- Manager.prototype.parseData = function(obj) {
281
- return util.parseData(obj)
280
+ Manager.prototype.parseData = function(obj, parseBracketToDotNotation, parseDotNotation) {
281
+ return util.parseData(obj, parseBracketToDotNotation, parseDotNotation)
282
+ }
283
+
284
+ Manager.prototype.parseBracketNotation = function(obj) {
285
+ return util.parseBracketNotation(obj)
286
+ }
287
+
288
+ Manager.prototype.parseBracketToDotNotation = function(obj) {
289
+ return util.parseBracketToDotNotation(obj)
290
+ }
291
+
292
+ Manager.prototype.parseDotNotation = function(obj) {
293
+ return util.parseDotNotation(obj)
282
294
  }
283
295
 
284
296
  Manager.prototype.model = Model
package/lib/model-crud.js CHANGED
@@ -5,9 +5,9 @@ Model.prototype.count = async function (opts) {
5
5
  /**
6
6
  * Count document(s)
7
7
  * @param {object} opts
8
- * @param {object} <opts.query> - mongodb query object
8
+ * @param {object} <opts.query> - mongodb query object
9
9
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
10
- * @param {any} <any mongodb option>
10
+ * @param {any} <any mongodb option>
11
11
  * @return promise
12
12
  * @this model
13
13
  */
@@ -28,30 +28,38 @@ Model.prototype.count = async function (opts) {
28
28
  Model.prototype.insert = async function (opts) {
29
29
  /**
30
30
  * Inserts document(s) after validating data & before hooks.
31
+ *
31
32
  * @param {object} opts
32
- * @param {object|array} opts.data - documents to insert
33
- * @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove blacklisting
34
- * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
35
- * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
36
- * @param {array|string|true} <opts.skipValidation> - skip validation for these fields, or pass `true` to skip
37
- * all fields and hooks
38
- * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
39
- * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
40
- * default, but false on update
41
- * @param {any} <any mongodb option>
33
+ * @param {object|array} opts.data - documents to insert
34
+ * @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove blacklisting
35
+ * @param {boolean} <opts.bracketToDotNotation> - covert fields in bracket notation (form data) to
36
+ * paths in dot notation instead of objects, e.g. { 'user[names][0]': 'John' } => { user.names.0 }
37
+ * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
38
+ * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
39
+ * @param {boolean} <opts.skipHooks> - skip insert and validate before/after hooks
40
+ * @param {array|string|boolean} <opts.skipValidation> - skip validation for these fields, or pass `true` for all fields.
41
+ * $set and $unset objects are skipped by default, but can be enabled via opts.skipValidation=false
42
+ * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
43
+ * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
44
+ * default, but false on update
45
+ * @param {any} <any mongodb option>
46
+ *
42
47
  * @return promise
43
48
  * @this model
44
49
  */
45
50
  try {
46
51
  opts = await this._queryObject(opts, 'insert')
52
+ let data = opts.data
47
53
 
48
54
  // Validate
49
- let data = await this.validate(opts.data || {}, opts) // was { ...opts }
55
+ if (this._shouldValidate(opts, 'data')) {
56
+ data = await this.validate(data || {}, opts) // was { ...opts }
57
+ }
50
58
 
51
59
  // Insert
52
- data = await util.runSeries.call(this, this.beforeInsert.map(f => f.bind(opts)), 'beforeInsert', data)
60
+ data = await this._callHooks('beforeInsert', data, opts)
53
61
  let response = await this._insert(data, util.omit(opts, this._queryOptions))
54
- response = await util.runSeries.call(this, this.afterInsert.map(f => f.bind(opts)), 'afterInsert', response)
62
+ response = await this._callHooks('afterInsert', response, opts)
55
63
 
56
64
  // Success/error
57
65
  if (opts.req && opts.respond) opts.req.res.json(response)
@@ -66,15 +74,18 @@ Model.prototype.insert = async function (opts) {
66
74
  Model.prototype.find = async function (opts, _one) {
67
75
  /**
68
76
  * Finds document(s), with auto population
77
+ *
69
78
  * @param {object} opts (todo doc getSignedUrls like in the doc)
70
- * @param {array|string|false} <opts.blacklist> - augment schema.findBL, `false` will remove all blacklisting
71
- * @param {boolean|string|array} <opts.noDefaults> - dont add defaults for any matching paths, e.g. ['pet.name']
72
- * @param {array} <opts.populate> - population, see docs
73
- * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
74
- * @param {object} <opts.query> - mongodb query object
75
- * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
76
- * @param {any} <any mongodb option>
79
+ * @param {object} <opts.query> - mongodb query object
80
+ * @param {array|string|false} <opts.blacklist> - augment schema.findBL, `false` will remove all blacklisting
81
+ * @param {boolean|string|array} <opts.noDefaults> - dont add defaults for any matching paths, e.g. ['pet.name']
82
+ * @param {array} <opts.populate> - population, see docs
83
+ * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
84
+ * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
85
+ * @param {boolean} <opts.skipHooks> - skip after find hooks
86
+ * @param {any} <any mongodb option>
77
87
  * @param {boolean} <_one> - return one document
88
+ *
78
89
  * @return promise
79
90
  * @this model
80
91
  */
@@ -215,22 +226,20 @@ Model.prototype.findOne = async function (opts) {
215
226
  Model.prototype.findOneAndUpdate = async function (opts) {
216
227
  /**
217
228
  * Find and update document(s) with auto population
229
+ *
218
230
  * @param {object} opts
219
- * @param {array|string|false} <opts.blacklist> - augment findBL/updateBL, `false` will remove all blacklisting
220
- * @param {boolean|string|array} <opts.noDefaults> - dont add defaults for any matching paths, e.g. ['pet.name']
221
- * @param {array} <opts.populate> - find population, see docs
222
- * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
223
- * @param {object} <opts.query> - mongodb query object
224
- * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
225
- * @param {any} <any mongodb option>
226
- *
227
- * Update options:
228
- * @param {object|array} opts.data - mongodb document update object(s)
229
- * @param {array|string|true} <opts.skipValidation>- skip validation for these fields, or pass `true` to skip
230
- * all fields and hooks
231
- * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
232
- * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
233
- * default, but false on update
231
+ * Find options:
232
+ * @param {object} <opts.query> - mongodb query object
233
+ * @param {array} <opts.populate> - find population, see docs
234
+ * @param {any} <any model.find option>
235
+ *
236
+ * Update options:
237
+ * @param {object|array} opts.data - mongodb document update object(s)
238
+ * @param {any} <any model.update option>
239
+ *
240
+ * Mongo options:
241
+ * @param {any} <any mongodb option>
242
+ *
234
243
  * @return promise
235
244
  * @this model
236
245
  */
@@ -259,50 +268,59 @@ Model.prototype.findOneAndUpdate = async function (opts) {
259
268
  Model.prototype.update = async function (opts, type='update') {
260
269
  /**
261
270
  * Updates document(s) after validating data & before hooks.
271
+ *
262
272
  * @param {object} opts
263
- * @param {object|array} opts.data - mongodb document update object(s)
264
- * @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove blacklisting
265
- * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
266
- * @param {object} <opts.query> - mongodb query object
267
- * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
268
- * @param {array|string|true} <opts.skipValidation> - skip validation for these fields, or pass `true` to skip
269
- * all fields and hooks
270
- * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
271
- * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
272
- * default, but false on update
273
- * @param {any} <any mongodb option>
273
+ * @param {object} opts.query - mongodb query object
274
+ * @param {object|array} opts.data - mongodb document update object(s)
275
+ * @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove blacklisting
276
+ * @param {boolean} <opts.bracketToDotNotation> - covert fields in bracket notation (form data) to
277
+ * paths in dot notation instead of objects, e.g. { 'user[names][0]': 'John' } => { user.names.0 }
278
+ * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
279
+ * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
280
+ * @param {boolean} <opts.skipHooks> - validate and update before/after hooks
281
+ * @param {array|string|boolean} <opts.skipValidation> - skip validation for these fields, or pass `true` for all fields.
282
+ * $set and $unset objects are skipped by default, but can be enabled via opts.skipValidation=false
283
+ * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
284
+ * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
285
+ * default, but false on update
286
+ * @param {any} <any mongodb option or operation>
274
287
  * @param {function} <type> - 'update', or 'findOneAndUpdate'
288
+ *
275
289
  * @return promise(data)
276
290
  * @this model
277
291
  */
278
292
  try {
279
293
  opts = await this._queryObject(opts, type)
280
- let data = opts.data
281
294
  let response = null
282
- let operators = util.pick(opts, [/^\$/])
295
+ let operators = util.removeUndefined(util.pick(opts, [/^\$/, 'data']))
283
296
 
284
- // Validate
285
- if (util.isDefined(data)) {
286
- data = await this.validate(opts.data, opts) // was {...opts}
287
- }
288
- if (!util.isDefined(data) && util.isEmpty(operators)) {
297
+ if (operators.data && operators.$set) {
298
+ this.info(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
299
+ } else if (!Object.keys(operators).length) {
289
300
  throw new Error(`Please pass an update operator to ${this.name}.${type}(), e.g. data, $unset, etc`)
290
301
  }
291
- if (util.isDefined(data) && (!data || util.isEmpty(data))) {
292
- throw new Error(`No valid data passed to ${this.name}.${type}({ data: .. })`)
293
- }
294
302
 
295
- // Hook: beforeUpdate (has access to original, non-validated opts.data)
296
- data = data || {}
297
- data = await util.runSeries.call(this, this.beforeUpdate.map(f => f.bind(opts)), 'beforeUpdate', data)
298
-
299
- if (data && operators['$set']) {
300
- this.info(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
303
+ // Validate data (doesn't mutate opts)
304
+ for (let key in operators) {
305
+ if (this._shouldValidate(opts, key)) {
306
+ operators[key] = await this.validate(operators[key], opts)
307
+ }
308
+ if (util.isEmpty(operators[key] || {})) {
309
+ throw new Error(`No valid data passed to ${this.name}.${type}({ ${key}: .. })`)
310
+ }
301
311
  }
302
- if (data || operators['$set']) {
303
- operators['$set'] = { ...data, ...(operators['$set'] || {}) }
312
+
313
+ // Merge data into $set
314
+ if (operators.data || operators.$set) {
315
+ operators.$set = { ...(operators.data||{}), ...(operators['$set'] || {}) }
316
+ delete operators.data
304
317
  }
305
318
 
319
+ // Hook: beforeUpdate (has access to original, non-validated data via opts.data)
320
+ const onlySet = Object.keys(operators).length == 1 && operators.$set
321
+ if (onlySet) operators.$set = await this._callHooks('beforeUpdate', operators.$set, opts)
322
+ else operators = await this._callHooks('beforeUpdate', operators, opts)
323
+
306
324
  // findOneAndUpdate, get 'find' projection
307
325
  if (type == 'findOneAndUpdate') {
308
326
  if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
@@ -323,10 +341,8 @@ Model.prototype.update = async function (opts, type='update') {
323
341
  )
324
342
  }
325
343
 
326
- // Hook: afterUpdate (doesn't have access to validated data)
327
- if (response) {
328
- response = await util.runSeries.call(this, this.afterUpdate.map(f => f.bind(opts)), 'afterUpdate', response)
329
- }
344
+ // Hook: afterUpdate (doesn't have access to validated data, just the response)
345
+ if (response) response = await this._callHooks('afterUpdate', response, opts)
330
346
 
331
347
  // Hook: afterFind if findOneAndUpdate
332
348
  if (response && type == 'findOneAndUpdate') {
@@ -346,11 +362,14 @@ Model.prototype.update = async function (opts, type='update') {
346
362
  Model.prototype.remove = async function (opts) {
347
363
  /**
348
364
  * Remove document(s) with before and after hooks.
365
+ *
349
366
  * @param {object} opts
350
- * @param {object} <opts.query> - mongodb query object
351
- * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
352
- * @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
353
- * @param {any} <any mongodb option>
367
+ * @param {object} <opts.query> - mongodb query object
368
+ * @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
369
+ * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
370
+ * @param {boolean} <opts.skipHooks> - remove before/after hooks
371
+ * @param {any} <any mongodb option>
372
+ *
354
373
  * @return promise
355
374
  * @this model
356
375
  */
@@ -358,9 +377,9 @@ Model.prototype.remove = async function (opts) {
358
377
  opts = await this._queryObject(opts, 'remove')
359
378
 
360
379
  // Remove
361
- await util.runSeries.call(this, this.beforeRemove.map(f => f.bind(opts)), 'beforeRemove')
380
+ await this._callHooks('beforeRemove', null, opts)
362
381
  let response = await this._remove(opts.query, util.omit(opts, this._queryOptions))
363
- await util.runSeries.call(this, this.afterRemove.map(f => f.bind(response)), 'afterRemove')
382
+ await this._callHooks('afterRemove', response, opts)
364
383
 
365
384
  // Success
366
385
  if (opts.req && opts.respond) opts.req.res.json(response)
@@ -379,9 +398,10 @@ Model.prototype._getProjectionFromBlacklist = function (type, customBlacklist) {
379
398
  * Path collisions are removed
380
399
  * E.g. ['pets.dogs', 'pets.dogs.name', '-cat', 'pets.dogs.age'] = { 'pets.dog': 0 }
381
400
  *
382
- * @param {string} type - find, insert, or update
401
+ * @param {string} type - find, insert, or update
383
402
  * @param {array|string|false} customBlacklist - normally passed through options
384
- * @return {array|undefined} exclusion $project {'pets.name': 0}
403
+ *
404
+ * @return {array|undefined} exclusion $project {'pets.name': 0}
385
405
  * @this model
386
406
  *
387
407
  * 1. collate deep-blacklists
@@ -467,7 +487,8 @@ Model.prototype._getProjectionFromProject = function (customProject) {
467
487
  * todo: tests
468
488
  *
469
489
  * @param {object|array|string} customProject - normally passed through options
470
- * @return {array|undefined} in/exclusion projection {'pets.name': 0}
490
+ *
491
+ * @return {array|undefined} in/exclusion projection {'pets.name': 0}
471
492
  * @this model
472
493
  */
473
494
  let projection
@@ -488,9 +509,11 @@ Model.prototype._getProjectionFromProject = function (customProject) {
488
509
  Model.prototype._queryObject = async function (opts, type, _one) {
489
510
  /**
490
511
  * Normalize options
512
+ *
491
513
  * @param {MongoId|string|object} opts
492
- * @param {string} type - insert, update, find, remove, findOneAndUpdate
493
- * @param {boolean} _one - return one document
514
+ * @param {string} type - insert, update, find, remove, findOneAndUpdate
515
+ * @param {boolean} _one - return one document
516
+ *
494
517
  * @return {Promise} opts
495
518
  * @this model
496
519
  *
@@ -531,6 +554,9 @@ Model.prototype._queryObject = async function (opts, type, _one) {
531
554
  let order = (opts.sort.match(/:(-?[0-9])/) || [])[1]
532
555
  opts.sort = { [name]: parseInt(order || 1) }
533
556
  }
557
+ if (typeof opts.noDefaults == 'undefined' && typeof this.manager.opts.noDefaults != 'undefined') {
558
+ opts.noDefaults = this.manager.opts.noDefaults
559
+ }
534
560
  if (util.isString(opts.noDefaults)) {
535
561
  opts.noDefaults = [opts.noDefaults]
536
562
  }
@@ -539,7 +565,7 @@ Model.prototype._queryObject = async function (opts, type, _one) {
539
565
  // Data
540
566
  if (!opts) opts = {}
541
567
  if (!util.isDefined(opts.data) && util.isDefined((opts.req||{}).body)) opts.data = opts.req.body
542
- if (util.isDefined(opts.data)) opts.data = await util.parseData(opts.data)
568
+ if (util.isDefined(opts.data)) opts.data = await util.parseData(opts.data, opts.bracketToDotNotation)/////
543
569
 
544
570
  opts.type = type
545
571
  opts[type] = true // still being included in the operation options..
@@ -551,11 +577,13 @@ Model.prototype._queryObject = async function (opts, type, _one) {
551
577
  Model.prototype._pathBlacklisted = function (path, projectionInclusion, projectionKeys, matchDeepWhitelistedKeys=true) {
552
578
  /**
553
579
  * Checks if the path is blacklisted within a inclusion/exclusion projection
554
- * @param {string} path - path without array brackets e.g. '.[]'
580
+ *
581
+ * @param {string} path - path without array brackets e.g. '.[]'
555
582
  * @param {boolean} projectionInclusion - is a inclusion or exclusion projection (default is exclusion)
556
- * @param {array} projectionKeys - inclusion/exclusion projection keys, not mixed
583
+ * @param {array} projectionKeys - inclusion/exclusion projection keys, not mixed
557
584
  * @param {boolean} matchDeepWhitelistedKeys - match deep whitelisted keys containing path
558
585
  * E.g. pets.color == pets.color.age
586
+ *
559
587
  * @return {boolean}
560
588
  */
561
589
  if (projectionInclusion) {
@@ -589,8 +617,9 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
589
617
  * e.g. "nurses": [{ model: 'user' }]
590
618
  *
591
619
  * @param {object|array|null} data
592
- * @param {object} projection - opts.projection (== opts.blacklist is merged with all found deep model blacklists)
593
- * @param {object} afterFindContext - handy context object given to schema.afterFind
620
+ * @param {object} projection - opts.projection (== opts.blacklist is merged with all found deep model blacklists)
621
+ * @param {object} afterFindContext - handy context object given to schema.afterFind
622
+ *
594
623
  * @return Promise(data)
595
624
  * @this model
596
625
  */
@@ -603,6 +632,7 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
603
632
  const projectionKeys = Object.keys(projection)
604
633
  const projectionInclusion = projection[projectionKeys[0]] ? true : false // default false
605
634
  if (!isArray) data = [data]
635
+
606
636
  let modelFields = this
607
637
  ._recurseAndFindModels(data)
608
638
  .concat(data.map((o, i) => ({
@@ -649,7 +679,7 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
649
679
  const _opts = { ...afterFindContext, afterFindName: _modelName }
650
680
  const _dataRef = _item.dataRefParent[_item.dataRefKey]
651
681
  _item.dataRefParent[_item.dataRefKey] = (
652
- await util.runSeries.call(_model, _model.afterFind.map(f => f.bind(_opts)), 'afterFind', _dataRef)
682
+ await _model._callHooks('afterFind', _dataRef, _opts)
653
683
  )
654
684
  }).bind(null, item)
655
685
  )
@@ -662,8 +692,10 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
662
692
  Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='') {
663
693
  /**
664
694
  * Returns a flattened list of models fields, sorted by depth
695
+ *
665
696
  * @param {object|array} dataArr
666
- * @param {string} <dataParentPath>
697
+ * @param {string} <dataParentPath>
698
+ *
667
699
  * @this Model
668
700
  * @return [{
669
701
  * dataRefParent: { *fields here* },
@@ -733,6 +765,14 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='') {
733
765
  return out
734
766
  }
735
767
 
768
+ Model.prototype._shouldValidate = function (opts, operatorName) {
769
+ return ['data', '$set', '$unset'].includes(operatorName) && (
770
+ opts.skipValidation === false ? true
771
+ : opts.skipValidation && opts.skipValidation !== true ? true
772
+ : operatorName == 'data' // enabled by default
773
+ )
774
+ }
775
+
736
776
  Model.prototype._queryOptions = [
737
777
  // todo: remove type properties
738
778
  'blacklist', 'data', 'find', 'findOneAndUpdate', 'insert', 'model', '_one', 'populate', 'project',