monastery 1.31.7 → 1.32.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.
package/changelog.md CHANGED
@@ -2,6 +2,28 @@
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.32.2](https://github.com/boycce/monastery/compare/1.32.1...1.32.2) (2022-03-04)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * missing original non-validated this.data in beforeUpdate ([4b4002d](https://github.com/boycce/monastery/commit/4b4002d3d4d2609025cbb22cacecf948252be38b))
11
+
12
+ ### [1.32.1](https://github.com/boycce/monastery/compare/1.32.0...1.32.1) (2022-03-01)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * processAfterFind bug ([3183b79](https://github.com/boycce/monastery/commit/3183b79fc288665000b63e0221fbe8acf6f482aa))
18
+
19
+ ## [1.32.0](https://github.com/boycce/monastery/compare/1.31.7...1.32.0) (2022-02-28)
20
+
21
+
22
+ ### Features
23
+
24
+ * added getSignedUrl(s) ([3552a4d](https://github.com/boycce/monastery/commit/3552a4d0b21c192a256a590e3ac1cb48b31c6564))
25
+ * added image optiosn filename, and params ([353b2f0](https://github.com/boycce/monastery/commit/353b2f09ed429a5cd8d74a3b2e94493650fb52e4))
26
+
5
27
  ### [1.31.7](https://github.com/boycce/monastery/compare/1.31.6...1.31.7) (2022-02-28)
6
28
 
7
29
 
@@ -14,6 +36,7 @@ All notable changes to this project will be documented in this file. See [standa
14
36
 
15
37
  ### Bug Fixes
16
38
 
39
+ * Fixed validateUndefined ([58daed1](https://github.com/boycce/monastery/commit/58daed1ca5317c061a4ddde280bf45b0a134ab30))
17
40
  * added partial unqiue index tests ([ff6f193](https://github.com/boycce/monastery/commit/ff6f1938e333407ee17895873d2b42fa5263d7e3))
18
41
  * scripts ([bc32680](https://github.com/boycce/monastery/commit/bc326809098ae24686158a0386fbbd6671d86c98))
19
42
 
@@ -26,9 +26,12 @@ Then in your model schema, e.g.
26
26
  ```js
27
27
  let user = db.model('user', { fields: {
28
28
  logo: {
29
- type: 'image',
30
- formats: ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'tiff'], // optional plugin rule
31
- fileSize: 1000 * 1000 * 5 // optional plugin rule, size in bytes
29
+ type: 'image', // required
30
+ formats: ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'tiff'],
31
+ filename: 'avatar',
32
+ filesize: 1000 * 1000 * 5, // max size in bytes
33
+ getSignedUrl: true, // get a s3 signed url after every `find()` operation (can be overridden per request)
34
+ params: {}, // upload params, https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property
32
35
  },
33
36
  logos: [{
34
37
  type: 'image'
@@ -61,4 +64,3 @@ user.update({
61
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.
62
65
 
63
66
  ...to be continued
64
-
@@ -15,6 +15,7 @@ Find document(s) in a collection and call related hook: `schema.afterFind`
15
15
  - [[`options.populate`](#populate)] *(array)*
16
16
  - [`options.sort`] *(string\|array\|object)*: same as the mongodb option, but allows string parsing e.g. 'name', 'name:1'
17
17
  - [`options.blacklist`] *(array\|string\|false)*: augment `schema.findBL`. `false` will remove all blacklisting
18
+ - [`options.getSignedUrls`] *(boolean)*: get signed urls for all image objects
18
19
  - [`options.project`] *(string\|array\|object)*: return only these fields, ignores blacklisting
19
20
  - [[`any mongodb option`](http://mongodb.github.io/node-mongodb-native/3.2/api/Collection.html#find)] *(any)*
20
21
 
package/lib/model-crud.js CHANGED
@@ -28,11 +28,11 @@ module.exports = {
28
28
  ])
29
29
 
30
30
  // Validate
31
- opts.data = await this.validate(opts.data||{}, { ...opts })
31
+ let data = await this.validate(opts.data||{}, { ...opts })
32
32
 
33
33
  // Insert
34
- await util.runSeries(this.beforeInsert.map(f => f.bind(opts, opts.data)))
35
- let response = await this._insert(opts.data, options)
34
+ await util.runSeries(this.beforeInsert.map(f => f.bind(opts, data)))
35
+ let response = await this._insert(data, options)
36
36
  await util.runSeries(this.afterInsert.map(f => f.bind(opts, response)))
37
37
 
38
38
  // Success/error
@@ -193,31 +193,32 @@ module.exports = {
193
193
  }
194
194
  try {
195
195
  opts = await this._queryObject(opts, 'update')
196
+ let data = opts.data
196
197
  let response = null
197
198
  let operators = util.pluck(opts, [/^\$/])
198
199
  let options = util.omit(opts, ['data', 'query', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'])
199
200
 
200
201
  // Validate
201
- if (util.isDefined(opts.data)) opts.data = await this.validate(opts.data, { ...opts })
202
- if (!util.isDefined(opts.data) && util.isEmpty(operators)) {
202
+ if (util.isDefined(data)) data = await this.validate(opts.data, { ...opts })
203
+ if (!util.isDefined(data) && util.isEmpty(operators)) {
203
204
  throw new Error(`Please pass an update operator to ${this.name}.update(), e.g. data, $unset, etc`)
204
205
  }
205
- if (util.isDefined(opts.data) && (!opts.data || util.isEmpty(opts.data))) {
206
+ if (util.isDefined(data) && (!data || util.isEmpty(data))) {
206
207
  throw new Error(`No valid data passed to ${this.name}.update({ data: .. })`)
207
208
  }
208
- // Hook: beforeUpdate
209
- await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, opts.data||{})))
210
- if (opts.data && operators['$set']) {
209
+ // Hook: beforeUpdate (has access to original, non-validated opts.data)
210
+ await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, data||{})))
211
+ if (data && operators['$set']) {
211
212
  this.warn(`'$set' fields take precedence over the data fields for \`${this.name}.update()\``)
212
213
  }
213
- if (opts.data || operators['$set']) {
214
- operators['$set'] = { ...opts.data, ...(operators['$set'] || {}) }
214
+ if (data || operators['$set']) {
215
+ operators['$set'] = { ...data, ...(operators['$set'] || {}) }
215
216
  }
216
217
  // Update
217
218
  let update = await this._update(opts.query, operators, options)
218
219
  if (update.n) response = Object.assign(Object.create({ _output: update }), operators['$set']||{})
219
220
 
220
- // Hook: afterUpdate
221
+ // Hook: afterUpdate (doesn't have access to validated data)
221
222
  if (update.n) await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)))
222
223
 
223
224
  // Success
@@ -238,6 +239,7 @@ module.exports = {
238
239
  * @param {object} opts
239
240
  * @param {object} <opts.query> - mongodb query object
240
241
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
242
+ * @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
241
243
  * @param {any} <opts.any> - any mongodb option
242
244
  * @param {function} <cb> - execute cb(err, data) instead of responding
243
245
  * @this model
@@ -304,7 +306,7 @@ module.exports = {
304
306
  opts.query = util.removeUndefined(opts.query)
305
307
 
306
308
  // Query options
307
- opts.limit = opts.one? 1 : parseInt(opts.limit || (type == 'remove' ? 1 : this.manager.limit || 0))
309
+ opts.limit = opts.one? 1 : parseInt(opts.limit || this.manager.limit || 0)
308
310
  opts.skip = Math.max(0, opts.skip || 0)
309
311
  opts.sort = opts.sort || { 'createdAt': -1 }
310
312
  if (util.isString(opts.sort)) {
@@ -324,7 +326,7 @@ module.exports = {
324
326
  return opts
325
327
  },
326
328
 
327
- _processAfterFind: function(data, projection, afterFindContext) {
329
+ _processAfterFind: function(data, projection={}, afterFindContext={}) {
328
330
  /**
329
331
  * Todo: Maybe make this method public?
330
332
  * Recurses through fields that are models and populates missing default-fields and calls model.afterFind([fn,..])
@@ -290,9 +290,9 @@ module.exports = {
290
290
  },
291
291
 
292
292
  _ignoredRules: [ // todo: change name? i.e. 'specialFields'
293
- // Need to remove fileSize and formats..
294
- 'default', 'defaultOverride', 'fileSize', 'formats', 'image', 'index', 'insertOnly', 'model',
295
- 'nullObject', 'timestampField', 'type', 'virtual'
293
+ // Need to remove filesize and formats..
294
+ 'default', 'defaultOverride', 'filename', 'filesize', 'formats', 'image', 'index', 'insertOnly',
295
+ 'model', 'nullObject', 'params', 'getSignedUrl', 'timestampField', 'type', 'virtual'
296
296
  ]
297
297
 
298
298
  }
package/lib/model.js CHANGED
@@ -188,9 +188,12 @@ Model.prototype._setupFields = function(fields) {
188
188
 
189
189
  // Rule doesn't exist
190
190
  util.forEach(field, (rule, ruleName) => {
191
+ if ((this.rules[ruleName] || rules[ruleName]) && this._ignoredRules.indexOf(ruleName) != -1) {
192
+ this.error(`The rule name "${ruleName}" for the model "${this.name}" is a reserved keyword, ignoring rule.`)
193
+ }
191
194
  if (!this.rules[ruleName] && !rules[ruleName] && this._ignoredRules.indexOf(ruleName) == -1) {
192
195
  // console.log(field)
193
- this.error(`No rule "${ruleName}" exists for model "${this.name}". Ignoring rule.`)
196
+ this.error(`No rule "${ruleName}" exists for model "${this.name}", ignoring rule.`)
194
197
  delete field[ruleName]
195
198
  }
196
199
  }, this)
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.31.7",
5
+ "version": "1.32.2",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -32,7 +32,9 @@ let plugin = module.exports = {
32
32
  return
33
33
  }
34
34
 
35
- // Create s3 service instance
35
+ // Create s3 'service' instance
36
+ // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property
37
+ manager.getSignedUrl = this._getSignedUrl
36
38
  this.s3 = new S3({
37
39
  credentials: {
38
40
  accessKeyId: this.awsAccessKeyId,
@@ -71,6 +73,9 @@ let plugin = module.exports = {
71
73
  model.afterInsert.push(function(data, n) {
72
74
  plugin.addImages(this, data).then(() => n(null, data)).catch(e => n(e))
73
75
  })
76
+ model.afterFind.push(function(data, n) {
77
+ plugin.getSignedUrls.call(model, this, data).then(() => n(null, data)).catch(e => n(e))
78
+ })
74
79
  }
75
80
  },
76
81
 
@@ -121,16 +126,17 @@ let plugin = module.exports = {
121
126
  return Promise.all(filesArr.map(file => {
122
127
  return new Promise((resolve, reject) => {
123
128
  let uid = nanoid.nanoid()
129
+ let pathFilename = filesArr.imageField.filename ? '/' + filesArr.imageField.filename : ''
124
130
  let image = {
125
- bucket: this.awsBucket,
126
- date: this.manager.useMilliseconds? Date.now() : Math.floor(Date.now() / 1000),
131
+ bucket: plugin.awsBucket,
132
+ date: plugin.manager.useMilliseconds? Date.now() : Math.floor(Date.now() / 1000),
127
133
  filename: file.name,
128
134
  filesize: file.size,
129
- path: `${plugin.bucketDir}/${uid}.${file.ext}`,
135
+ path: `${plugin.bucketDir}/${uid}${pathFilename}.${file.ext}`,
130
136
  // sizes: ['large', 'medium', 'small'],
131
- uid: uid
137
+ uid: uid,
132
138
  }
133
- this.manager.info(
139
+ plugin.manager.info(
134
140
  `Uploading '${image.filename}' to '${image.bucket}/${image.path}'`
135
141
  )
136
142
  if (test) {
@@ -138,10 +144,14 @@ let plugin = module.exports = {
138
144
  resolve()
139
145
  } else {
140
146
  plugin.s3.upload({
141
- Bucket: this.awsBucket,
147
+ Bucket: plugin.awsBucket,
142
148
  Key: image.path,
143
149
  Body: file.data,
144
- ACL: 'public-read'
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
+
145
155
  }, (err, response) => {
146
156
  if (err) return reject(err)
147
157
  plugin._addImageObjectsToData(filesArr.inputPath, data, image)
@@ -161,7 +171,7 @@ let plugin = module.exports = {
161
171
  return model._update(
162
172
  idquery,
163
173
  { '$set': prunedData },
164
- { 'multi': options.multi || options.create }
174
+ { 'multi': options.multi || options.create },
165
175
  )
166
176
 
167
177
  // If errors, remove inserted documents to prevent double ups when the user resaves.
@@ -172,6 +182,30 @@ let plugin = module.exports = {
172
182
  })
173
183
  },
174
184
 
185
+ getSignedUrls: async function(options, data) {
186
+ /**
187
+ * Get signed urls for all image objects in data
188
+ * @param {object} options - monastery operation options {model, query, files, ..}
189
+ * @param {object} data
190
+ * @return promise(data)
191
+ * @this model
192
+ */
193
+ // Not wanting signed urls for this operation?
194
+ if (util.isDefined(options.getSignedUrls) && !options.getSignedUrls) return
195
+
196
+ // Find all image objects in data
197
+ for (let doc of util.toArray(data)) {
198
+ for (let imageField of this.imageFields) {
199
+ if (options.getSignedUrls || imageField.getSignedUrl) {
200
+ let images = plugin._findImagesInData(doc, imageField, 0, '').filter(o => o.image)
201
+ for (let image of images) {
202
+ image.image.signedUrl = plugin._getSignedUrl(image.image.path)
203
+ }
204
+ }
205
+ }
206
+ }
207
+ },
208
+
175
209
  keepImagePlacement: async function(options, data) {
176
210
  /**
177
211
  * Hook before update/remove
@@ -303,7 +337,7 @@ let plugin = module.exports = {
303
337
  { Key: `medium/${key}.jpg` },
304
338
  { Key: `large/${key}.jpg` }
305
339
  )
306
- this.manager.info(
340
+ plugin.manager.info(
307
341
  `Removing '${pre.image.filename}' from '${pre.image.bucket}/${pre.image.path}'`
308
342
  )
309
343
  }
@@ -376,7 +410,7 @@ let plugin = module.exports = {
376
410
  return Promise.all(filesArr.map((file, i) => {
377
411
  return new Promise((resolve, reject) => {
378
412
  fileType.fromBuffer(file.data).then(res => {
379
- let maxSize = filesArr.imageField.fileSize
413
+ let maxSize = filesArr.imageField.filesize
380
414
  let formats = filesArr.imageField.formats || plugin.formats
381
415
  let allowAny = util.inArray(formats, 'any')
382
416
  file.format = res? res.ext : ''
@@ -421,17 +455,20 @@ let plugin = module.exports = {
421
455
  // Subdocument field
422
456
  if (util.isSubdocument(field)) {//schema.isObject
423
457
  // log(`Recurse 1: ${path2}`)
424
- list = list.concat(this._findAndTransformImageFields(field, path2))
458
+ list = list.concat(plugin._findAndTransformImageFields(field, path2))
425
459
 
426
460
  // Array field
427
461
  } else if (util.isArray(field)) {//schema.isArray
428
462
  // log(`Recurse 2: ${path2}`)
429
- list = list.concat(this._findAndTransformImageFields(field, path2))
463
+ list = list.concat(plugin._findAndTransformImageFields(field, path2))
430
464
 
431
465
  // Image field. Test for field.image as field.type may be 'any'
432
466
  } else if (field.type == 'image' || field.image) {
433
467
  let formats = field.formats
434
- let fileSize = field.fileSize
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||{} }
435
472
  // Convert image field to subdocument
436
473
  fields[fieldName] = {
437
474
  bucket: { type: 'string' },
@@ -440,13 +477,16 @@ let plugin = module.exports = {
440
477
  filesize: { type: 'number' },
441
478
  path: { type: 'string' },
442
479
  schema: { image: true, nullObject: true, isImageObject: true },
443
- uid: { type: 'string' }
480
+ uid: { type: 'string' },
444
481
  }
445
482
  list.push({
446
483
  fullPath: path2,
447
484
  fullPathRegex: new RegExp('^' + path2.replace(/\.[0-9]+/g, '.[0-9]+').replace(/\./g, '\\.') + '$'),
448
485
  formats: formats,
449
- fileSize: fileSize
486
+ filesize: filesize,
487
+ filename: filename,
488
+ getSignedUrl: getSignedUrl,
489
+ params: params,
450
490
  })
451
491
  }
452
492
  })
@@ -474,7 +514,7 @@ let plugin = module.exports = {
474
514
  if (`${dataPath}.${m}`.match(imageField.fullPathRegex)) {
475
515
  list.push({ imageField: imageField, dataPath: `${dataPath}.${m}`, image: target[m] })
476
516
  } else {
477
- list.push(...this._findImagesInData(
517
+ list.push(...plugin._findImagesInData(
478
518
  target[m],
479
519
  imageField,
480
520
  imageFieldChunkIndex+i+1,
@@ -495,6 +535,16 @@ let plugin = module.exports = {
495
535
  }
496
536
 
497
537
  return list
498
- }
538
+ },
539
+
540
+ _getSignedUrl: (path, expires=3600) => {
541
+ // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
542
+ let signedUrl = plugin.s3.getSignedUrl('getObject', {
543
+ Bucket: plugin.awsBucket,
544
+ Key: path,
545
+ Expires: expires
546
+ })
547
+ return signedUrl
548
+ },
499
549
 
500
550
  }
package/test/crud.js CHANGED
@@ -415,6 +415,40 @@ module.exports = function(monastery, opendb) {
415
415
  db.close()
416
416
  })
417
417
 
418
+ test('remove basics', async () => {
419
+ let db = (await opendb(null)).db
420
+ let user = db.model('user', {
421
+ fields: {
422
+ name: { type: 'string' },
423
+ },
424
+ })
425
+
426
+ // Insert multiple
427
+ let inserted2 = await user.insert({ data: [{ name: 'Martin' }, { name: 'Martin' }, { name: 'Martin' }]})
428
+ expect(inserted2).toEqual([
429
+ {
430
+ _id: expect.any(Object),
431
+ name: 'Martin'
432
+ }, {
433
+ _id: expect.any(Object),
434
+ name: 'Martin'
435
+ }, {
436
+ _id: expect.any(Object),
437
+ name: 'Martin'
438
+ }
439
+ ])
440
+
441
+ // Remove one
442
+ await expect(user.remove({ query: { name: 'Martin' }, multi: false }))
443
+ .resolves.toMatchObject({ deletedCount: 1, result: { n: 1, ok: 1 }})
444
+
445
+ // Remove many (default)
446
+ await expect(user.remove({ query: { name: 'Martin' } }))
447
+ .resolves.toMatchObject({ deletedCount: 2, result: { n: 2, ok: 1 }})
448
+
449
+ db.close()
450
+ })
451
+
418
452
  test('hooks', async () => {
419
453
  let db = (await opendb(null)).db
420
454
  let user = db.model('user', {
@@ -482,6 +516,26 @@ module.exports = function(monastery, opendb) {
482
516
  last: 'Luther'
483
517
  })
484
518
 
519
+ // beforeUpdate/beforeInsert should have access to the original non-validated data
520
+ let user2 = db.model('user2', {
521
+ fields: {
522
+ first: { type: 'string' },
523
+ },
524
+ beforeInsert: [function (data, next) {
525
+ if (this.data.bad === true && !data.bad) next(new Error('error1'))
526
+ else next()
527
+ }],
528
+ beforeUpdate: [function (data, next) {
529
+ if (this.data.bad === true && !data.bad) next(new Error('error2'))
530
+ else next()
531
+ }],
532
+ })
533
+ let userDoc2 = await user2._insert({ first: 'M' })
534
+ await expect(user2.insert({ data: { first: 'M' } })).resolves.toMatchObject({ first: 'M' })
535
+ await expect(user2.insert({ data: { first: 'M', bad: true } })).rejects.toThrow('error1')
536
+ await expect(user2.update({ query: userDoc2._id, data: { first: 'M', } })).resolves.toEqual({ first: 'M' })
537
+ await expect(user2.update({ query: userDoc2._id, data: { first: 'M', bad: true } })).rejects.toThrow('error2')
538
+
485
539
  db.close()
486
540
  })
487
541
 
package/test/model.js CHANGED
@@ -115,6 +115,29 @@ module.exports = function(monastery, opendb) {
115
115
  })
116
116
  })
117
117
 
118
+ test('model reserved rules', async () => {
119
+ // Setup
120
+ let db = (await opendb(false, {})).db
121
+ db.error = () => {} // hiding debug error
122
+ let user = db.model('user', {
123
+ fields: {
124
+ name: {
125
+ type: 'string',
126
+ params: {}, // reserved keyword (image plugin)
127
+ paramsUnreserved: {}
128
+ },
129
+ },
130
+ rules: {
131
+ params: (value) => {
132
+ return false // shouldn'r run
133
+ }
134
+ }
135
+ })
136
+ await expect(user.validate({ name: 'Martin' })).resolves.toMatchObject({
137
+ name: 'Martin',
138
+ })
139
+ })
140
+
118
141
  test('model indexes', async () => {
119
142
  // Setup: Need to test different types of indexes
120
143
  let db = (await opendb(null)).db
@@ -707,4 +707,57 @@ module.exports = function(monastery, opendb) {
707
707
  db.close()
708
708
  })
709
709
 
710
+ test('images getSignedUrls', async () => {
711
+ // latest (2022.02)
712
+ let db = (await opendb(null, {
713
+ timestamps: false,
714
+ serverSelectionTimeoutMS: 2000,
715
+ imagePlugin: { awsBucket: 'fake', awsAccessKeyId: 'fake', awsSecretAccessKey: 'fake' }
716
+ })).db
717
+
718
+ db.model('user', { fields: {
719
+ photos: [{ type: 'image' }],
720
+ photos2: [{ type: 'image', getSignedUrl: true }],
721
+ }})
722
+
723
+ let image = {
724
+ bucket: 'test',
725
+ date: 1234,
726
+ filename: 'lion1.png',
727
+ filesize: 1234,
728
+ path: 'test/lion1.png',
729
+ uid: 'lion1'
730
+ }
731
+
732
+ let userInserted = await db.user._insert({
733
+ photos: [image, image],
734
+ photos2: [image, image],
735
+ })
736
+
737
+ // Find signed URL via query option
738
+ let imageWithSignedUrl = { ...image, signedUrl: expect.stringMatching(/^https/) }
739
+ await expect(db.user.findOne({ query: userInserted._id, getSignedUrls: true })).resolves.toEqual({
740
+ _id: expect.any(Object),
741
+ photos: [imageWithSignedUrl, imageWithSignedUrl],
742
+ photos2: [imageWithSignedUrl, imageWithSignedUrl],
743
+ })
744
+
745
+ // Find signed URL via schema option
746
+ await expect(db.user.findOne({ query: userInserted._id })).resolves.toEqual({
747
+ _id: expect.any(Object),
748
+ photos: [image, image],
749
+ photos2: [imageWithSignedUrl, imageWithSignedUrl],
750
+ })
751
+
752
+ // Works with _processAfterFind
753
+ let rawUser = await db.user._findOne({ _id: userInserted._id })
754
+ await expect(db.user._processAfterFind(rawUser)).resolves.toEqual({
755
+ _id: expect.any(Object),
756
+ photos: [image, image],
757
+ photos2: [imageWithSignedUrl, imageWithSignedUrl],
758
+ })
759
+
760
+ db.close()
761
+ })
762
+
710
763
  }