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 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,8 @@
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
+
5
7
  ## [3.3.0](https://github.com/boycce/monastery/compare/3.2.1...3.3.0) (2024-08-07)
6
8
 
7
9
  ### [3.2.1](https://github.com/boycce/monastery/compare/3.2.0...3.2.1) (2024-08-05)
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
@@ -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
  *
@@ -542,7 +565,7 @@ Model.prototype._queryObject = async function (opts, type, _one) {
542
565
  // Data
543
566
  if (!opts) opts = {}
544
567
  if (!util.isDefined(opts.data) && util.isDefined((opts.req||{}).body)) opts.data = opts.req.body
545
- 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)/////
546
569
 
547
570
  opts.type = type
548
571
  opts[type] = true // still being included in the operation options..
@@ -554,11 +577,13 @@ Model.prototype._queryObject = async function (opts, type, _one) {
554
577
  Model.prototype._pathBlacklisted = function (path, projectionInclusion, projectionKeys, matchDeepWhitelistedKeys=true) {
555
578
  /**
556
579
  * Checks if the path is blacklisted within a inclusion/exclusion projection
557
- * @param {string} path - path without array brackets e.g. '.[]'
580
+ *
581
+ * @param {string} path - path without array brackets e.g. '.[]'
558
582
  * @param {boolean} projectionInclusion - is a inclusion or exclusion projection (default is exclusion)
559
- * @param {array} projectionKeys - inclusion/exclusion projection keys, not mixed
583
+ * @param {array} projectionKeys - inclusion/exclusion projection keys, not mixed
560
584
  * @param {boolean} matchDeepWhitelistedKeys - match deep whitelisted keys containing path
561
585
  * E.g. pets.color == pets.color.age
586
+ *
562
587
  * @return {boolean}
563
588
  */
564
589
  if (projectionInclusion) {
@@ -592,8 +617,9 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
592
617
  * e.g. "nurses": [{ model: 'user' }]
593
618
  *
594
619
  * @param {object|array|null} data
595
- * @param {object} projection - opts.projection (== opts.blacklist is merged with all found deep model blacklists)
596
- * @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
+ *
597
623
  * @return Promise(data)
598
624
  * @this model
599
625
  */
@@ -606,6 +632,7 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
606
632
  const projectionKeys = Object.keys(projection)
607
633
  const projectionInclusion = projection[projectionKeys[0]] ? true : false // default false
608
634
  if (!isArray) data = [data]
635
+
609
636
  let modelFields = this
610
637
  ._recurseAndFindModels(data)
611
638
  .concat(data.map((o, i) => ({
@@ -652,7 +679,7 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
652
679
  const _opts = { ...afterFindContext, afterFindName: _modelName }
653
680
  const _dataRef = _item.dataRefParent[_item.dataRefKey]
654
681
  _item.dataRefParent[_item.dataRefKey] = (
655
- await util.runSeries.call(_model, _model.afterFind.map(f => f.bind(_opts)), 'afterFind', _dataRef)
682
+ await _model._callHooks('afterFind', _dataRef, _opts)
656
683
  )
657
684
  }).bind(null, item)
658
685
  )
@@ -665,8 +692,10 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
665
692
  Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='') {
666
693
  /**
667
694
  * Returns a flattened list of models fields, sorted by depth
695
+ *
668
696
  * @param {object|array} dataArr
669
- * @param {string} <dataParentPath>
697
+ * @param {string} <dataParentPath>
698
+ *
670
699
  * @this Model
671
700
  * @return [{
672
701
  * dataRefParent: { *fields here* },
@@ -736,6 +765,14 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='') {
736
765
  return out
737
766
  }
738
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
+
739
776
  Model.prototype._queryOptions = [
740
777
  // todo: remove type properties
741
778
  'blacklist', 'data', 'find', 'findOneAndUpdate', 'insert', 'model', '_one', 'populate', 'project',