monastery 1.28.5 → 1.30.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.
@@ -54,8 +54,11 @@ let plugin = module.exports = {
54
54
  model.imageFields = plugin._findAndTransformImageFields(model.fields, '')
55
55
 
56
56
  if (model.imageFields.length) {
57
- // Update image fields and whitelists with the new object schema
58
- // model._setupFieldsAndWhitelists(model.fields)
57
+ // Todo?: Update image fields and whitelists with the new object schema
58
+ // model._setupFieldsAndWhitelists(model.fields)
59
+ model.beforeValidate.push(function(data, n) {
60
+ plugin.keepImagePlacement(this, data).then(() => n(null, data)).catch(e => n(e))
61
+ })
59
62
  model.beforeUpdate.push(function(data, n) {
60
63
  plugin.removeImages(this, data).then(() => n(null, data)).catch(e => n(e))
61
64
  })
@@ -91,7 +94,9 @@ let plugin = module.exports = {
91
94
  * @param {object} options - monastery operation options {model, query, files, ..}
92
95
  * @param {object} data -
93
96
  * @param {boolean} test -
94
- * @return promise
97
+ * @return promise(
98
+ * {object} data - data object containing new S3 image-object
99
+ * ])
95
100
  * @this plugin
96
101
  */
97
102
  let { model, query, files } = options
@@ -167,7 +172,33 @@ let plugin = module.exports = {
167
172
  })
168
173
  },
169
174
 
