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.
- package/lib/model-validate.js +7 -4
- package/lib/model.js +1 -0
- package/lib/rules.js +48 -43
- package/lib/util.js +1 -1
- package/package.json +4 -2
- package/plugins/images/index.js +123 -87
- package/test/plugin-images.js +153 -6
- package/test/validate.js +45 -28
package/lib/model-validate.js
CHANGED
|
@@ -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(/\./)))
|
|
269
|
+
if (typeof value === 'undefined' && (!validateUndefined || (opts.update && !path.match(/\./)))) return
|
|
270
270
|
|
|
271
|
-
// Ignore null (if nullObject is set on objects or arrays)
|
|
272
|
-
if (
|
|
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 (
|
|
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
|
|
18
|
+
// "Type" rules below ignore undefined and null
|
|
18
19
|
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
+
isAny: {
|
|
117
126
|
validateEmptyString: true,
|
|
118
127
|
message: '',
|
|
119
128
|
fn: function(x) {
|
|
120
129
|
return true
|
|
121
130
|
}
|
|
122
131
|
},
|
|
123
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
+
isUUID: {
|
|
214
219
|
message: 'Value was not a valid UUID.',
|
|
215
220
|
fn: function(x, arg) { return validator.isUUID(x) }
|
|
216
221
|
},
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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', '
|
|
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.
|
|
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
|
},
|
package/plugins/images/index.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
|
222
|
+
if (typeof options.files == 'undefined') return
|
|
189
223
|
|
|
190
224
|
// Find all documents from the same query
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
238
|
+
// console.log(1, useCount, preExistingImages)
|
|
205
239
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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 (
|
|
250
|
-
useCount[
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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) {
|
package/test/plugin-images.js
CHANGED
|
@@ -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:
|
|
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('
|
|
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 : '' }
|
|
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 })).
|
|
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: [
|
|
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:
|
|
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
|
-
.
|
|
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 })
|
|
770
|
-
await expect(
|
|
771
|
-
await expect(user.validate({ amount: undefined })).resolves.toEqual({})
|
|
772
|
-
await expect(user.validate({ amount: null })).
|
|
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 = {
|