monastery 1.31.7 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/changelog.md CHANGED
@@ -2,6 +2,14 @@
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.0](https://github.com/boycce/monastery/compare/1.31.7...1.32.0) (2022-02-28)
6
+
7
+
8
+ ### Features
9
+
10
+ * added getSignedUrl(s) ([3552a4d](https://github.com/boycce/monastery/commit/3552a4d0b21c192a256a590e3ac1cb48b31c6564))
11
+ * added image optiosn filename, and params ([353b2f0](https://github.com/boycce/monastery/commit/353b2f09ed429a5cd8d74a3b2e94493650fb52e4))
12
+
5
13
  ### [1.31.7](https://github.com/boycce/monastery/compare/1.31.6...1.31.7) (2022-02-28)
6
14
 
7
15
 
@@ -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 by default after `find()`
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
@@ -238,6 +238,7 @@ module.exports = {
238
238
  * @param {object} opts
239
239
  * @param {object} <opts.query> - mongodb query object
240
240
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
241
+ * @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
241
242
  * @param {any} <opts.any> - any mongodb option
242
243
  * @param {function} <cb> - execute cb(err, data) instead of responding
243
244
  * @this model
@@ -304,7 +305,7 @@ module.exports = {
304
305
  opts.query = util.removeUndefined(opts.query)
305
306
 
306
307
  // Query options
307
- opts.limit = opts.one? 1 : parseInt(opts.limit || (type == 'remove' ? 1 : this.manager.limit || 0))
308
+ opts.limit = opts.one? 1 : parseInt(opts.limit || this.manager.limit || 0)
308
309
  opts.skip = Math.max(0, opts.skip || 0)
309
310
  opts.sort = opts.sort || { 'createdAt': -1 }
310
311
  if (util.isString(opts.sort)) {
@@ -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.0",
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(this, data).then(() => n(null, data)).catch(e => n(e))
78
+ })
74
79
  }
75
80
  },
76
81
 
@@ -121,14 +126,15 @@ 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
131
  bucket: this.awsBucket,
126
132
  date: this.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
139
  this.manager.info(
134
140
  `Uploading '${image.filename}' to '${image.bucket}/${image.path}'`
@@ -141,7 +147,11 @@ let plugin = module.exports = {
141
147
  Bucket: this.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,23 @@ let plugin = module.exports = {
172
182
  })
173
183
  },
174
184
 
185
+ getSignedUrls: async function(options, data) {
186
+ // Not wanting signed urls for this operation?
187
+ if (util.isDefined(options.getSignedUrls) && !options.getSignedUrls) return
188
+
189
+ // Find all image objects in data
190
+ for (let doc of util.toArray(data)) {
191
+ for (let imageField of options.model.imageFields) {
192
+ if (options.getSignedUrls || imageField.getSignedUrl) {
193
+ let images = plugin._findImagesInData(doc, imageField, 0, '').filter(o => o.image)
194
+ for (let image of images) {
195
+ image.image.signedUrl = this._getSignedUrl(image.image.path)
196
+ }
197
+ }
198
+ }
199
+ }
200
+ },
201
+
175
202
  keepImagePlacement: async function(options, data) {
176
203
  /**
177
204
  * Hook before update/remove
@@ -376,7 +403,7 @@ let plugin = module.exports = {
376
403
  return Promise.all(filesArr.map((file, i) => {
377
404
  return new Promise((resolve, reject) => {
378
405
  fileType.fromBuffer(file.data).then(res => {
379
- let maxSize = filesArr.imageField.fileSize
406
+ let maxSize = filesArr.imageField.filesize
380
407
  let formats = filesArr.imageField.formats || plugin.formats
381
408
  let allowAny = util.inArray(formats, 'any')
382
409
  file.format = res? res.ext : ''
@@ -431,7 +458,10 @@ let plugin = module.exports = {
431
458
  // Image field. Test for field.image as field.type may be 'any'
432
459
  } else if (field.type == 'image' || field.image) {
433
460
  let formats = field.formats
434
- let fileSize = field.fileSize
461
+ let filesize = field.filesize || field.fileSize // old <= v1.31.7
462
+ let filename = field.filename
463
+ let getSignedUrl = field.getSignedUrl
464
+ let params = { ...field.params||{} }
435
465
  // Convert image field to subdocument
436
466
  fields[fieldName] = {
437
467
  bucket: { type: 'string' },
@@ -440,13 +470,16 @@ let plugin = module.exports = {
440
470
  filesize: { type: 'number' },
441
471
  path: { type: 'string' },
442
472
  schema: { image: true, nullObject: true, isImageObject: true },
443
- uid: { type: 'string' }
473
+ uid: { type: 'string' },
444
474
  }
445
475
  list.push({
446
476
  fullPath: path2,
447
477
  fullPathRegex: new RegExp('^' + path2.replace(/\.[0-9]+/g, '.[0-9]+').replace(/\./g, '\\.') + '$'),
448
478
  formats: formats,
449
- fileSize: fileSize
479
+ filesize: filesize,
480
+ filename: filename,
481
+ getSignedUrl: getSignedUrl,
482
+ params: params,
450
483
  })
451
484
  }
452
485
  })
@@ -495,6 +528,16 @@ let plugin = module.exports = {
495
528
  }
496
529
 
497
530
  return list
498
- }
531
+ },
532
+
533
+ _getSignedUrl: (path, expires=3600) => {
534
+ // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
535
+ let signedUrl = plugin.s3.getSignedUrl('getObject', {
536
+ Bucket: plugin.awsBucket,
537
+ Key: path,
538
+ Expires: expires
539
+ })
540
+ return signedUrl
541
+ },
499
542
 
500
543
  }
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', {
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,59 @@ 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
738
+ await expect(db.user.findOne({ query: userInserted._id, getSignedUrls: true })).resolves.toEqual({
739
+ _id: expect.any(Object),
740
+ photos: [
741
+ { ...image, signedUrl: expect.stringMatching(/^https/) },
742
+ { ...image, signedUrl: expect.stringMatching(/^https/) },
743
+ ],
744
+ photos2: [
745
+ { ...image, signedUrl: expect.stringMatching(/^https/) },
746
+ { ...image, signedUrl: expect.stringMatching(/^https/) },
747
+ ]
748
+ })
749
+ // Find signed URL
750
+ await expect(db.user.findOne({ query: userInserted._id })).resolves.toEqual({
751
+ _id: expect.any(Object),
752
+ photos: [
753
+ { ...image },
754
+ { ...image },
755
+ ],
756
+ photos2: [
757
+ { ...image, signedUrl: expect.stringMatching(/^https/) },
758
+ { ...image, signedUrl: expect.stringMatching(/^https/) },
759
+ ]
760
+ })
761
+
762
+ db.close()
763
+ })
764
+
710
765
  }