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 +8 -0
- package/docs/image-plugin.md +6 -4
- package/docs/model/find.md +1 -0
- package/lib/model-crud.js +2 -1
- package/lib/model-validate.js +3 -3
- package/lib/model.js +4 -1
- package/package.json +1 -1
- package/plugins/images/index.js +53 -10
- package/test/crud.js +34 -0
- package/test/model.js +23 -0
- package/test/plugin-images.js +55 -0
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
|
|
package/docs/image-plugin.md
CHANGED
|
@@ -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'],
|
|
31
|
-
|
|
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
|
-
|
package/docs/model/find.md
CHANGED
|
@@ -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 ||
|
|
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)) {
|
package/lib/model-validate.js
CHANGED
|
@@ -290,9 +290,9 @@ module.exports = {
|
|
|
290
290
|
},
|
|
291
291
|
|
|
292
292
|
_ignoredRules: [ // todo: change name? i.e. 'specialFields'
|
|
293
|
-
// Need to remove
|
|
294
|
-
'default', 'defaultOverride', '
|
|
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}"
|
|
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.
|
|
5
|
+
"version": "1.32.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:boycce/monastery",
|
|
8
8
|
"homepage": "https://boycce.github.io/monastery/",
|
package/plugins/images/index.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
package/test/plugin-images.js
CHANGED
|
@@ -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
|
}
|