monastery 1.36.2 → 1.37.1

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/changelog.md CHANGED
@@ -2,6 +2,32 @@
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
+ ### [1.37.1](https://github.com/boycce/monastery/compare/1.37.0...1.37.1) (2022-06-13)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * add _id to upserted document ([a78512f](https://github.com/boycce/monastery/commit/a78512f16b39422160756fe91ef4d4a3239f5890))
11
+
12
+ ## [1.37.0](https://github.com/boycce/monastery/compare/1.36.3...1.37.0) (2022-06-12)
13
+
14
+
15
+ ### Features
16
+
17
+ * added manager options (path, params, getSignedUrl, filesize, awsAcl) ([3e95609](https://github.com/boycce/monastery/commit/3e95609385820ecfd606500676fa87f8c9b4f02d))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * warnings printing by default ([e1dc442](https://github.com/boycce/monastery/commit/e1dc442f20c78fcc65e19afaf5f0f8410bbcc31d))
23
+
24
+ ### [1.36.3](https://github.com/boycce/monastery/compare/1.36.2...1.36.3) (2022-05-27)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * order maintained when mixing of formData/dotNotation with normal key values ([898fa90](https://github.com/boycce/monastery/commit/898fa90a25e81ae1d2413e32ca67173fdef733a9))
30
+
5
31
  ### [1.36.2](https://github.com/boycce/monastery/compare/1.36.1...1.36.2) (2022-05-26)
6
32
 
7
33
 
@@ -10,11 +10,15 @@ To use the default image plugin shipped with monastery, you need to use the opti
10
10
  ```js
11
11
  let db = monastery('localhost/mydb', {
12
12
  imagePlugin: {
13
+ awsAcl: 'public-read', // default
13
14
  awsBucket: 'your-bucket-name',
14
15
  awsAccessKeyId: 'your-key-here',
15
16
  awsSecretAccessKey: 'your-key-here',
16
- bucketDir: 'full', // default
17
- formats: ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'tiff'] // or 'any' to include everything
17
+ filesize: undefined, // default (max filesize in bytes)
18
+ formats: ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'tiff'], // default (use 'any' to allow all extensions)
19
+ getSignedUrl: false, // default (get a S3 signed url after `model.find()`, can be defined per request)
20
+ path: (uid, basename, ext, file) => `/full/${uid}.${ext}`, // default
21
+ params: {}, // upload params (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property)
18
22
  }
19
23
  })
20
24
  ```
@@ -25,15 +29,12 @@ Then in your model definition, e.g.
25
29
  let user = db.model('user', {
26
30
  fields: {
27
31
  logo: {
28
- type: 'image', // required
29
- formats: ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'tiff'],
30
- filename: 'avatar',
31
- filesize: 1000 * 1000 * 5, // max size in bytes
32
- getSignedUrl: true, // get a s3 signed url after every `find()` operation (can be overridden per request)
33
- params: {}, // upload params, https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property
32
+ type: 'image',
33
+ // ...any imagePlugin option, excluding awsAccessKeyId and awsSecretAccessKey
34
34
  },
35
35
  logos: [{
36
- type: 'image'
36
+ type: 'image',
37
+ // ...any imagePlugin option, excluding awsAccessKeyId and awsSecretAccessKey
37
38
  }],
38
39
  }
39
40
  })
@@ -61,6 +62,6 @@ user.update({
61
62
 
62
63
  ### File types
63
64
 
64
- Due to known limitations, we are inaccurately able to validate non-binary file types (e.g. txt, svg) before uploading to S3, and rely on their file processing to remove any malicious files.
65
+ Due to known limitations, we are not able to verify the contents of non-binary files, only the filename extension (e.g. .txt, .svg) before uploading to S3
65
66
 
66
67
  ...to be continued
@@ -13,6 +13,8 @@ Monastery constructor, same as the [monk constructor](https://automattic.github.
13
13
  `uri` *(string\|array)*: A [mongo connection string URI](https://docs.mongodb.com/manual/reference/connection-string/). Replica sets can be an array or comma separated.
14
14
 
15
15
  [`options`] *(object)*:
16
+ - [`hideWarnings=false`] *(boolean)*: hide monastery warnings
17
+ - [`hideErrors=false`] *(boolean)*: hide monastery errors
16
18
  - [`defaultObjects=false`] *(boolean)*: when [inserting](../model/insert.html#defaults-example), undefined embedded documents and arrays are defined
17
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`.
18
20
  - [`timestamps=true`] *(boolean)*: whether to use [`createdAt` and `updatedAt`](../definition), this can be overridden per operation
package/docs/readme.md CHANGED
@@ -10,6 +10,8 @@
10
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:
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'] || {}) }
@@ -255,7 +255,11 @@ module.exports = {
255
255
  // Update
256
256
  let update = await this['_' + type](opts.query, operators, util.omit(opts, this._queryOptions))
257
257
  if (type == 'findOneAndUpdate') response = update
258
- else if (update.n) response = Object.assign(Object.create({ _output: update }), operators['$set']||{})
258
+ else if (update.n) response = Object.assign(
259
+ Object.create({ _output: update }),
260
+ operators['$set'] || {},
261
+ (update.upserted||[])[0] ? { _id: update.upserted[0]._id } : {},
262
+ )
259
263
 
260
264
  // Hook: afterUpdate (doesn't have access to validated data)
261
265
  if (response) await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)))
@@ -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.2",
5
+ "version": "1.37.1",
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",
@@ -18,13 +18,24 @@ let plugin = module.exports = {
18
18
  * @this plugin
19
19
  */
20
20
 
21
+ // Depreciation warnings
22
+ if (options.bucketDir) { // > 1.36.2
23
+ manager.warn('imagePlugin.bucketDir has been depreciated in favour of imagePlugin.path')
24
+ options.path = function (uid, basename, ext, file) { `${options.bucketDir}/${uid}.${ext}` }
25
+ }
26
+
21
27
  // Settings
28
+ this.awsAcl = options.awsAcl || 'public-read'
22
29
  this.awsBucket = options.awsBucket
23
30
  this.awsAccessKeyId = options.awsAccessKeyId
24
31
  this.awsSecretAccessKey = options.awsSecretAccessKey
25
- this.bucketDir = options.bucketDir || 'full'
32
+ this.bucketDir = options.bucketDir || 'full' // depreciated > 1.36.2
33
+ this.filesize = options.filesize
26
34
  this.formats = options.formats || ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'tiff']
35
+ this.getSignedUrl = options.getSignedUrl
27
36
  this.manager = manager
37
+ this.params = options.params ? util.deepCopy(options.params) : {},
38
+ this.path = options.path || function (uid, basename, ext, file) { return `full/${uid}.${ext}` }
28
39
 
29
40
  if (!options.awsBucket || !options.awsAccessKeyId || !options.awsSecretAccessKey) {
30
41
  manager.error('Monastery imagePlugin: awsBucket, awsAccessKeyId, or awsSecretAccessKey is not defined')
@@ -34,7 +45,7 @@ let plugin = module.exports = {
34
45
 
35
46
  // Create s3 'service' instance
36
47
  // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property
37
- manager.getSignedUrl = this._getSignedUrl
48
+ manager._getSignedUrl = this._getSignedUrl
38
49
  this.s3 = new S3({
39
50
  credentials: {
40
51
  accessKeyId: this.awsAccessKeyId,
@@ -114,9 +125,13 @@ let plugin = module.exports = {
114
125
  if (util.isArray(data)) {
115
126
  return Promise.reject('Adding images to mulitple data objects is not supported.')
116
127
 
128
+ // We currently don't support an array of data objects.
129
+ } else if (!util.isObject(data)) {
130
+ return Promise.reject('No creat e/ update data object passed to addImages?')
131
+
117
132
  // We require the update query OR data object to contain _id. This is because non-id queries
118
133
  // will not suffice when updating the document(s) against the same query again.
119
- } else if (!data._id && (!query || !query._id)) {
134
+ } else if (!(data||{})._id && (!query || !query._id)) {
120
135
  return Promise.reject('Adding images requires the update operation to query via _id\'s.')
121
136
  }
122
137
 
@@ -126,36 +141,36 @@ let plugin = module.exports = {
126
141
  return Promise.all(filesArr.map(file => {
127
142
  return new Promise((resolve, reject) => {
128
143
  let uid = nanoid.nanoid()
129
- let pathFilename = filesArr.imageField.filename ? '/' + filesArr.imageField.filename : ''
144
+ let path = filesArr.imageField.path || plugin.path
130
145
  let image = {
131
- bucket: plugin.awsBucket,
146
+ bucket: filesArr.imageField.awsBucket || plugin.awsBucket,
132
147
  date: plugin.manager.useMilliseconds? Date.now() : Math.floor(Date.now() / 1000),
133
148
  filename: file.name,
134
149
  filesize: file.size,
135
- path: `${plugin.bucketDir}/${uid}${pathFilename}.${file.ext}`,
150
+ path: path(uid, file.name, file.ext, file),
136
151
  // sizes: ['large', 'medium', 'small'],
137
152
  uid: uid,
138
153
  }
154
+ let s3Options = {
155
+ // ACL: Some IAM permissions "s3:PutObjectACL" must be included in the policy
156
+ // params: https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property
157
+ ACL: filesArr.imageField.awsAcl || plugin.awsAcl,
158
+ Body: file.data,
159
+ Bucket: image.bucket,
160
+ Key: image.path,
161
+ ...(filesArr.imageField.params || plugin.params),
162
+ }
139
163
  plugin.manager.info(
140
164
  `Uploading '${image.filename}' to '${image.bucket}/${image.path}'`
141
165
  )
142
166
  if (test) {
143
167
  plugin._addImageObjectsToData(filesArr.inputPath, data, image)
144
- resolve()
168
+ resolve(s3Options)
145
169
  } else {
146
- plugin.s3.upload({
147
- Bucket: plugin.awsBucket,
148
- Key: image.path,
149
- Body: file.data,
150
- // The IAM permission "s3:PutObjectACL" must be included in the appropriate policy
151
- ACL: 'public-read',
152
- // upload params,https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property
153
- ...filesArr.imageField.params,
154
-
155
- }, (err, response) => {
170
+ plugin.s3.upload(s3Options, (err, response) => {
156
171
  if (err) return reject(err)
157
172
  plugin._addImageObjectsToData(filesArr.inputPath, data, image)
158
- resolve()
173
+ resolve(s3Options)
159
174
  })
160
175
  }
161
176
  })
@@ -163,11 +178,11 @@ let plugin = module.exports = {
163
178
  }))
164
179
 
165
180
  // Save the data against the matching document(s)
166
- }).then(() => {
167
- // Remove update's _output object
181
+ }).then((s3Options) => {
168
182
  let prunedData = { ...data }
183
+ // Remove update's _output object
169
184
  delete prunedData._output
170
- if (test) return [prunedData]
185
+ if (test) return [prunedData, s3Options]
171
186
  return model._update(
172
187
  idquery,
173
188
  { '$set': prunedData },
@@ -196,10 +211,11 @@ let plugin = module.exports = {
196
211
  // Find all image objects in data
197
212
  for (let doc of util.toArray(data)) {
198
213
  for (let imageField of this.imageFields) {
199
- if (options.getSignedUrls || imageField.getSignedUrl) {
214
+ if (options.getSignedUrls
215
+ || (util.isDefined(imageField.getSignedUrl) ? imageField.getSignedUrl : plugin.getSignedUrl)) {
200
216
  let images = plugin._findImagesInData(doc, imageField, 0, '').filter(o => o.image)
201
217
  for (let image of images) {
202
- image.image.signedUrl = plugin._getSignedUrl(image.image.path)
218
+ image.image.signedUrl = plugin._getSignedUrl(image.image.path, 3600, imageField.awsBucket)
203
219
  }
204
220
  }
205
221
  }
@@ -346,7 +362,10 @@ let plugin = module.exports = {
346
362
  // the file doesnt get deleted, we only delete from plugin.awsBucket.
347
363
  if (!unused.length) return
348
364
  await new Promise((resolve, reject) => {
349
- plugin.s3.deleteObjects({ Bucket: plugin.awsBucket, Delete: { Objects: unused }}, (err, data) => {
365
+ plugin.s3.deleteObjects({
366
+ Bucket: plugin.awsBucket,
367
+ Delete: { Objects: unused }
368
+ }, (err, data) => {
350
369
  if (err) reject(err)
351
370
  resolve()
352
371
  })
@@ -410,7 +429,7 @@ let plugin = module.exports = {
410
429
  return Promise.all(filesArr.map((file, i) => {
411
430
  return new Promise((resolve, reject) => {
412
431
  fileType.fromBuffer(file.data).then(res => {
413
- let maxSize = filesArr.imageField.filesize
432
+ let filesize = filesArr.imageField.filesize || plugin.filesize
414
433
  let formats = filesArr.imageField.formats || plugin.formats
415
434
  let allowAny = util.inArray(formats, 'any')
416
435
  file.format = res? res.ext : ''
@@ -421,9 +440,9 @@ let plugin = module.exports = {
421
440
  title: filesArr.inputPath + (i? `.${i}` : ''),
422
441
  detail: `The file size for '${file.nameClipped}' is too big.`
423
442
  })
424
- else if (maxSize && maxSize < file.size) reject({ // file.size == bytes
443
+ else if (filesize && filesize < file.size) reject({ // file.size == bytes
425
444
  title: filesArr.inputPath + (i? `.${i}` : ''),
426
- detail: `The file size for '${file.nameClipped}' is bigger than ${(maxSize/1000/1000).toFixed(1)}MB.`
445
+ detail: `The file size for '${file.nameClipped}' is bigger than ${(filesize/1000/1000).toFixed(1)}MB.`
427
446
  })
428
447
  else if (file.ext == 'unknown') reject({
429
448
  title: filesArr.inputPath + (i? `.${i}` : ''),
@@ -446,8 +465,10 @@ let plugin = module.exports = {
446
465
  * @param {object|array} fields
447
466
  * @param {string} path
448
467
  * @return [{}, ...]
468
+ * @this plugin
449
469
  */
450
470
  let list = []
471
+ let that = this
451
472
  util.forEach(fields, (field, fieldName) => {
452
473
  let path2 = `${path}.${fieldName}`.replace(/^\./, '')
453
474
  // let schema = field.schema || {}
@@ -464,11 +485,28 @@ let plugin = module.exports = {
464
485
 
465
486
  // Image field. Test for field.image as field.type may be 'any'
466
487
  } else if (field.type == 'image' || field.image) {
467
- let formats = field.formats
468
- let filesize = field.filesize || field.fileSize // old <= v1.31.7
469
- let filename = field.filename
470
- let getSignedUrl = field.getSignedUrl
471
- let params = { ...field.params||{} }
488
+ if (field.fileSize) { // > v1.31.7
489
+ this.manager.warn(`${path2}.fileSize has been depreciated in favour of ${path2}.filesize`)
490
+ field.filesize = field.filesize || field.fileSize
491
+ }
492
+ if (field.filename) { // > v1.36.3
493
+ this.manager.warn(`${path2}.filename has been depreciated in favour of ${path2}.path()`)
494
+ field.path = field.path
495
+ || function(uid, basename, ext, file) { return `${that.bucketDir}/${uid}/${field.filename}.${ext}` }
496
+ }
497
+
498
+ list.push({
499
+ awsAcl: field.awsAcl,
500
+ awsBucket: field.awsBucket,
501
+ filename: field.filename,
502
+ filesize: field.filesize,
503
+ formats: field.formats,
504
+ fullPath: path2,
505
+ fullPathRegex: new RegExp('^' + path2.replace(/\.[0-9]+/g, '.[0-9]+').replace(/\./g, '\\.') + '$'),
506
+ getSignedUrl: field.getSignedUrl,
507
+ path: field.path,
508
+ params: field.params ? util.deepCopy(field.params) : undefined,
509
+ })
472
510
  // Convert image field to subdocument
473
511
  fields[fieldName] = {
474
512
  bucket: { type: 'string' },
@@ -479,15 +517,6 @@ let plugin = module.exports = {
479
517
  schema: { image: true, nullObject: true, isImageObject: true },
480
518
  uid: { type: 'string' },
481
519
  }
482
- list.push({
483
- fullPath: path2,
484
- fullPathRegex: new RegExp('^' + path2.replace(/\.[0-9]+/g, '.[0-9]+').replace(/\./g, '\\.') + '$'),
485
- formats: formats,
486
- filesize: filesize,
487
- filename: filename,
488
- getSignedUrl: getSignedUrl,
489
- params: params,
490
- })
491
520
  }
492
521
  })
493
522
  return list
@@ -537,12 +566,17 @@ let plugin = module.exports = {
537
566
  return list
538
567
  },
539
568
 
540
- _getSignedUrl: (path, expires=3600) => {
541
- // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
569
+ _getSignedUrl: (path, expires=3600, bucket) => {
570
+ /**
571
+ * @param {string} path - aws file path
572
+ * @param {number} <expires> - seconds
573
+ * @param {number} <bucket>
574
+ * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
575
+ */
542
576
  let signedUrl = plugin.s3.getSignedUrl('getObject', {
543
- Bucket: plugin.awsBucket,
577
+ Bucket: bucket || plugin.awsBucket,
578
+ Expires: expires,
544
579
  Key: path,
545
- Expires: expires
546
580
  })
547
581
  return signedUrl
548
582
  },
Binary file
package/test/crud.js CHANGED
@@ -201,6 +201,15 @@ module.exports = function(monastery, opendb) {
201
201
  name: 'Martin Luther3'
202
202
  }
203
203
  ])
204
+
205
+ // Upsert
206
+ let newId = db.id()
207
+ await expect(user.update({ query: newId, data: { name: 'Martin Luther3' }, upsert: true }))
208
+ .resolves.toEqual({ _id: newId, name: 'Martin Luther3' }) // inserted
209
+
210
+ await expect(user.update({ query: inserted._id, data: { name: 'Martin Luther4' }, upsert: true }))
211
+ .resolves.toEqual({ name: 'Martin Luther4' }) // updated
212
+
204
213
  db.close()
205
214
  })
206
215
 
@@ -309,6 +318,77 @@ module.exports = function(monastery, opendb) {
309
318
  db.close()
310
319
  })
311
320
 
321
+ test('update mixing formData', async() => {
322
+ // Mixing data
323
+ let db = (await opendb(null)).db
324
+ let consignment = db.model('consignment', {
325
+ fields: {
326
+ specialInstructions: [{
327
+ text: { type: 'string' },
328
+ createdAt: { type: 'date' },
329
+ updatedByName: { type: 'string' },
330
+ importance: { type: 'string' },
331
+ }],
332
+ },
333
+ })
334
+ let inserted = await consignment.insert({
335
+ data: {
336
+ specialInstructions: [{
337
+ text: 'filler',
338
+ createdAt: 1653601752472,
339
+ updatedByName: 'Paul',
340
+ importance: 'low'
341
+ }]
342
+ }
343
+ })
344
+ let specialInstructions = [
345
+ {
346
+ text: 'POD added by driver',
347
+ createdAt: 1653603212886,
348
+ updatedByName: 'Paul Driver 3',
349
+ importance: 'low'
350
+ }, {
351
+ text: 'filler',
352
+ createdAt: 1653601752472,
353
+ updatedByName: 'Paul',
354
+ importance: 'low'
355
+ }
356
+ ]
357
+ // Key order maintained (not always guaranteed in browsers)
358
+ await consignment.update({
359
+ query: inserted._id,
360
+ data: {
361
+ 'specialInstructions[0][text]': 'filler',
362
+ 'specialInstructions[0][createdAt]': 1653601752472,
363
+ 'specialInstructions[0][updatedByName]': 'Paul',
364
+ 'specialInstructions[0][importance]': 'low',
365
+ specialInstructions: specialInstructions.map(o => ({ ...o })),
366
+ }
367
+ })
368
+ await expect(consignment.findOne({ query: inserted._id })).resolves.toEqual({
369
+ _id: expect.any(Object),
370
+ specialInstructions: specialInstructions,
371
+ })
372
+
373
+ // Key order maintained (not always guaranteed in browsers)
374
+ await consignment.update({
375
+ query: inserted._id,
376
+ data: {
377
+ specialInstructions: specialInstructions.map(o => ({ ...o })),
378
+ 'specialInstructions[0][text]': 'filler',
379
+ 'specialInstructions[0][createdAt]': 1653601752472,
380
+ 'specialInstructions[0][updatedByName]': 'Paul',
381
+ 'specialInstructions[0][importance]': 'low',
382
+ }
383
+ })
384
+ await expect(consignment.findOne({ query: inserted._id })).resolves.toEqual({
385
+ _id: expect.any(Object),
386
+ specialInstructions: [specialInstructions[1], specialInstructions[1]],
387
+ })
388
+
389
+ db.close()
390
+ })
391
+
312
392
  test('insert with id casting', async () => {
313
393
  let db = (await opendb(null)).db
314
394
  db.model('company', { fields: {
package/test/model.js CHANGED
@@ -117,8 +117,7 @@ module.exports = function(monastery, opendb) {
117
117
 
118
118
  test('model reserved rules', async () => {
119
119
  // Setup
120
- let db = (await opendb(false, {})).db
121
- db.error = () => {} // hiding debug error
120
+ let db = (await opendb(false, { hideErrors: true })).db // hide debug error
122
121
  let user = db.model('user', {
123
122
  fields: {
124
123
  name: {