monastery 1.36.1 → 1.37.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.
@@ -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,10 +6,12 @@
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
+ * CRUD operations can accept bracket (multipart/form-data) and dot notation data formats, you can also mix these together
14
+
13
15
 
14
16
  ## Install
15
17
 
@@ -63,12 +65,10 @@ db.user.insert({
63
65
  ```
64
66
  ## Debugging
65
67
 
66
- This package uses [Debug](https://github.com/visionmedia/debug) which allows you to see different levels of output:
68
+ This package uses [debug](https://github.com/visionmedia/debug) which allows you to set different levels of output via the `DEBUG` environment variable. Due to known limations `monastery:warning` and `monastery:error` are forced on, you can however disable these via [manager settings](./manager).
67
69
 
68
70
  ```bash
69
- $ DEBUG=monastery:info
70
- # or show all levels of output, currently shows the same output as above
71
- $ DEBUG=monastery:*
71
+ $ DEBUG=monastery:info # shows operation information
72
72
  ```
73
73
 
74
74
  To run isolated tests with Jest:
@@ -83,15 +83,16 @@ Coming soon...
83
83
 
84
84
  ## Roadmap
85
85
 
86
+ - Add Aggregate
86
87
  - ~~Add FindOneAndUpdate~~
87
- - Add before/afterInsertUpdate
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
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
package/lib/index.js CHANGED
@@ -16,13 +16,19 @@ module.exports = function(uri, opts, fn) {
16
16
  * @param {object} opts
17
17
  * @return monk manager
18
18
  */
19
+ if (!opts) opts = {}
19
20
  let monasteryOpts = [
20
- 'defaultObjects', 'imagePlugin', 'limit', 'nullObjects', 'timestamps', 'useMilliseconds'
21
+ 'defaultObjects', 'hideWarnings', 'hideErrors', 'imagePlugin', 'limit', 'nullObjects',
22
+ 'timestamps', 'useMilliseconds',
21
23
  ]
22
24
 
23
- if (!opts) opts = {}
25
+ // Note: Debug doesn't allow debuggers to be enabled by default
26
+ let info = debug('monastery:info')
27
+ let warn = debug('monastery:warn' + (opts.hideWarnings ? '' : '*'))
28
+ let error = debug('monastery:error' + (opts.hideErrors ? '' : '*'))
29
+
24
30
  if (util.isDefined(opts.defaultFields)) {
25
- var depreciationWarningDefaultField = true
31
+ warn('opts.defaultFields has been depreciated in favour of opts.timestamps')
26
32
  opts.timestamps = opts.defaultFields
27
33
  delete opts.defaultFields
28
34
  }
@@ -44,20 +50,15 @@ module.exports = function(uri, opts, fn) {
44
50
  manager.model = require('./model')
45
51
  manager.models = models
46
52
  manager.parseData = util.parseData.bind(util)
47
- manager.warn = debug('monastery:warn')
48
- manager.error = debug('monastery:error*')
49
- manager.info = debug('monastery:info')
53
+ manager.info = info
54
+ manager.warn = warn
55
+ manager.error = error
50
56
 
51
57
  // Add opts onto manager
52
58
  for (let key of monasteryOpts) {
53
59
  manager[key] = opts[key]
54
60
  }
55
61
 
56
- // Depreciation warnings
57
- if (depreciationWarningDefaultField) {
58
- manager.error('opts.defaultFields has been depreciated in favour of opts.timestamps')
59
- }
60
-
61
62
  // Initiate any plugins
62
63
  if (manager.imagePlugin) {
63
64
  manager.imagePluginFile.setup(manager, util.isObject(manager.imagePlugin)? manager.imagePlugin : {})
package/lib/model-crud.js CHANGED
@@ -238,7 +238,7 @@ module.exports = {
238
238
  await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, data||{})))
239
239
 
240
240
  if (data && operators['$set']) {
241
- this.warn(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
241
+ this.info(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
242
242
  }
243
243
  if (data || operators['$set']) {
244
244
  operators['$set'] = { ...data, ...(operators['$set'] || {}) }
@@ -245,7 +245,7 @@ module.exports = {
245
245
  if (typeof value === 'undefined' && (!validateUndefined || !rule.validateUndefined)) return
246
246
 
247
247
  // Ignore null (if nullObject is set on objects or arrays)
248
- if (value === null && (field.isObject || field.isArray) && field.nullObject) return
248
+ if (value === null && (field.isObject || field.isArray) && field.nullObject && !rule.validateNull) return
249
249
 
250
250
  // Ignore null
251
251
  if (value === null && !(field.isObject || field.isArray) && !rule.validateNull) return
@@ -266,8 +266,9 @@ module.exports = {
266
266
 
267
267
  _ignoredRules: [ // todo: change name? i.e. 'specialFields'
268
268
  // Need to remove filesize and formats..
269
- 'default', 'defaultOverride', 'filename', 'filesize', 'fileSize', 'formats', 'image', 'index', 'insertOnly',
270
- 'model', 'nullObject', 'params', 'getSignedUrl', 'timestampField', 'type', 'virtual'
269
+ 'awsAcl', 'awsBucket', 'default', 'defaultOverride', 'filename', 'filesize', 'fileSize', 'formats',
270
+ 'image', 'index', 'insertOnly', 'model', 'nullObject', 'params', 'path', 'getSignedUrl', 'timestampField',
271
+ 'type', 'virtual',
271
272
  ]
272
273
 
273
274
  }
package/lib/model.js CHANGED
@@ -86,7 +86,7 @@ let Model = module.exports = function(name, opts, manager) {
86
86
  if (typeof this.manager[name] === 'undefined' || typeof this.manager.model[name] !== 'undefined') {
87
87
  this.manager[name] = this
88
88
  } else {
89
- this.warn(`Your model name '${name}' is conflicting, you are only able to
89
+ this.warn(`Your model name '${name}' is conflicting with an builtin manager property, you are only able to
90
90
  access this model via \`db.model.${name}\``)
91
91
  }
92
92
 
package/lib/util.js CHANGED
@@ -163,8 +163,15 @@ module.exports = {
163
163
  * Mutates dot notation objects into a deep object
164
164
  * @param {object}
165
165
  */
166
+ let original = this.deepCopy(obj)
166
167
  for (let key in obj) {
167
- if (key.indexOf('.') !== -1) recurse(key, obj[key], obj)
168
+ if (key.indexOf('.') !== -1) {
169
+ recurse(key, obj[key], obj)
170
+ } else {
171
+ // Ordinary values may of been updated by the bracket notation values, we are
172
+ // reassigning, trying to preserve the order of keys (not always guaranteed in for loops)
173
+ obj[key] = original[key]
174
+ }
168
175
  }
169
176
  return obj
170
177
  function recurse(str, val, obj) {
@@ -188,6 +195,7 @@ module.exports = {
188
195
  },
189
196
 
190
197
  parseFormData: function(obj) {
198
+ let original = this.deepCopy(obj)
191
199
  /**
192
200
  * Mutates FormData (bracket notation) objects into a deep object
193
201
  * @param {object}
@@ -203,6 +211,10 @@ module.exports = {
203
211
  }
204
212
  if (key.indexOf('[') !== -1) {
205
213
  setup(key)
214
+ } else {
215
+ // Ordinary values may of been updated by the bracket notation values, we are
216
+ // reassigning, trying to preserve the order of keys (not always guaranteed in for loops)
217
+ obj[key] = original[key]
206
218
  }
207
219
  }
208
220
  res(obj)
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "monastery",
3
3
  "description": "⛪ A straight forward MongoDB ODM built around Monk",
4
4
  "author": "Ricky Boyce",
5
- "version": "1.36.1",
5
+ "version": "1.37.0",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -17,7 +17,7 @@
17
17
  "mongo odm"
18
18
  ],
19
19
  "scripts": {
20
- "dev": "npm run lint & DEBUG=-monastery:info jest --watchAll --runInBand --verbose=false",
20
+ "dev": "npm run lint & jest --watchAll --runInBand --verbose=false",
21
21
  "docs": "cd docs && bundle exec jekyll serve --livereload --livereload-port 4001",
22
22
  "lint": "eslint ./lib ./plugins ./test",
23
23
  "mong": "nodemon resources/mong.js",