170
- removeImages: function(options, data, test) {
175
+ keepImagePlacement: async function(options, data) {
176
+ /**
177
+ * Hook before update/remove
178
+ * Since monastery removes undefined array items on validate, we need to convert any
179
+ * undefined array items to null where files are located to maintain image ordering
180
+ * Todo: maybe dont remove undefined array items in general
181
+ *
182
+ * E.g.
183
+ * req.body = 'photos[0]' : undefined || non existing (set to null)
184
+ * req.files = 'photos[0]' : { ...binary }
185
+ *
186
+ * @param {object} options - monastery operation options {query, model, files, multi, ..}
187
+ * @return promise
188
+ * @this plugin
189
+ */
190
+ if (typeof options.files == 'undefined') return
191
+ // Check upload errors and find valid uploaded images
192
+ let files = await plugin._findValidImages(options.files || {}, options.model)
193
+ // Set undefined primative-array items to null where files are located
194
+ for (let filesArray of files) {
195
+ if (filesArray.inputPath.match(/\.[0-9]+$/)) {
196
+ util.setDeepValue(data, filesArray.inputPath, null, true, false, true)
197
+ }
198
+ }
199
+ },
200
+
201
+ removeImages: async function(options, data, test) {
171
202
  /**
172
203
  * Hook before update/remove
173
204
  * Removes images not found in data, this means you will need to pass the image objects to every update operation
@@ -179,108 +210,113 @@ let plugin = module.exports = {
179
210
  *
180
211
  * @ref https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#deleteObjects-property
181
212
  * @param {object} options - monastery operation options {query, model, files, multi, ..}
182
- * @return promise
213
+ * @return promise([
214
+ * {object} useCount - images that wont be removed, e.g. { lion1: 1 }
215
+ * {array} unused - S3 image uris to be removed, e.g. [{ Key: 'small/lion1.jpg' }, ..]
216
+ * ])
183
217
  * @this plugin
184
218
  */
185
219
  let pre
186
220
  let preExistingImages = []
187
221
  let useCount = {}
188
- if (typeof options.files == 'undefined') return new Promise(res => res())
222
+ if (typeof options.files == 'undefined') return
189
223
 
190
224
  // Find all documents from the same query
191
- return options.model._find(options.query, options)
192
- .then(docs => {
193
- // Find all pre-existing image objects in documents
194
- for (let doc of util.toArray(docs)) { //x2
195
- for (let imageField of options.model.imageFields) { //x5
196
- let images = plugin._findImagesInData(doc, imageField, 0, '').filter(o => o.image)
197
- for (let image of images) {
198
- preExistingImages.push(image)
199
- useCount[image.image.uid] = (useCount[image.image.uid] || 0) + 1
200
- }
201
- }
225
+ let docs = await options.model._find(options.query, options)
226
+
227
+ // Find all pre-existing image objects in documents
228
+ for (let doc of util.toArray(docs)) { //x2
229
+ for (let imageField of options.model.imageFields) { //x5
230
+ let images = plugin._findImagesInData(doc, imageField, 0, '').filter(o => o.image)
231
+ for (let image of images) {
232
+ preExistingImages.push(image)
233
+ useCount[image.image.uid] = (useCount[image.image.uid] || 0) + 1
202
234
  }
235
+ }
236
+ }
203
237
 
204
- // console.log(1, useCount, preExistingImages)
238
+ // console.log(1, useCount, preExistingImages)
205
239
 
206
- // Assign pre-existing images within undefined deep objects and missing array items to null
207
- // ignore undefined root images
208
- let dataFilled = util.deepCopy(data)
209
- for (let key in dataFilled) {
210
- for (let pre of preExistingImages) {
211
- if (!pre.dataPath.match(new RegExp('^' + key + '(\\.|$)'))) continue
212
- util.setDeepValue(dataFilled, pre.dataPath, null, true)
213
- }
214
- }
240
+ // Assign pre-existing images within undefined deep objects and missing array items to null,
241
+ // ignore undefined root images
242
+ let dataFilled = util.deepCopy(data)
243
+ for (let key in dataFilled) {
244
+ for (let pre of preExistingImages) {
245
+ if (!pre.dataPath.match(new RegExp('^' + key + '(\\.|$)'))) continue
246
+ util.setDeepValue(dataFilled, pre.dataPath, null, true)
247
+ }
248
+ }
249
+ // console.log(dataFilled)
215
250
 
216
- // Loop all schema image fields
217
- for (let imageField of options.model.imageFields) { //x5
218
- let images = plugin._findImagesInData(dataFilled, imageField, 0, '')
219
- if (!images.length) continue
220
- // console.log(images)
251
+ // Check upload errors and find valid uploaded images
252
+ let files = await plugin._findValidImages(options.files || {}, options.model)
221
253
 
222
- // Data contains null images that once had a pre-existing image
223
- for (let image of images) {
224
- if (image.image == null && (pre = preExistingImages.find(o => o.dataPath == image.dataPath))) {
225
- useCount[pre.image.uid]--
226
- }
227
- }
254
+ // Loop all schema image fields
255
+ for (let imageField of options.model.imageFields) { //x5
256
+ let images = plugin._findImagesInData(dataFilled, imageField, 0, '')
257
+ if (!images.length) continue
258
+ // console.log(images)
228
259
 
229
- // Data contains valid (pre-existing) image-objects? And are we overriding a pre-existing image?
230
- for (let image of images) {
231
- if (image.image != null) {
232
- let pre = preExistingImages.find(o => o.dataPath == image.dataPath)
233
- if (typeof useCount[image.image.uid] == 'undefined') {
234
- throw `The passed image object for '${image.dataPath}' does not match any pre-existing
235
- images saved on this document.`
236
- } else if (pre && pre.image.uid != image.image.uid) {
237
- useCount[pre.image.uid]--
238
- useCount[image.image.uid]++
239
- } else if (!pre) {
240
- useCount[image.image.uid]++
241
- }
242
- }
243
- }
260
+ // Data contains null images that once had a pre-existing image
261
+ for (let image of images) {
262
+ if (image.image == null && (pre = preExistingImages.find(o => o.dataPath == image.dataPath))) {
263
+ useCount[pre.image.uid]--
244
264
  }
245
- // Check upload errors and find valid uploaded images. If any file is overriding a
246
- // pre-existing image, push to unused
247
- return plugin._findValidImages(options.files || {}, options.model).then(files => {
265
+ }
266
+
267
+ // Loop images found in the data
268
+ for (let image of images) {
269
+ if (image.image != null) {
270
+ let preExistingImage = preExistingImages.find(o => o.dataPath == image.dataPath)
271
+ // valid image-object?
272
+ if (typeof useCount[image.image.uid] == 'undefined') {
273
+ throw `The passed image object for '${image.dataPath}' does not match any pre-existing
274
+ images saved on this document.`
275
+ // Different image from prexisting image
276
+ } else if (preExistingImage && preExistingImage.image.uid != image.image.uid) {
277
+ useCount[preExistingImage.image.uid]--
278
+ useCount[image.image.uid]++
279
+ // No pre-existing image found
280
+ } else if (!preExistingImage) {
281
+ useCount[image.image.uid]++
282
+ }
283
+ // Any file overriding this image?
248
284
  for (let filesArray of files) {
249
- if ((pre = preExistingImages.find(o => o.dataPath == filesArray.inputPath))) {
250
- useCount[pre.image.uid]--
285
+ if (image.dataPath == filesArray.inputPath) {
286
+ useCount[image.image.uid]--
251
287
  }
252
288
  }
253
- })
254
-
255
- }).then(() => {
256
- // Retrieve all the unused files
257
- let unused = []
258
- // console.log(3, useCount)
259
- for (let key in useCount) {
260
- if (useCount[key] > 0) continue
261
- let pre = preExistingImages.find(o => o.image.uid == key)
262
- unused.push(
263
- // original key can have a different extension
264
- { Key: pre.image.path },
265
- { Key: `small/${key}.jpg` },
266
- { Key: `medium/${key}.jpg` },
267
- { Key: `large/${key}.jpg` }
268
- )
269
- this.manager.info(
270
- `Removing '${pre.image.filename}' from '${pre.image.bucket}/${pre.image.path}'`
271
- )
272
289
  }
273
- if (test) return Promise.resolve([useCount, unused])
274
- // Delete any unused images from s3. If the image is on a different bucket
275
- // the file doesnt get deleted, we only delete from plugin.awsBucket.
276
- if (!unused.length) return
277
- return new Promise((resolve, reject) => {
278
- plugin.s3.deleteObjects({ Bucket: plugin.awsBucket, Delete: { Objects: unused }}, (err, data) => {
279
- if (err) reject(err)
280
- resolve()
281
- })
282
- })
290
+ }
291
+ }
292
+
293
+ // Retrieve all the unused files
294
+ // console.log(3, useCount)
295
+ let unused = []
296
+ for (let key in useCount) {
297
+ if (useCount[key] > 0) continue
298
+ let pre = preExistingImages.find(o => o.image.uid == key)
299
+ unused.push(
300
+ // original key can have a different extension
301
+ { Key: pre.image.path },
302
+ { Key: `small/${key}.jpg` },
303
+ { Key: `medium/${key}.jpg` },
304
+ { Key: `large/${key}.jpg` }
305
+ )
306
+ this.manager.info(
307
+ `Removing '${pre.image.filename}' from '${pre.image.bucket}/${pre.image.path}'`
308
+ )
309
+ }
310
+ if (test) return [useCount, unused]
311
+ // Delete any unused images from s3. If the image is on a different bucket
312
+ // the file doesnt get deleted, we only delete from plugin.awsBucket.
313
+ if (!unused.length) return
314
+ await new Promise((resolve, reject) => {
315
+ plugin.s3.deleteObjects({ Bucket: plugin.awsBucket, Delete: { Objects: unused }}, (err, data) => {
316
+ if (err) reject(err)
317
+ resolve()
283
318
  })
319
+ })
284
320
  },
285
321
 
286
322
  _addImageObjectsToData: function(path, data, image) {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/test/model.js CHANGED
@@ -116,10 +116,9 @@ module.exports = function(monastery, opendb) {
116
116
  })
117
117
 
118
118
  test('Model indexes', async () => {
119
- // Need to test different types of indexes
119
+ // Setup: Need to test different types of indexes
120
120
  let db = (await opendb(null)).db
121
-
122
- // Drop previously tested collections
121
+ // Setup: Drop previously tested collections
123
122
  if ((await db._db.listCollections().toArray()).find(o => o.name == 'userIndexRaw')) {
124
123
  await db._db.collection('userIndexRaw').drop()
125
124
  }
@@ -190,6 +189,96 @@ module.exports = function(monastery, opendb) {
190
189
  db.close()
191
190
  })
192
191
 
192
+ test('Model subdocument indexes', async () => {
193
+ // Setup: Need to test different types of indexes
194
+ let db = (await opendb(null)).db
195
+ // Setup: Drop previously tested collections
196
+ if ((await db._db.listCollections().toArray()).find(o => o.name == 'userIndexSubdoc')) {
197
+ await db._db.collection('userIndexSubdoc').drop()
198
+ }
199
+ // Run
200
+ let userModel = await db.model('userIndexSubdoc', {
201
+ fields: {}
202
+ })
203
+ await expect(userModel._setupIndexes(
204
+ {
205
+ animals: {
206
+ name: { type: 'string', index: 'unique' },
207
+ },
208
+ animals2: {
209
+ names: {
210
+ name: { type: 'string', index: 'unique' },
211
+ },
212
+ },
213
+ animals3: {
214
+ names: {
215
+ name: { type: 'string', index: 'text' },
216
+ },
217
+ },
218
+ }, {
219
+ dryRun: true
220
+ }
221
+ )).resolves.toEqual([{
222
+ 'key': { 'animals.name': 1 },
223
+ 'name': 'animals.name_1',
224
+ 'unique': true,
225
+ }, {
226
+ 'key': { 'animals2.names.name': 1 },
227
+ 'name': 'animals2.names.name_1',
228
+ 'unique': true,
229
+ }, {
230
+ 'key': { 'animals3.names.name': 'text' },
231
+ 'name': 'text',
232
+ }])
233
+
234
+ db.close()
235
+ })
236
+
237
+ test('Model array indexes', async () => {
238
+ // Setup: Need to test different types of indexes
239
+ let db = (await opendb(null)).db
240
+ // Setup: Drop previously tested collections
241
+ if ((await db._db.listCollections().toArray()).find(o => o.name == 'userIndexArray')) {
242
+ await db._db.collection('userIndexArray').drop()
243
+ }
244
+ // Run
245
+ let userModel = await db.model('userIndexArray', {
246
+ fields: {}
247
+ })
248
+ await expect(userModel._setupIndexes(
249
+ {
250
+ animals: [{
251
+ name: { type: 'string', index: 'unique' },
252
+ }],
253
+ animals2: [{ type: 'string', index: true }],
254
+ animals3: [[{ type: 'string', index: true }]],
255
+ animals4: [{
256
+ names: [{
257
+ name: { type: 'string', index: 'unique' },
258
+ }],
259
+ }],
260
+ }, {
261
+ dryRun: true
262
+ }
263
+ )).resolves.toEqual([{
264
+ 'key': { 'animals.name': 1 },
265
+ 'name': 'animals.name_1',
266
+ 'unique': true,
267
+ }, {
268
+ 'key': { 'animals2': 1 },
269
+ 'name': 'animals2_1',
270
+ }, {
271
+ 'key': { 'animals3.0': 1 },
272
+ 'name': 'animals3.0_1',
273
+ }, {
274
+ 'key': { 'animals4.names.name': 1 },
275
+ 'name': 'animals4.names.name_1',
276
+ 'unique': true,
277
+ }])
278
+
279
+ db.close()
280
+ })
281
+
193
282
  test('Model 2dsphere indexes', async () => {
194
283
  // Setup. The tested model needs to be unique as race condition issue arises when the same model
195
284
  // with text indexes are setup at the same time
package/test/monk.js CHANGED
File without changes
@@ -1,3 +1,5 @@
1
+ let util = require('../lib/util')
2
+
1
3
  module.exports = function(monastery, opendb) {
2
4
 
3
5
  // Data no images doesn't throw error
@@ -299,13 +301,8 @@ module.exports = function(monastery, opendb) {
299
301
 
300
302
  plugin.removeImages(options, req.body, true)
301
303
  .then(res => {
302
- expect(res[0]).toEqual({ test1: 0, test2: -1, test3: 1, test4: 0 })
304
+ expect(res[0]).toEqual({ test1: 1, test2: 0, test3: 1, test4: 0 })
303
305
  expect(res[1]).toEqual([
304
- { Key: 'dir/test1.png' },
305
- { Key: 'small/test1.jpg' },
306
- { Key: 'medium/test1.jpg' },
307
- { Key: 'large/test1.jpg' },
308
-
309
306
  { Key: 'dir/test2.png' },
310
307
  { Key: 'small/test2.jpg' },
311
308
  { Key: 'medium/test2.jpg' },
@@ -560,4 +557,154 @@ module.exports = function(monastery, opendb) {
560
557
  })()
561
558
  })
562
559
 
560
+ test('images reorder', async () => {
561
+ let db = (await opendb(null, {
562
+ timestamps: false,
563
+ serverSelectionTimeoutMS: 2000,
564
+ imagePlugin: { awsBucket: 'fake', awsAccessKeyId: 'fake', awsSecretAccessKey: 'fake' }
565
+ })).db
566
+
567
+ let user = db.model('user', { fields: {
568
+ logos: [{ type: 'image' }],
569
+ }})
570
+
571
+ let image = {
572
+ bucket: 'test',
573
+ date: 1234,
574
+ filename: 'lion1.png',
575
+ filesize: 1234,
576
+ path: 'test/lion1.png',
577
+ uid: 'lion1'
578
+ }
579
+
580
+ let user1 = await db.user._insert({
581
+ logos: [image],
582
+ })
583
+
584
+ let plugin = db.imagePluginFile
585
+ let supertest = require('supertest')
586
+ let express = require('express')
587
+ let upload = require('express-fileupload')
588
+ let app = express()
589
+ app.use(upload())
590
+
591
+ // Reorder
592
+ app.post('/', async function(req, res) {
593
+ try {
594
+ req.body.logos = JSON.parse(req.body.logos)
595
+ let options = { files: req.files, model: user, query: { _id: user1._id } }
596
+ let response = await plugin.removeImages(options, req.body, true)
597
+ expect(response[0]).toEqual({ lion1: 1 })
598
+ expect(response[1]).toEqual([])
599
+ res.send()
600
+ } catch (e) {
601
+ console.log(e.message || e)
602
+ res.status(500).send()
603
+ }
604
+ })
605
+ await supertest(app)
606
+ .post('/')
607
+ .field('logos', JSON.stringify([ null, image ]))
608
+ .expect(200)
609
+
610
+ db.close()
611
+ })
612
+
613
+ test('images reorder and added image', async () => {
614
+ // latest (2022.02)
615
+ let db = (await opendb(null, {
616
+ timestamps: false,
617
+ serverSelectionTimeoutMS: 2000,
618
+ imagePlugin: { awsBucket: 'fake', awsAccessKeyId: 'fake', awsSecretAccessKey: 'fake' }
619
+ })).db
620
+
621
+ let user = db.model('user', { fields: {
622
+ photos: [{ type: 'image' }],
623
+ }})
624
+
625
+ let image = {
626
+ bucket: 'test',
627
+ date: 1234,
628
+ filename: 'lion1.png',
629
+ filesize: 1234,
630
+ path: 'test/lion1.png',
631
+ uid: 'lion1'
632
+ }
633
+
634
+ let user1 = await db.user._insert({
635
+ photos: [image],
636
+ })
637
+
638
+ let plugin = db.imagePluginFile
639
+ let supertest = require('supertest')
640
+ let express = require('express')
641
+ let upload = require('express-fileupload')
642
+ let app = express()
643
+ app.use(upload())
644
+
645
+ app.post('/', async function(req, res) {
646
+ try {
647
+ // Parse and validate data which is used before in update/insert
648
+ let options = { files: req.files, model: user, query: { _id: user1._id } }
649
+ let data = await util.parseData(req.body)
650
+ data = await user.validate(data, { ...options, update: true })
651
+
652
+ // Empty photo placeholder not removed in validate?
653
+ expect(data.photos[0]).toEqual(null)
654
+ expect(data.photos[1]).toEqual(image)
655
+
656
+ // Remove images
657
+ let response = await plugin.removeImages(options, data, true)
658
+ expect(response[0]).toEqual({ lion1: 1 }) // useCount
659
+ expect(response[1]).toEqual([]) // unused
660
+
661
+ // New file exists
662
+ let validFiles = await plugin._findValidImages(req.files, user)
663
+ expect(((validFiles||[])[0]||{}).inputPath).toEqual('photos.0') // Valid inputPath
664
+
665
+ // Add images
666
+ response = await plugin.addImages(options, data, true)
667
+ expect(response[0]).toEqual({
668
+ photos: [{
669
+ bucket: 'fake',
670
+ date: expect.any(Number),
671
+ filename: 'lion2.jpg',
672
+ filesize: expect.any(Number),
673
+ path: expect.any(String),
674
+ uid: expect.any(String)
675
+ }, {
676
+ bucket: 'test', // still the same image-object reference (nothing new)
677
+ date: expect.any(Number),
678
+ filename: 'lion1.png',
679
+ filesize: expect.any(Number),
680
+ path: expect.any(String),
681
+ uid: expect.any(String)
682
+ },],
683
+ })
684
+
685
+ res.send()
686
+ } catch (e) {
687
+ console.log(e.message || e)
688
+ res.status(500).send()
689
+ }
690
+ })
691
+
692
+ await supertest(app)
693
+ .post('/')
694
+ // Mock multipart/form-data syntax which is not supported by supertest (formdata sent with axios)
695
+ // E.g.
696
+ // req.body = 'photos[1][bucket]' : '...'
697
+ // req.files = 'photos[0]' : { ...binary }
698
+ .field('photos[1][bucket]', image.bucket)
699
+ .field('photos[1][date]', image.date)
700
+ .field('photos[1][filename]', image.filename)
701
+ .field('photos[1][filesize]', image.filesize)
702
+ .field('photos[1][path]', image.path)
703
+ .field('photos[1][uid]', image.uid)
704
+ .attach('photos[0]', `${__dirname}/assets/lion2.jpg`)
705
+ .expect(200)
706
+
707
+ db.close()
708
+ })
709
+
563
710
  }
package/test/populate.js CHANGED
File without changes
package/test/util.js CHANGED
File without changes