monastery 1.35.0 → 1.36.2

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.
@@ -0,0 +1,42 @@
1
+ ---
2
+ title: findOneAndUpdate
3
+ parent: Model
4
+ ---
5
+
6
+ # `model.findOneAndUpdate`
7
+
8
+ Find a document and update it in one atomic operation (unless using `opt.populate`), requires a write lock for the duration of the operation. Calls the following model hooks: `beforeUpdate`, `afterUpdate`, `afterFind`.
9
+
10
+ ### Arguments
11
+
12
+ Same argument signatures as [`model.find`](./find) and [`model.update`](./update).
13
+
14
+ ### Returns
15
+
16
+ A promise if no callback is passed in.
17
+
18
+ ### Example
19
+
20
+ ```js
21
+ await user.findOneAndUpdate({
22
+ query: { name: "Martin" },
23
+ data: { name: "Martin2" },
24
+ })
25
+ // { name: 'Martin2', ... }
26
+
27
+ // You can return a populated model which isn't atomic
28
+ await user.findOneAndUpdate({
29
+ query: { name: "Martin" },
30
+ data: { name: "Martin2" },
31
+ populate: ['pet'],
32
+ })
33
+ // { name: 'Martin2', pet: {...}, ... }
34
+
35
+ // Blacklisting prunes the data and returned document
36
+ await user.findOneAndUpdate({
37
+ query: { name: "Martin" },
38
+ data: { name: "Martin2", age: 100 },
39
+ blacklist: ['age'],
40
+ })
41
+ // { name: 'Martin2', ... }
42
+ ```
@@ -8,9 +8,9 @@ has_children: true
8
8
 
9
9
  Created via [`manager.model`](../manager/model).
10
10
 
11
- #### Monk collection instance methods
11
+ #### Monk collection instance operators
12
12
 
