monastery 1.30.0 → 1.30.4

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.
@@ -266,13 +266,16 @@ module.exports = {
266
266
  if (!ruleMessage) ruleMessage = rule.message
267
267
 
268
268
  // Ignore undefined (if updated root property, or ignoring)
269
- if ((!validateUndefined || (opts.update && !path.match(/\./))) && typeof value === 'undefined') return
269
+ if (typeof value === 'undefined' && (!validateUndefined || (opts.update && !path.match(/\./)))) return
270
270
 
271
- // Ignore null (if nullObject is set on objects or arrays) (todo: change to ignoreNull)
272
- if (field.nullObject && (field.isObject || field.isArray) && value === null) return
271
+ // Ignore null (if nullObject is set on objects or arrays)
272
+ if (value === null && (field.isObject || field.isArray) && field.nullObject) return
273
+
274
+ // Ignore null
275
+ if (value === null && !(field.isObject || field.isArray) && !rule.validateNull) return
273
276
 
274
277
  // Ignore empty strings
275
- if (!rule.validateEmptyString && value === '') return
278
+ if (value === '' && !rule.validateEmptyString) return
276
279
 
277
280
  // Rule failed
278
281
  if (!rule.fn.call(dataRoot, value, ruleArg, path, this)) return {
package/lib/model.js CHANGED
@@ -61,6 +61,7 @@ let Model = module.exports = function(name, opts, manager) {
61
61
  // Update with formatted rule
62
62
  let formattedRule = util.isObject(rule)? rule : { fn: rule }
63
63
  if (!formattedRule.message) formattedRule.message = `Invalid data property for rule "${ruleName}".`
64
+ if (typeof formattedRule.validateNull == 'undefined') formattedRule.validateNull = true
64
65
  if (typeof formattedRule.validateEmptyString == 'undefined') formattedRule.validateEmptyString = true
65
66
  this.rules[ruleName] = formattedRule
66
67
  }
package/lib/rules.js CHANGED
@@ -5,7 +5,8 @@ let validator = require('validator')
5
5
  module.exports = {
6
6
 
7
7
  required: {
8
- validateUndefined: true,
8
+ validateUndefined: true, // (false for custom rules)
9
+ validateNull: true,
9
10
  validateEmptyString: true,
10
11
  message: 'This field is required.',
11
12
  fn: function(x) {
@@ -14,12 +15,13 @@ module.exports = {
14
15
  }
15
16
  },
16
17
 
17
- // Type rules below ignore undefined (default for custom model rules)
18
+ // "Type" rules below ignore undefined and null
18
19
 
19
- 'isBoolean': {
20
+ isBoolean: {
20
21
  validateEmptyString: true,
21
22
  message: 'Value was not a boolean.',
22
23
  tryParse: function(x) {
24
+ if (x === '') return null
23
25
  if (typeof x === 'string' && x === 'true') return true
24
26
  else if (typeof x === 'string' && x === 'false') return false
25
27
  else return x
@@ -28,7 +30,7 @@ module.exports = {
28
30
  return typeof x === 'boolean'
29
31
  }
30
32
  },
31
- 'isArray': {
33
+ isArray: {
32
34
  validateEmptyString: true,
33
35
  message: 'Value was not an array.',
34
36
  tryParse: function(x) {
@@ -41,10 +43,11 @@ module.exports = {
41
43
  return Array.isArray(x)
42
44
  }
43
45
  },
44
- 'isDate': {
46
+ isDate: {
45
47
  validateEmptyString: true,
46
48
  message: 'Value was not a unix timestamp.',
47
49
  tryParse: function(x) {
50
+ if (x === '') return null
48
51
  if (util.isString(x) && x.match(/^[+-]?[0-9]+$/)) return x // keep string nums intact
49
52
  return isNaN(parseInt(x))? x : parseInt(x)
50
53
  },
@@ -53,7 +56,7 @@ module.exports = {
53
56
  return typeof x === 'number' && (parseInt(x) === x)
54
57
  }
55
58
  },
56
- 'isImageObject': {
59
+ isImageObject: {
57
60
  validateEmptyString: true,
58
61
  message: 'Invalid image value',
59
62
  messageLong: 'Image fields need to either be null, undefined, file, or an object containing the following '
@@ -69,10 +72,11 @@ module.exports = {
69
72
  if (isObject && x.bucket && x.date && x.filename && x.filesize && x.path && x.uid) return true
70
73
  }
71
74
  },
72
- 'isInteger': {
75
+ isInteger: {
73
76
  validateEmptyString: true,
74
77
  message: 'Value was not an integer.',
75
78
  tryParse: function(x) {
79
+ if (x === '') return null
76
80
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
77
81
  return isNaN(parseInt(x)) || (!x && x!==0) || x===true? x : parseInt(x)
78
82
  },
@@ -81,10 +85,11 @@ module.exports = {
81
85
  return typeof x === 'number' && (parseInt(x) === x)
82
86
  }
83
87
  },
84
- 'isNumber': {
88
+ isNumber: {
85
89
  validateEmptyString: true,
86
90
  message: 'Value was not a number.',
87
91
  tryParse: function(x) {
92
+ if (x === '') return null
88
93
  if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
89
94
  return isNaN(Number(x)) || (!x && x!==0) || x===true? x : Number(x)
90
95
  },
@@ -93,7 +98,7 @@ module.exports = {
93
98
  return typeof x === 'number'
94
99
  }
95
100
  },
96
- 'isObject': {
101
+ isObject: {
97
102
  validateEmptyString: true,
98
103
  message: 'Value was not an object.',
99
104
  tryParse: function(x) {
@@ -106,21 +111,25 @@ module.exports = {
106
111
  return x !== null && typeof x === 'object' && !(x instanceof Array)
107
112
  }
108
113
  },
109
- 'isString': {
114
+ isString: {
110
115
  validateEmptyString: true,
111
116
  message: 'Value was not a string.',
117
+ tryParse: function(x) {
118
+ if (typeof x === 'number') return x + ''
119
+ else return x
120
+ },
112
121
  fn: function(x) {
113
122
  return typeof x === 'string'
114
123
  }
115
124
  },
116
- 'isAny': {
125
+ isAny: {
117
126
  validateEmptyString: true,
118
127
  message: '',
119
128
  fn: function(x) {
120
129
  return true
121
130
  }
122
131
  },
123
- 'isId': {
132
+ isId: {
124
133
  validateEmptyString: true,
125
134
  message: 'Value was not a valid ObjectId.',
126
135
  tryParse: function(x) {
@@ -135,34 +144,27 @@ module.exports = {
135
144
  return util.isObject(x) && ObjectId.isValid(x)/*x.get_inc*/? true : false
136
145
  }
137
146
  },
138
- 'max': {
139
- validateEmptyString: true,
147
+
148
+ /* "Number" rules below ignore undefined and null */
149
+
150
+ max: {
140
151
  message: (x, arg) => 'Value was greater than the configured maximum (' + arg + ')',
141
152
  fn: function(x, arg) {
142
153
  if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
143
154
  return x <= arg
144
155
  }
145
156
  },
146
- 'min': {
147
- validateEmptyString: true,
157
+ min: {
148
158
  message: (x, arg) => 'Value was less than the configured minimum (' + arg + ')',
149
159
  fn: function(x, arg) {
150
160
  if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
151
161
  return x >= arg
152
162
  }
153
163
  },
154
- 'isNotEmptyString': {
155
- validateEmptyString: true,
156
- message: 'Value was an empty string.',
157
- fn: function(x) {
158
- return x !== ''
159
- }
160
- },
161
164
 
162
- // Rules below ignore undefined, & empty strings
163
- // (e.g. an empty email field can be saved that isn't required)
165
+ /* "String" rules below ignore undefined, null, and empty strings */
164
166
 
165
- 'enum': {
167
+ enum: {
166
168
  message: (x, arg) => 'Invalid enum value',
167
169
  fn: function(x, arg) {
168
170
  for (let item of arg) {
@@ -170,51 +172,54 @@ module.exports = {
170
172
  }
171
173
  }
172
174
  },
173
- // 'hasAgreed': {
174
- // message: (x, arg) => 'Please agree to the terms and conditions.',
175
- // fn: function(x, arg) { return !x }
176
- // },
177
- 'isAfter': {
175
+ isAfter: {
178
176
  message: (x, arg) => 'Value was before the configured time (' + arg + ')',
179
177
  fn: function(x, arg) { return validator.isAfter(x, arg) }
180
178
  },
181
- 'isBefore': {
179
+ isBefore: {
182
180
  message: (x, arg) => 'Value was after the configured time (' + arg + ')',
183
181
  fn: function(x, arg) { return validator.isBefore(x, arg) }
184
182
  },
185
- 'isCreditCard': {
183
+ isCreditCard: {
186
184
  message: 'Value was not a valid credit card.',
187
185
  fn: function(x, arg) { return validator.isCreditCard(x, arg) }
188
186
  },
189
- 'isEmail': {
187
+ isEmail: {
190
188
  message: 'Please enter a valid email address.',
191
189
  fn: function(x, arg) { return validator.isEmail(x, arg) }
192
190
  },
193
- 'isHexColor': {
191
+ isHexColor: {
194
192
  message: 'Value was not a valid hex color.',
195
193
  fn: function(x, arg) { return validator.isHexColor(x, arg) }
196
194
  },
197
- 'isIn': {
195
+ isIn: {
198
196
  message: (x, arg) => 'Value was not in the configured whitelist (' + arg.join(', ') + ')',
199
197
  fn: function(x, arg) { return validator.isIn(x, arg) }
200
198
  },
201
- 'isIP': {
199
+ isIP: {
202
200
  message: 'Value was not a valid IP address.',
203
201
  fn: function(x, arg) { return validator.isIP(x, arg) }
204
202
  },
205
- 'isNotIn': {
203
+ isNotEmptyString: {
204
+ validateEmptyString: true,
205
+ message: 'Value was an empty string.',
206
+ fn: function(x) {
207
+ return x !== ''
208
+ }
209
+ },
210
+ isNotIn: {
206
211
  message: (x, arg) => 'Value was in the configured blacklist (' + arg.join(', ') + ')',
207
212
  fn: function(x, arg) { return !validator.isIn(x, arg) }
208
213
  },
209
- 'isURL': {
214
+ isURL: {
210
215
  message: 'Value was not a valid URL.',
211
216
  fn: function(x, arg) { return validator.isURL(x, arg === true? undefined : arg) }
212
217
  },
213
- 'isUUID': {
218
+ isUUID: {
214
219
  message: 'Value was not a valid UUID.',
215
220
  fn: function(x, arg) { return validator.isUUID(x) }
216
221
  },
217
- 'minLength': {
222
+ minLength: {
218
223
  message: function(x, arg) {
219
224
  if (typeof x === 'string') return 'Value needs to be at least ' + arg + ' characters long.'
220
225
  else return 'Value needs to contain a minimum of ' + arg + ' items.'
@@ -225,7 +230,7 @@ module.exports = {
225
230
  else return x.length >= arg
226
231
  }
227
232
  },
228
- 'maxLength': {
233
+ maxLength: {
229
234
  message: function(x, arg) {
230
235
  if (typeof x === 'string') return 'Value was longer than the configured maximum length (' + arg + ')'
231
236
  else return 'Value cannot contain more than ' + arg + ' items.'
@@ -236,7 +241,7 @@ module.exports = {
236
241
  else return x.length <= arg
237
242
  }
238
243
  },
239
- 'regex': {
244
+ regex: {
240
245
  message: (x, arg) => 'Value did not match the configured regular expression (' + arg + ')',
241
246
  fn: function(x, arg) {
242
247
  if (util.isRegex(arg)) return validator.matches(x, arg)
package/lib/util.js CHANGED
@@ -300,7 +300,7 @@ module.exports = {
300
300
  * @param {boolean} ignoreEmptyArrays - ignore empty arrays
301
301
  * @return obj
302
302
  */
303
- let chunks = path.split('.') // ['pets', '$', 'dog']
303
+ let chunks = path.split('.') // ['pets', '$|0-9', 'dog']
304
304
  let target = obj
305
305
  for (let i=0, l=chunks.length; i<l; i++) {
306
306
  if (l === i+1) { // Last
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.30.0",
5
+ "version": "1.30.4",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -23,7 +23,8 @@
23
23
  "test-one-example": "jest -t images",
24
24
  "dev": "npm run lint & DEBUG=-monastery:info jest --watchAll --runInBand --verbose=false",
25
25
  "lint": "eslint ./lib ./plugins ./test",
26
- "docs": "cd docs && bundle exec jekyll serve --livereload --livereload-port 4001"
26
+ "docs": "cd docs && bundle exec jekyll serve --livereload --livereload-port 4001",
27
+ "mong": "nodemon resources/mong.js"
27
28
  },
28
29
  "dependencies": {
29
30
  "aws-sdk": "2.1062.0",
@@ -39,6 +40,7 @@
39
40
  "express": "4.17.1",
40
41
  "express-fileupload": "1.2.0",
41
42
  "jest": "27.4.7",
43
+ "nodemon": "2.0.15",
42
44
  "standard-version": "9.3.2",
43
45
  "supertest": "4.0.2"
44
46
  },
@@ -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) {
@@ -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/validate.js CHANGED
@@ -2,7 +2,7 @@ let validate = require('../lib/model-validate')
2
2
 
3
3
  module.exports = function(monastery, opendb) {
4
4
 
5
- test('Validation basic errors', async () => {
5
+ test('validation basic errors', async () => {
6
6
  // Setup
7
7
  let db = (await opendb(false)).db
8
8
  let user = db.model('user', { fields: {
@@ -19,7 +19,7 @@ module.exports = function(monastery, opendb) {
19
19
  detail: 'This field is required.',
20
20
  meta: { rule: 'required', model: 'user', field: 'name' }
21
21
  })
22
- await expect(user.validate({ name : '' }, { validateUndefined: false })).rejects.toContainEqual({
22
+ await expect(user.validate({ name : '' })).rejects.toContainEqual({
23
23
  status: '400',
24
24
  title: 'name',
25
25
  detail: 'This field is required.',
@@ -34,7 +34,16 @@ module.exports = function(monastery, opendb) {
34
34
  await expect(user.validate({}, { update: true })).resolves.toEqual({})
35
35
 
36
36
  // Type error (string)
37
- await expect(user.validate({ name: 1 })).rejects.toContainEqual({
37
+ await expect(user.validate({ name: 1 })).resolves.toEqual({ name: '1' })
38
+ await expect(user.validate({ name: 1.123 })).resolves.toEqual({ name: '1.123' })
39
+ await expect(user.validate({ name: undefined }, { validateUndefined: false })).resolves.toEqual({})
40
+ await expect(user.validate({ name: null })).rejects.toContainEqual({
41
+ status: '400',
42
+ title: 'name',
43
+ detail: 'This field is required.',
44
+ meta: { rule: 'required', model: 'user', field: 'name' }
45
+ })
46
+ await expect(user.validate({ name: true })).rejects.toContainEqual({
38
47
  status: '400',
39
48
  title: 'name',
40
49
  detail: 'Value was not a string.',
@@ -42,6 +51,7 @@ module.exports = function(monastery, opendb) {
42
51
  })
43
52
 
44
53
  // Type error (date)
54
+ await expect(user.validate({ name: 'a', date: null })).resolves.toEqual({ name: 'a', date: null })
45
55
  await expect(user.validate({ name: 'a', date: 'fe' })).rejects.toContainEqual({
46
56
  status: '400',
47
57
  title: 'date',
@@ -49,6 +59,30 @@ module.exports = function(monastery, opendb) {
49
59
  meta: { rule: 'isDate', model: 'user', field: 'date' }
50
60
  })
51
61
 
62
+ // Type error (number)
63
+ let usernum = db.model('usernum', { fields: { amount: { type: 'number', required: true }}})
64
+ await expect(usernum.validate({ amount: 0 })).resolves.toEqual({ amount: 0 })
65
+ await expect(usernum.validate({ amount: '0' })).resolves.toEqual({ amount: 0 })
66
+ await expect(usernum.validate({ amount: undefined }, { validateUndefined: false })).resolves.toEqual({})
67
+ await expect(usernum.validate({ amount: false })).rejects.toEqual([{
68
+ status: '400',
69
+ title: 'amount',
70
+ detail: 'Value was not a number.',
71
+ meta: { rule: 'isNumber', model: 'usernum', field: 'amount' }
72
+ }])
73
+ await expect(usernum.validate({ amount: null })).rejects.toEqual([{
74
+ status: '400',
75
+ title: 'amount',
76
+ detail: 'This field is required.',
77
+ meta: { rule: 'required', model: 'usernum', field: 'amount' },
78
+ }])
79
+ await expect(usernum.validate({ amount: null }, { validateUndefined: false })).rejects.toEqual([{
80
+ status: '400',
81
+ title: 'amount',
82
+ detail: 'This field is required.',
83
+ meta: { rule: 'required', model: 'usernum', field: 'amount' },
84
+ }])
85
+
52
86
  // Type error (array)
53
87
  await expect(user.validate({ name: 'a', colors: 1 })).rejects.toContainEqual({
54
88
  status: '400',
@@ -224,7 +258,7 @@ module.exports = function(monastery, opendb) {
224
258
 
225
259
  // Type error within an array (string)
226
260
  await expect(user.validate({
227
- animals: { cats: [1] }
261
+ animals: { cats: [true] }
228
262
  })).rejects.toContainEqual({
229
263
  status: '400',
230
264
  title: 'animals.cats.0',
@@ -239,7 +273,7 @@ module.exports = function(monastery, opendb) {
239
273
  detail: 'This field is required.',
240
274
  meta: { rule: 'required', model: 'user', field: 'color' }
241
275
  }
242
- await expect(user.validate({ animals: { dogs: [{ name: 'sparky', color: 1 }] }}))
276
+ await expect(user.validate({ animals: { dogs: [{ name: 'sparky', color: false }] }}))
243
277
  .rejects.toContainEqual({
244
278
  ...error,
245
279
  detail: 'Value was not a string.',
@@ -623,16 +657,9 @@ module.exports = function(monastery, opendb) {
623
657
  // Ignores invalid data
624
658
  await expect(user.validate({ badprop: true, schema: {} })).resolves.toEqual({})
625
659
 
626
- // Rejects null for non object/array fields
627
- await expect(user.validate({ name: null })).rejects.toEqual([{
628
- 'detail': 'Value was not a string.',
629
- 'meta': {'detailLong': undefined, 'field': 'name', 'model': 'user', 'rule': 'isString'},
630
- 'status':'400',
631
- 'title': 'name'
632
- }])
633
-
634
660
  // String data
635
661
  await expect(user.validate({ name: 'Martin Luther' })).resolves.toEqual({ name: 'Martin Luther' })
662
+ await expect(user.validate({ name: null })).resolves.toEqual({ name: null })
636
663
 
637
664
  // Array data
638
665
  await expect(user.validate({ names: ['blue'] })).resolves.toEqual({ names: ['blue'] })
@@ -652,12 +679,7 @@ module.exports = function(monastery, opendb) {
652
679
 
653
680
  // Subdocument property data (null)
654
681
  await expect(user.validate({ animals: { dog: null }}))
655
- .rejects.toEqual([{
656
- 'detail': 'Value was not a string.',
657
- 'meta': {'detailLong': undefined, 'field': 'dog', 'model': 'user', 'rule': 'isString'},
658
- 'status': '400',
659
- 'title': 'animals.dog',
660
- }])
682
+ .resolves.toEqual({ animals: { dog: null }})
661
683
 
662
684
  // Subdocument property data (unknown data)
663
685
  await expect(user.validate({ animals: { dog: 'sparky', cat: 'grumpy' } }))
@@ -766,15 +788,10 @@ module.exports = function(monastery, opendb) {
766
788
  })
767
789
 
768
790
  // Number valid
769
- await expect(user2.validate({ amount: 0 })).resolves.toEqual({ amount: 0 }) // required
770
- await expect(user.validate({ amount: '0' })).resolves.toEqual({ amount: 0 }) // required
771
- await expect(user.validate({ amount: undefined })).resolves.toEqual({}) // not required
772
- await expect(user.validate({ amount: null })).rejects.toEqual([{ // type error
773
- 'detail': 'Value was not a number.',
774
- 'meta': { 'field': 'amount', 'model': 'user', 'rule': 'isNumber'},
775
- 'status': '400',
776
- 'title': 'amount'
777
- }])
791
+ await expect(user2.validate({ amount: 0 })).resolves.toEqual({ amount: 0 })
792
+ await expect(user2.validate({ amount: '0' })).resolves.toEqual({ amount: 0 })
793
+ await expect(user.validate({ amount: undefined })).resolves.toEqual({})
794
+ await expect(user.validate({ amount: null })).resolves.toEqual({ amount: null })
778
795
 
779
796
  // Number required
780
797
  let mock1 = {