13
- Additionally models inherit most of the [monk collection](https://automattic.github.io/monk/docs/collection/) instance methods which are available under `model`.
13
+ Additionally models inherit most of the [monk collection](https://automattic.github.io/monk/docs/collection/) instance operators which are available under `model`.
14
14
 
15
15
  * model.[_aggregate](https://automattic.github.io/monk/docs/collection/aggregate.html)
16
16
  * model.[_bulkWrite](https://automattic.github.io/monk/docs/collection/bulkWrite.html)
@@ -5,16 +5,17 @@ parent: Model
5
5
 
6
6
  # `model.insert`
7
7
 
8
- Validate and insert document(s) in a collection and call related hooks: `schema.beforeInsert`, `schema.afterInsert`
8
+ Validate and insert document(s) in a collection and calls model hooks: `beforeInsert`, `afterInsert`
9
9
 
10
10
  ### Arguments
11
11
 
12
12
  `options` *(object)*
13
13
 
14
- - `options.data` *(object\|array)* - Data that is validated against the model schema. Key names can be in dot or bracket notation which is handy for HTML FormData.
15
- - [`options.skipValidation`] (string\|array): skip validation for this field name(s)
16
- - [`options.timestamps`] *(boolean)*: whether `createdAt` and `updatedAt` are automatically inserted, defaults to `manager.timestamps`
17
- - [`options.blacklist`] *(array\|string\|false)*: augment `schema.insertBL`. `false` will remove all blacklisting
14
+ - `data` *(object\|array)* - Data that is validated against the model fields. Key names can be in dot or bracket notation which is handy for HTML FormData.
15
+ - [[`blacklist`](#blacklisting)] *(array\|string\|false)*: augment `definition.insertBL`. `false` will remove all blacklisting
16
+ - [`project`] *(string\|array\|object)*: project these fields, ignores blacklisting
17
+ - [`skipValidation`] (string\|array): skip validation for this field name(s)
18
+ - [`timestamps`] *(boolean)*: whether `createdAt` and `updatedAt` are automatically inserted, defaults to `manager.timestamps`
18
19
  - [[`any mongodb option`](http://mongodb.github.io/node-mongodb-native/3.2/api/Collection.html#insert)] *(any)*
19
20
 
20
21
  [`callback`] *(function)*: pass instead of return a promise
@@ -32,32 +33,33 @@ user.insert({ data: [{ name: 'Martin Luther' }, { name: 'Bruce Lee' }]})
32
33
 
33
34
  ### Blacklisting
34
35
 
35
- You can augment the model's `schema.insertBL` blacklist by passing a custom `blacklist`:
36
+ You can augment the model's blacklist (`definition.insertBL`) by passing a custom `blacklist`:
36
37
 
37
38
  ```js
38
39
  // Prevents `name` and `pets.$.name` (array) from being returned.
39
40
  user.insert({ data: {}, blacklist: ['name', 'pets.name'] })
40
- // You can also whitelist any blacklisted fields found in schema.insertBL
41
+ // You can also whitelist any blacklisted fields found in definition.insertBL
41
42
  user.insert({ data: {}, blacklist: ['-name', '-pet'] })
42
43
  ```
43
44
 
44
45
  ### Defaults example
45
46
 
46
- When defaultObjects is enabled, undefined subdocuments and arrays will default to `{}` `[]` respectively when inserting. You can enable `defaultObjects` via the [manager options](../manager#arguments).
47
+ When defaultObjects is enabled, undefined embedded documents and arrays will default to `{}` `[]` respectively when inserting. You can enable `defaultObjects` via the [manager options](../manager#arguments).
47
48
 
48
49
  ```js
49
- db.model({ fields: {
50
- names: [{ type: 'string' }],
51
- pets: {
52
- name: { type: 'string' },
53
- colors: [{ type: 'string' }]
50
+ db.model({
51
+ fields: {
52
+ names: [{ type: 'string' }],
53
+ pets: {
54
+ name: { type: 'string' },
55
+ colors: [{ type: 'string' }]
56
+ }
54
57
  }
55
- }})
56
-
57
- user.insert({ data: {} }).then(data => {
58
- // data = {
59
- // names: [],
60
- // pets: { colors: [] }
61
- // }
62
58
  })
59
+
60
+ await user.insert({ data: {} })
61
+ // data = {
62
+ // names: [],
63
+ // pets: { colors: [] }
64
+ // }
63
65
  ```
@@ -5,14 +5,14 @@ parent: Model
5
5
 
6
6
  # `model.remove`
7
7
 
8
- Remove document(s) in a collection and call related hooks: `schema.beforeRemove`, `schema.afterRemove`
8
+ Remove document(s) in a collection and calls model hooks: `beforeRemove`, `afterRemove`
9
9
 
10
10
  ### Arguments
11
11
 
12
12
  `options` *(object)*
13
13
 
14
- - `options.query` *(object\|id)*
15
- - [`options.sort`] *(string\|object\|array)*: same as the mongodb option, but allows for string parsing e.g. 'name', 'name:1'
14
+ - `query` *(object\|id)*
15
+ - [`sort`] *(string\|object\|array)*: same as the mongodb option, but allows for string parsing e.g. 'name', 'name:1'
16
16
  - [[`any mongodb option`](http://mongodb.github.io/node-mongodb-native/3.2/api/Collection.html#remove)] *(any)*
17
17
 
18
18
  [`callback`] *(function)*: pass instead of return a promise
@@ -5,18 +5,19 @@ parent: Model
5
5
 
6
6
  # `model.update`
7
7
 
8
- Update document(s) in a collection and call related hooks: `schema.beforeUpdate`, `schema.afterUpdate`. By default this method method updates a single document. Set the `multi` mongodb option to update all documents that match the query criteria.
8
+ Update document(s) in a collection and calls model hooks: `beforeUpdate`, `afterUpdate`. By default this method method updates a single document. Set the `multi` mongodb option to update all documents that match the query criteria.
9
9
 
10
10
  ### Arguments
11
11
 
12
12
  `options` *(object)*
13
13
 
14
- - `options.query` *(object\|id)*
15
- - `options.data` *(object)* - data that's validated against the model schema and then wrapped in `{ $set: .. }`, [`more below`](#data)
16
- - [`options.skipValidation`] (string\|array): skip validation for this field name(s)
17
- - [`options.sort`] *(string\|object\|array)*: same as the mongodb option, but allows for string parsing e.g. 'name', 'name:1'
18
- - [`options.timestamps`] *(boolean)*: whether `updatedAt` is automatically updated, defaults to the `manager.timestamps` value
19
- - [`options.blacklist`] *(array\|string\|false)*: augment `schema.updateBL`. `false` will remove all blacklisting
14
+ - `query` *(object\|id)*
15
+ - [`data`](#data) *(object)* - data that's validated against the model fields (always wrapped in `{ $set: .. }`)
16
+ - [[`blacklist`](#blacklisting)]*(array\|string\|false)*: augment `definition.updateBL`. `false` will remove all blacklisting
17
+ - [`project`] *(string\|array\|object)*: project these fields, ignores blacklisting
18
+ - [`skipValidation`] (string\|array): skip validation for this field name(s)
19
+ - [`sort`] *(string\|object\|array)*: same as the mongodb option, but allows for string parsing e.g. 'name', 'name:1'
20
+ - [`timestamps`] *(boolean)*: whether `updatedAt` is automatically updated, defaults to the `manager.timestamps` value
20
21
  - [[`any mongodb option`](http://mongodb.github.io/node-mongodb-native/3.2/api/Collection.html#update)] *(any)*
21
22
 
22
23
  [`callback`] *(function)*: pass instead of return a promise
@@ -33,7 +34,7 @@ user.update({ query: { name: 'foo' }, data: { name: 'bar' }})
33
34
 
34
35
  ### Data
35
36
 
36
- Data that is validated against the model schema and then wrapped in `{ $set: .. }`. Key names can be in dot or bracket notation which is handy for HTML FormData.
37
+ Data that's validated against the model fields (always wrapped in `{ $set: .. }`). Key names can be in dot or bracket notation which is handy for HTML FormData.
37
38
 
38
39
  You can also pass `options.$set` or any other mongodb update operation instead of `options.data`, which bypasses validation, e.g.
39
40
 
@@ -48,10 +49,10 @@ user.update({ query: {}, $pull: { name: 'Martin', badField: 1 }})
48
49
 
49
50
  ### Blacklisting
50
51
 
51
- You can augment the model's `schema.updateBL` blacklist by passing a custom `blacklist`:
52
+ You can augment the model's blacklist (`updateBL`) by passing a custom `blacklist`:
52
53
 
53
54
  ```js
54
55
  // Prevents `name` and `pets.$.name` (array) from being returned.
55
56
  user.update({ query: {}, data: {}, blacklist: ['name', 'pets.name'] })
56
- // You can also whitelist any blacklisted fields found in schema.updateBL
57
+ // You can also whitelist any blacklisted fields found in updateBL
57
58
  user.update({ query: {}, data: {}, blacklist: ['-name', '-pet'] })
@@ -5,7 +5,7 @@ parent: Model
5
5
 
6
6
  # `model.validate`
7
7
 
8
- Validate a model and call related hook: `schema.beforeValidate`
8
+ Validate a model and calls the model hook: `beforeValidate`
9
9
 
10
10
 
11
11
  ### Arguments
@@ -14,10 +14,11 @@ Validate a model and call related hook: `schema.beforeValidate`
14
14
 
15
15
  [`options`] *(object)*
16
16
 
17
- - [`options.skipValidation`] (string\|array): skip validation for this field name(s)
18
- - [`options.blacklist`] *(array\|string\|false)*: augment the model's blacklist. `false` will remove all blacklisting
19
- - [`options.timestamps`] *(boolean)*: whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is updated, depending on the `options.update` value. Defaults to the `manager.timestamps` value
20
- - [`options.update`] *(boolean)*: If true, required rules will be skipped, defaults to false
17
+ - [`skipValidation`] (string\|array): skip validation for this field name(s)
18
+ - [[`blacklist`](#blacklisting)] *(array\|string\|false)*: augment the model's blacklist. `false` will remove all blacklisting
19
+ - [`project`] *(string\|array\|object)*: project these fields, ignores blacklisting
20
+ - [`timestamps`] *(boolean)*: whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is updated, depending on the `update` value. Defaults to the `manager.timestamps` value
21
+ - [`update`] *(boolean)*: If true, required rules will be skipped, defaults to false
21
22
 
22
23
  ### Returns
23
24
 
@@ -34,39 +35,35 @@ db.model('user', {
34
35
  }
35
36
  })
36
37
 
37
- db.user.validate({
38
+ await db.user.validate({
38
39
  name: 'Martin Luther',
39
40
  unknownField: 'Some data'
40
-
41
- }).then(data => {
42
- // { name: "Martin Luther" }
43
41
  })
42
+ // { name: "Martin Luther" }
44
43
 
45
- db.user.validate({
44
+ await db.user.validate({
46
45
  name: 'Martin Luther'
47
46
  address: { city: 'Eisleben' }
48
-
49
- }).catch(errs => {
50
- // [{
51
- // detail: "Value needs to be at least 10 characters long.",
52
- // status: "400",
53
- // title: "address.city",
54
- // meta: {
55
- // field: "city",
56
- // model: "user",
57
- // rule: "minLength"
58
- // }
59
- // }]
60
47
  })
61
-
48
+ // Error [{
49
+ // detail: "Value needs to be at least 10 characters long.",
50
+ // status: "400",
51
+ // title: "address.city",
52
+ // meta: {
53
+ // field: "city",
54
+ // model: "user",
55
+ // rule: "minLength"
56
+ // }
57
+ // }]
62
58
  ```
59
+
63
60
  ### Blacklisting
64
61
 
65
- Depending on the `update` option, you can augment the model's `schema.insertBL` or `schema.updateBL` by passing a custom `blacklist`:
62
+ Depending on the `update` option, you can augment the model's `insertBL` or `updateBL` by passing a custom `blacklist`:
66
63
 
67
64
  ```js
68
65
  // Prevents `name` and `pets.$.name` (array) from being returned.
69
66
  user.validate({}, { blacklist: ['name', 'pets.name'] })
70
- // You can also whitelist any blacklisted fields found in schema's blacklist
67
+ // You can also whitelist any blacklisted fields found in insertBL/updateBL
71
68
  user.validate({}, { blacklist: ['-name', '-pet'] })
72
69
  ```
package/docs/readme.md CHANGED
@@ -6,8 +6,8 @@
6
6
 
7
7
  * User friendly API design, built around the awesome [Monk](https://automattic.github.io/monk/)
8
8
  * Simple CRUD operations with model population
9
- * Model validation deriving from your schema
10
- * Custom error messages can be defined in your schema
9
+ * Model validation deriving from your model definitions
10
+ * Custom error messages can be defined in your model definition
11
11
  * Normalised error responses ready for client consumption
12
12
  * Automatic mongodb index setup
13
13
 
@@ -83,20 +83,21 @@ Coming soon...
83
83
 
84
84
  ## Roadmap
85
85
 
86
- - Add FindOneAndUpdate
87
- - Add before/afterInsertUpdate
86
+ - Add Aggregate
87
+ - ~~Add FindOneAndUpdate~~
88
+ - ~~Add beforeInsertUpdate / afterInsertUpdate~~
88
89
  - Bug: Setting an object literal on an ID field ('model') saves successfully
89
90
  - Population within array items
90
- - ~~Blacklist `false` removes all blacklisting~~
91
+ - ~~Blacklist false removes all blacklisting~~
91
92
  - ~~Add project to insert/update/validate~~
92
93
  - ~~Whitelisting a parent will remove any previously blacklisted children~~
93
94
  - ~~Blacklist/project works the same across find/insert/update/validate~~
94
- - Automatic subdocument ids
95
+ - Automatic embedded document ids/createdAt/updatedAt fields
95
96
  - Remove ACL default 'public read'
96
97
  - ~~Public db.arrayWithSchema method~~
97
98
  - Global after/before hooks
98
- - Split away from Monk (unless updated)
99
99
  - docs: Make the implicit ID query conversion more apparent
100
+ - Split away from Monk (unless updated)
100
101
 
101
102
  ## Special Thanks
102
103
 
package/lib/index.js CHANGED
@@ -1,6 +1,10 @@
1
- let util = require('./util')
2
- let monk = require('monk')
3
1
  let debug = require('debug')
2
+ let monk = require('monk')
3
+ let util = require('./util')
4
+
5
+ // Apply monk monkey patches
6
+ monk.manager.prototype.open = require('./monk-monkey-patches').open
7
+ monk.Collection.prototype.findOneAndUpdate = require('./monk-monkey-patches').findOneAndUpdate
4
8
 
5
9
  module.exports = function(uri, opts, fn) {
6
10
  /**
@@ -12,59 +16,51 @@ module.exports = function(uri, opts, fn) {
12
16
  * @param {object} opts
13
17
  * @return monk manager
14
18
  */
19
+ let monasteryOpts = [
20
+ 'defaultObjects', 'imagePlugin', 'limit', 'nullObjects', 'timestamps', 'useMilliseconds'
21
+ ]
22
+
15
23
  if (!opts) opts = {}
16
24
  if (util.isDefined(opts.defaultFields)) {
17
25
  var depreciationWarningDefaultField = true
18
26
  opts.timestamps = opts.defaultFields
27
+ delete opts.defaultFields
28
+ }
29
+ if (!util.isDefined(opts.timestamps)) {
30
+ opts.timestamps = true
19
31
  }
20
- let defaultObjects = opts.defaultObjects
21
- let imagePlugin = opts.imagePlugin
22
- let limit = opts.limit
23
- let nullObjects = opts.nullObjects
24
- let timestamps = util.isDefined(opts.timestamps)? opts.timestamps : true
25
- let useMilliseconds = opts.useMilliseconds
26
- delete opts.defaultFields
27
- delete opts.defaultObjects
28
- delete opts.imagePlugin
29
- delete opts.limit
30
- delete opts.nullObjects
31
- delete opts.timestamps
32
- delete opts.useMilliseconds
33
32
 
34
- // Monk Manager instance or manager mock
33
+ // Monk manager instance or manager mock
35
34
  // Monk manager instances have manager._db defined which is the raw mongodb connection
36
35
  if (typeof uri === 'object') var manager = uri
37
- else if (uri) manager = monk(uri, { useUnifiedTopology: true, ...opts }, fn)
36
+ else if (uri) manager = monk(uri, { useUnifiedTopology: true, ...util.omit(opts, monasteryOpts) }, fn)
38
37
  else manager = { id: monk.id }
39
38
 
40
39
  // Add monastery properties
41
- manager.error = debug('monastery:error*')
42
- manager.warn = debug('monastery:warn')
43
- manager.info = debug('monastery:info')
44
- manager.model = require('./model')
45
- manager.models = models
46
- manager.defaultObjects = defaultObjects
47
- manager.imagePlugin = imagePlugin
40
+ manager.arrayWithSchema = arrayWithSchema
41
+ manager.beforeModel = []
48
42
  manager.imagePluginFile = require('../plugins/images')
49
43
  manager.isId = util.isId.bind(util)
50
- manager.limit = limit
51
- manager.nullObjects = nullObjects
44
+ manager.model = require('./model')
45
+ manager.models = models
52
46
  manager.parseData = util.parseData.bind(util)
53
- manager.timestamps = timestamps
54
- manager.useMilliseconds = useMilliseconds
55
- manager.beforeModel = []
56
- manager.arrayWithSchema = manager.arraySchema = (array, schema) => {
57
- array.schema = schema; return array
47
+ manager.warn = debug('monastery:warn')
48
+ manager.error = debug('monastery:error*')
49
+ manager.info = debug('monastery:info')
50
+
51
+ // Add opts onto manager
52
+ for (let key of monasteryOpts) {
53
+ manager[key] = opts[key]
58
54
  }
59
55
 
60
56
  // Depreciation warnings
61
57
  if (depreciationWarningDefaultField) {
62
- manager.error('manager.defaultFields has been depreciated in favour of manager.timestamps')
58
+ manager.error('opts.defaultFields has been depreciated in favour of opts.timestamps')
63
59
  }
64
60
 
65
61
  // Initiate any plugins
66
62
  if (manager.imagePlugin) {
67
- manager.imagePluginFile.setup(manager, util.isObject(imagePlugin)? imagePlugin : {})
63
+ manager.imagePluginFile.setup(manager, util.isObject(manager.imagePlugin)? manager.imagePlugin : {})
68
64
  }
69
65
 
70
66
  // Catch mongodb connectivity errors
@@ -72,11 +68,17 @@ module.exports = function(uri, opts, fn) {
72
68
  return manager
73
69
  }
74
70
 
75
- function models(path) {
71
+ let arrayWithSchema = function(array, schema) {
72
+ array.schema = schema
73
+ return array
74
+ }
75
+
76
+ let models = function(path) {
76
77
  /**
77
78
  * Setup model definitions from a folder location
78
79
  * @param {string} pathname
79
80
  * @return {object} - e.g. { user: , article: , .. }
81
+ * @this Manager
80
82
  */
81
83
  let models = {}
82
84
  if (!path || typeof path !== 'string') {
package/lib/model-crud.js CHANGED
@@ -24,14 +24,13 @@ module.exports = {
24
24
  }
25
25
  try {
26
26
  opts = await this._queryObject(opts, 'insert')
27
- let custom = ['blacklist', 'data', 'insert', 'model', 'respond', 'skipValidation', 'validateUndefined']
28
27
 
29
28
  // Validate
30
- let data = await this.validate(opts.data || {}, { ...opts })
29
+ let data = await this.validate(opts.data || {}, opts) // was { ...opts }
31
30
 
32
31
  // Insert
33
32
  await util.runSeries(this.beforeInsert.map(f => f.bind(opts, data)))
34
- let response = await this._insert(data, util.omit(opts, custom))
33
+ let response = await this._insert(data, util.omit(opts, this._queryOptions))
35
34
  await util.runSeries(this.afterInsert.map(f => f.bind(opts, response)))
36
35
 
37
36
  // Success/error
@@ -64,9 +63,8 @@ module.exports = {
64
63
  throw new Error(`The callback passed to ${this.name}.find() is not a function`)
65
64
  }
66
65
  try {
67
- opts = await this._queryObject(opts, 'find', one)
68
- let custom = ['blacklist', 'one', 'populate', 'project', 'query', 'respond']
69
66
  let lookups = []
67
+ opts = await this._queryObject(opts, 'find', one)
70
68
 
71
69
  // Get projection
72
70
  if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
@@ -80,7 +78,7 @@ module.exports = {
80
78
 
81
79
  // Wanting to populate?
82
80
  if (!opts.populate) {
83
- var response = await this[`_find${opts.one? 'One' : ''}`](opts.query, util.omit(opts, custom))
81
+ var response = await this[`_find${opts.one? 'One' : ''}`](opts.query, util.omit(opts, this._queryOptions))
84
82
  } else {
85
83
  loop: for (let item of opts.populate) {
86
84
  let path = util.isObject(item)? item.as : item
@@ -152,11 +150,52 @@ module.exports = {
152
150
  return this.find(opts, cb, true)
153
151
  },
154
152
 
155
- findOneAndUpdate: function(opts, cb) {
156
- return this._findOneAndUpdate(opts, cb)
153
+ findOneAndUpdate: async function(opts, cb) {
154
+ /**
155
+ * Find and update document(s) with monk, also auto populates
156
+ * @param {object} opts
157
+ * @param {array|string|false} <opts.blacklist> - augment findBL/updateBL, `false` will remove all blacklisting
158
+ * @param {array} <opts.populate> - find population, see docs
159
+ * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
160
+ * @param {object} <opts.query> - mongodb query object
161
+ * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
162
+ * @param {any} <any mongodb option>
163
+ *
164
+ * Update options:
165
+ * @param {object|array} opts.data - mongodb document update object(s)
166
+ * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
167
+ * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
168
+ * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
169
+ * default, but false on update
170
+ * @param {function} <cb> - execute cb(err, data) instead of responding
171
+ * @return promise
172
+ * @this model
173
+ */
174
+
175
+ if (opts.populate) {
176
+ try {
177
+ // todo: add transaction flag
178
+ delete opts.multi
179
+ let update = await this.update(opts, null, 'findOneAndUpdate')
180
+ if (update) var response = await this.findOne(opts)
181
+ else response = update
182
+
183
+ // Success
184
+ if (cb) cb(null, response)
185
+ else if (opts.req && opts.respond) opts.req.res.json(response)
186
+ else return Promise.resolve(response)
187
+
188
+ } catch (e) {
189
+ if (cb) cb(e)
190
+ else if (opts && opts.req && opts.respond) opts.req.res.error(e)
191
+ else throw e
192
+ }
193
+ } else {
194
+ return this.update(opts, cb, 'findOneAndUpdate')
195
+ }
157
196
  },
158
197
 
159
- update: async function(opts, cb) {
198
+ update: async function(opts, cb, type='update') {
160
199
  /**
161
200
  * Updates document(s) with monk after validating data & before hooks.
162
201
  * @param {object} opts
@@ -171,41 +210,60 @@ module.exports = {
171
210
  * default, but false on update
172
211
  * @param {any} <any mongodb option>
173
212
  * @param {function} <cb> - execute cb(err, data) instead of responding
213
+ * @param {function} <type> - 'update', or 'findOneAndUpdate'
174
214
  * @return promise(data)
175
215
  * @this model
176
216
  */
177
217
  if (cb && !util.isFunction(cb)) {
178
- throw new Error(`The callback passed to ${this.name}.update() is not a function`)
218
+ throw new Error(`The callback passed to ${this.name}.${type}() is not a function`)
179
219
  }
180
220
  try {
181
- opts = await this._queryObject(opts, 'update')
221
+ opts = await this._queryObject(opts, type)
182
222
  let data = opts.data
183
223
  let response = null
184
- let operators = util.pluck(opts, [/^\$/])
185
- let custom = ['blacklist', 'data', 'query', 'respond', 'skipValidation', 'validateUndefined']
224
+ let operators = util.pick(opts, [/^\$/])
186
225
 
187
226
  // Validate
188
- if (util.isDefined(data)) data = await this.validate(opts.data, { ...opts })
227
+ if (util.isDefined(data)) {
228
+ data = await this.validate(opts.data, opts) // was {...opts}
229
+ }
189
230
  if (!util.isDefined(data) && util.isEmpty(operators)) {
190
- throw new Error(`Please pass an update operator to ${this.name}.update(), e.g. data, $unset, etc`)
231
+ throw new Error(`Please pass an update operator to ${this.name}.${type}(), e.g. data, $unset, etc`)
191
232
  }
192
233
  if (util.isDefined(data) && (!data || util.isEmpty(data))) {
193
- throw new Error(`No valid data passed to ${this.name}.update({ data: .. })`)
234
+ throw new Error(`No valid data passed to ${this.name}.${type}({ data: .. })`)
194
235
  }
236
+
195
237
  // Hook: beforeUpdate (has access to original, non-validated opts.data)
196
238
  await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, data||{})))
239
+
197
240
  if (data && operators['$set']) {
198
- this.warn(`'$set' fields take precedence over the data fields for \`${this.name}.update()\``)
241
+ this.warn(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
199
242
  }
200
243
  if (data || operators['$set']) {
201
244
  operators['$set'] = { ...data, ...(operators['$set'] || {}) }
202
245
  }
246
+
247
+ // findOneAndUpdate, get 'find' projection
248
+ if (type == 'findOneAndUpdate') {
249
+ if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
250
+ else opts.projection = this._getProjectionFromBlacklist('find', opts.blacklist)
251
+ // Just peform a normal update if we need to populate a findOneAndUpdate
252
+ if (opts.populate) type = 'update'
253
+ }
254
+
203
255
  // Update
204
- let update = await this._update(opts.query, operators, util.omit(opts, custom))
205
- if (update.n) response = Object.assign(Object.create({ _output: update }), operators['$set']||{})
256
+ let update = await this['_' + type](opts.query, operators, util.omit(opts, this._queryOptions))
257
+ if (type == 'findOneAndUpdate') response = update
258
+ else if (update.n) response = Object.assign(Object.create({ _output: update }), operators['$set']||{})
206
259
 
207
260
  // Hook: afterUpdate (doesn't have access to validated data)
208
- if (update.n) await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)))
261
+ if (response) await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)))
262
+
263
+ // Hook: afterFind if findOneAndUpdate
264
+ if (response && type == 'findOneAndUpdate') {
265
+ response = await this._processAfterFind(response, opts.projection, opts)
266
+ }
209
267
 
210
268
  // Success
211
269
  if (cb) cb(null, response)
@@ -236,12 +294,11 @@ module.exports = {
236
294
  }
237
295
  try {
238
296
  opts = await this._queryObject(opts, 'remove')
239
- let custom = ['query', 'respond']
240
297
  if (util.isEmpty(opts.query)) throw new Error('Please specify opts.query')
241
298
 
242
299
  // Remove
243
300
  await util.runSeries(this.beforeRemove.map(f => f.bind(opts)))
244
- let response = await this._remove(opts.query, util.omit(opts, custom))
301
+ let response = await this._remove(opts.query, util.omit(opts, this._queryOptions))
245
302
  await util.runSeries(this.afterRemove.map(f => f.bind(response)))
246
303
 
247
304
  // Success
@@ -368,7 +425,7 @@ module.exports = {
368
425
  /**
369
426
  * Normalise options
370
427
  * @param {MongoId|string|object} opts
371
- * @param {string} type - operation type
428
+ * @param {string} type - insert, update, find, remove, findOneAndUpdate
372
429
  * @param {boolean} one - return one document
373
430
  * @return {Promise} opts
374
431
  * @this model
@@ -397,7 +454,7 @@ module.exports = {
397
454
  throw new Error('Please pass an object or MongoId to options.query')
398
455
  }
399
456
  if (util.isId(opts.query._id)) opts.query._id = this.manager.id(opts.query._id)
400
- if (isIdType(opts.query._id) || one) opts.one = true
457
+ if (isIdType(opts.query._id) || one || type == 'findOneAndUpdate') opts.one = true
401
458
  opts.query = util.removeUndefined(opts.query)
402
459
 
403
460
  // Query options
@@ -417,7 +474,7 @@ module.exports = {
417
474
  if (util.isDefined(opts.data)) opts.data = await util.parseData(opts.data)
418
475
 
419
476
  opts.type = type
420
- opts[type] = true
477
+ opts[type] = true // still being included in the operation options..
421
478
  opts.model = this
422
479
  return opts
423
480
  },
@@ -574,4 +631,11 @@ module.exports = {
574
631
  }, this)
575
632
  },
576
633
 
634
+ _queryOptions: [
635
+ // todo: remove type properties
636
+ 'blacklist', 'data', 'find', 'findOneAndUpdate', 'insert', 'model', 'one', 'populate', 'project',
637
+ 'projectionValidate', 'query', 'remove', 'req', 'respond', 'skipValidation', 'type', 'update',
638
+ 'validateUndefined',
639
+ ],
640
+
577
641
  }