monastery 1.29.0 → 1.30.3
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-crud.js +4 -4
- package/lib/model-validate.js +9 -5
- package/lib/model.js +11 -9
- package/lib/rules.js +16 -43
- package/lib/util.js +1 -1
- package/package.json +1 -1
- package/plugins/images/index.js +123 -87
- package/test/model.js +92 -3
- package/test/plugin-images.js +153 -6
- package/test/validate.js +104 -61
package/lib/model-crud.js
CHANGED
|
@@ -9,7 +9,7 @@ module.exports = {
|
|
|
9
9
|
* @param {object|array} <opts.data> - documents to insert
|
|
10
10
|
* @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove all blacklisting
|
|
11
11
|
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
12
|
-
* @param {array|string|
|
|
12
|
+
* @param {array|string|false} validateUndefined - ignore all required fields during insert, or
|
|
13
13
|
* undefined subdocument required fields that have a defined parent/grandparent during update
|
|
14
14
|
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
15
15
|
* @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
|
|
@@ -23,7 +23,7 @@ module.exports = {
|
|
|
23
23
|
opts.model = this
|
|
24
24
|
let data = opts.data = opts.data || (opts.req? opts.req.body : {})
|
|
25
25
|
let options = util.omit(opts, [
|
|
26
|
-
'data', 'insert', 'model', 'respond', '
|
|
26
|
+
'data', 'insert', 'model', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'
|
|
27
27
|
])
|
|
28
28
|
if (cb && !util.isFunction(cb)) {
|
|
29
29
|
throw new Error(`The callback passed to ${this.name}.insert() is not a function`)
|
|
@@ -194,7 +194,7 @@ module.exports = {
|
|
|
194
194
|
* @param {object} <opts.query> - mongodb query object
|
|
195
195
|
* @param {object|array} <opts.data> - mongodb document update object(s)
|
|
196
196
|
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
197
|
-
* @param {array|string|
|
|
197
|
+
* @param {array|string|false} validateUndefined - ignore all required fields during insert, or
|
|
198
198
|
* undefined subdocument required fields that have a defined parent/grandparent during update
|
|
199
199
|
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
200
200
|
* @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
|
|
@@ -215,7 +215,7 @@ module.exports = {
|
|
|
215
215
|
data = opts.data = opts.data || (opts.req? opts.req.body : null)
|
|
216
216
|
operators = util.pluck(opts, [/^\$/])
|
|
217
217
|
// Operation options
|
|
218
|
-
options = util.omit(opts, ['data', 'query', 'respond', '
|
|
218
|
+
options = util.omit(opts, ['data', 'query', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'])
|
|
219
219
|
options.sort = options.sort || { 'createdAt': -1 }
|
|
220
220
|
options.limit = parseInt(options.limit || 0)
|
|
221
221
|
// Sort string passed
|
package/lib/model-validate.js
CHANGED
|
@@ -12,7 +12,7 @@ module.exports = {
|
|
|
12
12
|
* @param {boolean(false)} update - are we validating for insert or update?
|
|
13
13
|
* @param {array|string|false} blacklist - augment schema blacklist, `false` will remove all blacklisting
|
|
14
14
|
* @param {array|string} projection - only return these fields, ignores blacklist
|
|
15
|
-
* @param {array|string|
|
|
15
|
+
* @param {array|string|false} validateUndefined - ignore all required fields during insert, or undefined
|
|
16
16
|
* subdocument required fields that have a defined parent/grandparent during update
|
|
17
17
|
* @param {array|string|true} skipValidation - skip validation on these fields
|
|
18
18
|
* @param {boolean} timestamps - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
|
|
@@ -177,7 +177,11 @@ module.exports = {
|
|
|
177
177
|
// Object schema errors
|
|
178
178
|
errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
|
|
179
179
|
// Recurse if data value is a subdocument, or when inserting, or when updating deep properties (non-root)
|
|
180
|
-
if (
|
|
180
|
+
if (
|
|
181
|
+
util.isObject(value) ||
|
|
182
|
+
opts.insert ||
|
|
183
|
+
((path2||'').match(/\./) && (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : true))
|
|
184
|
+
) {
|
|
181
185
|
var res = this._validateFields(dataRoot, field, value, opts, path2)
|
|
182
186
|
errors.push(...res[0])
|
|
183
187
|
}
|
|
@@ -258,17 +262,17 @@ module.exports = {
|
|
|
258
262
|
let fieldName = path.match(/[^.]+$/)[0]
|
|
259
263
|
let ruleMessageKey = this._getMostSpecificKeyMatchingPath(this.messages, path)
|
|
260
264
|
let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
|
|
261
|
-
let
|
|
265
|
+
let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : rule.validateUndefined
|
|
262
266
|
if (!ruleMessage) ruleMessage = rule.message
|
|
263
267
|
|
|
264
268
|
// Ignore undefined (if updated root property, or ignoring)
|
|
265
|
-
if ((
|
|
269
|
+
if ((!validateUndefined || (opts.update && !path.match(/\./))) && typeof value === 'undefined') return
|
|
266
270
|
|
|
267
271
|
// Ignore null (if nullObject is set on objects or arrays) (todo: change to ignoreNull)
|
|
268
272
|
if (field.nullObject && (field.isObject || field.isArray) && value === null) return
|
|
269
273
|
|
|
270
274
|
// Ignore empty strings
|
|
271
|
-
if (rule.
|
|
275
|
+
if (!rule.validateEmptyString && value === '') return
|
|
272
276
|
|
|
273
277
|
// Rule failed
|
|
274
278
|
if (!rule.fn.call(dataRoot, value, ruleArg, path, this)) return {
|
package/lib/model.js
CHANGED
|
@@ -61,7 +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.
|
|
64
|
+
if (typeof formattedRule.validateEmptyString == 'undefined') formattedRule.validateEmptyString = true
|
|
65
65
|
this.rules[ruleName] = formattedRule
|
|
66
66
|
}
|
|
67
67
|
}, this)
|
|
@@ -242,11 +242,10 @@ Model.prototype._setupFieldsAndWhitelists = function(fields, path) {
|
|
|
242
242
|
this.findBLProject = this.findBL.reduce((o, v) => { (o[v] = 0); return o }, {})
|
|
243
243
|
},
|
|
244
244
|
|
|
245
|
-
Model.prototype._setupIndexes = function(fields) {
|
|
245
|
+
Model.prototype._setupIndexes = function(fields, opts={}) {
|
|
246
246
|
/**
|
|
247
|
-
* Creates indexes for the model
|
|
247
|
+
* Creates indexes for the model (multikey, and sub-document supported)
|
|
248
248
|
* Note: only one text index per model(collection) is allowed due to mongodb limitations
|
|
249
|
-
* Note: we and currently don't support indexes on sub-collections, but sub-documents yes!
|
|
250
249
|
* @link https://docs.mongodb.com/manual/reference/command/createIndexes/
|
|
251
250
|
* @link https://mongodb.github.io/node-mongodb-native/2.1/api/Collection.html#createIndexes
|
|
252
251
|
* @param {object} <fields>
|
|
@@ -279,6 +278,7 @@ Model.prototype._setupIndexes = function(fields) {
|
|
|
279
278
|
recurseFields(fields || model.fields, '')
|
|
280
279
|
// console.log(2, indexes, fields)
|
|
281
280
|
if (hasTextIndex) indexes.push(textIndex)
|
|
281
|
+
if (opts.dryRun) return Promise.resolve(indexes || [])
|
|
282
282
|
if (!indexes.length) return Promise.resolve([]) // No indexes defined
|
|
283
283
|
|
|
284
284
|
// Create indexes
|
|
@@ -331,22 +331,24 @@ Model.prototype._setupIndexes = function(fields) {
|
|
|
331
331
|
util.forEach(fields, (field, name) => {
|
|
332
332
|
let index = field.index
|
|
333
333
|
if (index) {
|
|
334
|
-
let path = name == 'schema'? parentPath.slice(0, -1) : parentPath + name
|
|
335
334
|
let options = util.isObject(index)? util.omit(index, ['type']) : {}
|
|
336
335
|
let type = util.isObject(index)? index.type : index
|
|
336
|
+
let path = name == 'schema'? parentPath.slice(0, -1) : parentPath + name
|
|
337
|
+
let path2 = path.replace(/(^|\.)[0-9]+(\.|$)/g, '$2') // no numirical keys, e.g. pets.1.name
|
|
337
338
|
if (type === true) type = 1
|
|
338
|
-
|
|
339
339
|
if (type == 'text') {
|
|
340
|
-
hasTextIndex = textIndex.key[
|
|
340
|
+
hasTextIndex = textIndex.key[path2] = 'text'
|
|
341
341
|
Object.assign(textIndex, options)
|
|
342
342
|
} else if (type == '1' || type == '-1' || type == '2dsphere') {
|
|
343
|
-
indexes.push({ name: `${
|
|
343
|
+
indexes.push({ name: `${path2}_${type}`, key: { [path2]: type }, ...options })
|
|
344
344
|
} else if (type == 'unique') {
|
|
345
|
-
indexes.push({ name: `${
|
|
345
|
+
indexes.push({ name: `${path2}_1`, key: { [path2]: 1 }, unique: true, ...options })
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
if (util.isSubdocument(field)) {
|
|
349
349
|
recurseFields(field, parentPath + name + '.')
|
|
350
|
+
} else if (util.isArray(field)) {
|
|
351
|
+
recurseFields(field, parentPath + name + '.')
|
|
350
352
|
}
|
|
351
353
|
})
|
|
352
354
|
}
|
package/lib/rules.js
CHANGED
|
@@ -5,7 +5,8 @@ let validator = require('validator')
|
|
|
5
5
|
module.exports = {
|
|
6
6
|
|
|
7
7
|
required: {
|
|
8
|
-
|
|
8
|
+
validateUndefined: true,
|
|
9
|
+
validateEmptyString: true,
|
|
9
10
|
message: 'This field is required.',
|
|
10
11
|
fn: function(x) {
|
|
11
12
|
if (util.isArray(x) && !x.length) return false
|
|
@@ -16,7 +17,7 @@ module.exports = {
|
|
|
16
17
|
// Type rules below ignore undefined (default for custom model rules)
|
|
17
18
|
|
|
18
19
|
'isBoolean': {
|
|
19
|
-
|
|
20
|
+
validateEmptyString: true,
|
|
20
21
|
message: 'Value was not a boolean.',
|
|
21
22
|
tryParse: function(x) {
|
|
22
23
|
if (typeof x === 'string' && x === 'true') return true
|
|
@@ -28,7 +29,7 @@ module.exports = {
|
|
|
28
29
|
}
|
|
29
30
|
},
|
|
30
31
|
'isArray': {
|
|
31
|
-
|
|
32
|
+
validateEmptyString: true,
|
|
32
33
|
message: 'Value was not an array.',
|
|
33
34
|
tryParse: function(x) {
|
|
34
35
|
if (x === '') return null
|
|
@@ -41,7 +42,7 @@ module.exports = {
|
|
|
41
42
|
}
|
|
42
43
|
},
|
|
43
44
|
'isDate': {
|
|
44
|
-
|
|
45
|
+
validateEmptyString: true,
|
|
45
46
|
message: 'Value was not a unix timestamp.',
|
|
46
47
|
tryParse: function(x) {
|
|
47
48
|
if (util.isString(x) && x.match(/^[+-]?[0-9]+$/)) return x // keep string nums intact
|
|
@@ -53,7 +54,7 @@ module.exports = {
|
|
|
53
54
|
}
|
|
54
55
|
},
|
|
55
56
|
'isImageObject': {
|
|
56
|
-
|
|
57
|
+
validateEmptyString: true,
|
|
57
58
|
message: 'Invalid image value',
|
|
58
59
|
messageLong: 'Image fields need to either be null, undefined, file, or an object containing the following '
|
|
59
60
|
+ 'fields \'{ bucket, date, filename, filesize, path, uid }\'',
|
|
@@ -69,7 +70,7 @@ module.exports = {
|
|
|
69
70
|
}
|
|
70
71
|
},
|
|
71
72
|
'isInteger': {
|
|
72
|
-
|
|
73
|
+
validateEmptyString: true,
|
|
73
74
|
message: 'Value was not an integer.',
|
|
74
75
|
tryParse: function(x) {
|
|
75
76
|
if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
|
|
@@ -81,7 +82,7 @@ module.exports = {
|
|
|
81
82
|
}
|
|
82
83
|
},
|
|
83
84
|
'isNumber': {
|
|
84
|
-
|
|
85
|
+
validateEmptyString: true,
|
|
85
86
|
message: 'Value was not a number.',
|
|
86
87
|
tryParse: function(x) {
|
|
87
88
|
if (util.isString(x) && x.match(/^[+-][0-9]+$/)) return x // keep string nums intact
|
|
@@ -93,7 +94,7 @@ module.exports = {
|
|
|
93
94
|
}
|
|
94
95
|
},
|
|
95
96
|
'isObject': {
|
|
96
|
-
|
|
97
|
+
validateEmptyString: true,
|
|
97
98
|
message: 'Value was not an object.',
|
|
98
99
|
tryParse: function(x) {
|
|
99
100
|
if (x === '') return null
|
|
@@ -106,21 +107,21 @@ module.exports = {
|
|
|
106
107
|
}
|
|
107
108
|
},
|
|
108
109
|
'isString': {
|
|
109
|
-
|
|
110
|
+
validateEmptyString: true,
|
|
110
111
|
message: 'Value was not a string.',
|
|
111
112
|
fn: function(x) {
|
|
112
113
|
return typeof x === 'string'
|
|
113
114
|
}
|
|
114
115
|
},
|
|
115
116
|
'isAny': {
|
|
116
|
-
|
|
117
|
+
validateEmptyString: true,
|
|
117
118
|
message: '',
|
|
118
119
|
fn: function(x) {
|
|
119
120
|
return true
|
|
120
121
|
}
|
|
121
122
|
},
|
|
122
123
|
'isId': {
|
|
123
|
-
|
|
124
|
+
validateEmptyString: true,
|
|
124
125
|
message: 'Value was not a valid ObjectId.',
|
|
125
126
|
tryParse: function(x) {
|
|
126
127
|
// Try and parse value to a mongodb ObjectId
|
|
@@ -135,7 +136,7 @@ module.exports = {
|
|
|
135
136
|
}
|
|
136
137
|
},
|
|
137
138
|
'max': {
|
|
138
|
-
|
|
139
|
+
validateEmptyString: true,
|
|
139
140
|
message: (x, arg) => 'Value was greater than the configured maximum (' + arg + ')',
|
|
140
141
|
fn: function(x, arg) {
|
|
141
142
|
if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
|
|
@@ -143,7 +144,7 @@ module.exports = {
|
|
|
143
144
|
}
|
|
144
145
|
},
|
|
145
146
|
'min': {
|
|
146
|
-
|
|
147
|
+
validateEmptyString: true,
|
|
147
148
|
message: (x, arg) => 'Value was less than the configured minimum (' + arg + ')',
|
|
148
149
|
fn: function(x, arg) {
|
|
149
150
|
if (typeof x !== 'number') { throw new Error ('Value was not a number.') }
|
|
@@ -151,19 +152,17 @@ module.exports = {
|
|
|
151
152
|
}
|
|
152
153
|
},
|
|
153
154
|
'isNotEmptyString': {
|
|
154
|
-
|
|
155
|
+
validateEmptyString: true,
|
|
155
156
|
message: 'Value was an empty string.',
|
|
156
157
|
fn: function(x) {
|
|
157
158
|
return x !== ''
|
|
158
159
|
}
|
|
159
160
|
},
|
|
160
161
|
|
|
161
|
-
// Rules below ignore undefined & empty strings
|
|
162
|
+
// Rules below ignore undefined, & empty strings
|
|
162
163
|
// (e.g. an empty email field can be saved that isn't required)
|
|
163
164
|
|
|
164
165
|
'enum': {
|
|
165
|
-
ignoreEmptyString: true,
|
|
166
|
-
ignoreUndefined: true,
|
|
167
166
|
message: (x, arg) => 'Invalid enum value',
|
|
168
167
|
fn: function(x, arg) {
|
|
169
168
|
for (let item of arg) {
|
|
@@ -176,68 +175,46 @@ module.exports = {
|
|
|
176
175
|
// fn: function(x, arg) { return !x }
|
|
177
176
|
// },
|
|
178
177
|
'isAfter': {
|
|
179
|
-
ignoreEmptyString: true,
|
|
180
|
-
ignoreUndefined: true,
|
|
181
178
|
message: (x, arg) => 'Value was before the configured time (' + arg + ')',
|
|
182
179
|
fn: function(x, arg) { return validator.isAfter(x, arg) }
|
|
183
180
|
},
|
|
184
181
|
'isBefore': {
|
|
185
|
-
ignoreEmptyString: true,
|
|
186
|
-
ignoreUndefined: true,
|
|
187
182
|
message: (x, arg) => 'Value was after the configured time (' + arg + ')',
|
|
188
183
|
fn: function(x, arg) { return validator.isBefore(x, arg) }
|
|
189
184
|
},
|
|
190
185
|
'isCreditCard': {
|
|
191
|
-
ignoreEmptyString: true,
|
|
192
|
-
ignoreUndefined: true,
|
|
193
186
|
message: 'Value was not a valid credit card.',
|
|
194
187
|
fn: function(x, arg) { return validator.isCreditCard(x, arg) }
|
|
195
188
|
},
|
|
196
189
|
'isEmail': {
|
|
197
|
-
ignoreEmptyString: true,
|
|
198
|
-
ignoreUndefined: true,
|
|
199
190
|
message: 'Please enter a valid email address.',
|
|
200
191
|
fn: function(x, arg) { return validator.isEmail(x, arg) }
|
|
201
192
|
},
|
|
202
193
|
'isHexColor': {
|
|
203
|
-
ignoreEmptyString: true,
|
|
204
|
-
ignoreUndefined: true,
|
|
205
194
|
message: 'Value was not a valid hex color.',
|
|
206
195
|
fn: function(x, arg) { return validator.isHexColor(x, arg) }
|
|
207
196
|
},
|
|
208
197
|
'isIn': {
|
|
209
|
-
ignoreEmptyString: true,
|
|
210
|
-
ignoreUndefined: true,
|
|
211
198
|
message: (x, arg) => 'Value was not in the configured whitelist (' + arg.join(', ') + ')',
|
|
212
199
|
fn: function(x, arg) { return validator.isIn(x, arg) }
|
|
213
200
|
},
|
|
214
201
|
'isIP': {
|
|
215
|
-
ignoreEmptyString: true,
|
|
216
|
-
ignoreUndefined: true,
|
|
217
202
|
message: 'Value was not a valid IP address.',
|
|
218
203
|
fn: function(x, arg) { return validator.isIP(x, arg) }
|
|
219
204
|
},
|
|
220
205
|
'isNotIn': {
|
|
221
|
-
ignoreEmptyString: true,
|
|
222
|
-
ignoreUndefined: true,
|
|
223
206
|
message: (x, arg) => 'Value was in the configured blacklist (' + arg.join(', ') + ')',
|
|
224
207
|
fn: function(x, arg) { return !validator.isIn(x, arg) }
|
|
225
208
|
},
|
|
226
209
|
'isURL': {
|
|
227
|
-
ignoreEmptyString: true,
|
|
228
|
-
ignoreUndefined: true,
|
|
229
210
|
message: 'Value was not a valid URL.',
|
|
230
211
|
fn: function(x, arg) { return validator.isURL(x, arg === true? undefined : arg) }
|
|
231
212
|
},
|
|
232
213
|
'isUUID': {
|
|
233
|
-
ignoreEmptyString: true,
|
|
234
|
-
ignoreUndefined: true,
|
|
235
214
|
message: 'Value was not a valid UUID.',
|
|
236
215
|
fn: function(x, arg) { return validator.isUUID(x) }
|
|
237
216
|
},
|
|
238
217
|
'minLength': {
|
|
239
|
-
ignoreEmptyString: true,
|
|
240
|
-
ignoreUndefined: true,
|
|
241
218
|
message: function(x, arg) {
|
|
242
219
|
if (typeof x === 'string') return 'Value needs to be at least ' + arg + ' characters long.'
|
|
243
220
|
else return 'Value needs to contain a minimum of ' + arg + ' items.'
|
|
@@ -249,8 +226,6 @@ module.exports = {
|
|
|
249
226
|
}
|
|
250
227
|
},
|
|
251
228
|
'maxLength': {
|
|
252
|
-
ignoreEmptyString: true,
|
|
253
|
-
ignoreUndefined: true,
|
|
254
229
|
message: function(x, arg) {
|
|
255
230
|
if (typeof x === 'string') return 'Value was longer than the configured maximum length (' + arg + ')'
|
|
256
231
|
else return 'Value cannot contain more than ' + arg + ' items.'
|
|
@@ -262,8 +237,6 @@ module.exports = {
|
|
|
262
237
|
}
|
|
263
238
|
},
|
|
264
239
|
'regex': {
|
|
265
|
-
ignoreEmptyString: true,
|
|
266
|
-
ignoreUndefined: true,
|
|
267
240
|
message: (x, arg) => 'Value did not match the configured regular expression (' + arg + ')',
|
|
268
241
|
fn: function(x, arg) {
|
|
269
242
|
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.
|
|
5
|
+
"version": "1.30.3",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:boycce/monastery",
|
|
8
8
|
"homepage": "https://boycce.github.io/monastery/",
|
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/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/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
|
@@ -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 : '' }, { validateUndefined: false })).rejects.toContainEqual({
|
|
23
23
|
status: '400',
|
|
24
24
|
title: 'name',
|
|
25
25
|
detail: 'This field is required.',
|
|
@@ -27,8 +27,8 @@ module.exports = function(monastery, opendb) {
|
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
// Required error (insert, and with ignoreRequired)
|
|
30
|
-
await expect(user.validate({}, {
|
|
31
|
-
await expect(user.validate({}, {
|
|
30
|
+
await expect(user.validate({}, { validateUndefined: false })).resolves.toEqual({})
|
|
31
|
+
await expect(user.validate({}, { validateUndefined: false, update: true })).resolves.toEqual({})
|
|
32
32
|
|
|
33
33
|
// No required error (update)
|
|
34
34
|
await expect(user.validate({}, { update: true })).resolves.toEqual({})
|
|
@@ -87,77 +87,120 @@ module.exports = function(monastery, opendb) {
|
|
|
87
87
|
let db = (await opendb(false)).db
|
|
88
88
|
let user = db.model('user', { fields: {
|
|
89
89
|
animals: {
|
|
90
|
-
cat: { type: 'string', required: true },
|
|
90
|
+
cat: { type: 'string', required: true }, // {} = required on insert
|
|
91
91
|
dog: {
|
|
92
92
|
name: { type: 'string' },
|
|
93
|
-
color: { type: 'string', required: true }
|
|
93
|
+
color: { type: 'string', required: true } // {} = required on insert
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}})
|
|
97
97
|
|
|
98
|
-
//
|
|
99
|
-
await expect(user.validate({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
// Insert: Required subdocument properties
|
|
99
|
+
await expect(user.validate({})).rejects.toEqual(
|
|
100
|
+
expect.arrayContaining([
|
|
101
|
+
expect.objectContaining({
|
|
102
|
+
status: '400',
|
|
103
|
+
title: 'animals.cat',
|
|
104
|
+
detail: 'This field is required.',
|
|
105
|
+
meta: { rule: 'required', model: 'user', field: 'cat' }
|
|
106
|
+
}),
|
|
107
|
+
expect.objectContaining({
|
|
108
|
+
status: '400',
|
|
109
|
+
title: 'animals.dog.color',
|
|
110
|
+
detail: 'This field is required.',
|
|
111
|
+
meta: { rule: 'required', model: 'user', field: 'color' }
|
|
112
|
+
}),
|
|
113
|
+
])
|
|
114
|
+
)
|
|
105
115
|
|
|
106
|
-
// Required subdocument
|
|
107
|
-
await expect(user.validate({})).rejects.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
// Insert: Required subdocument properties
|
|
117
|
+
await expect(user.validate({ animals: {} })).rejects.toEqual(
|
|
118
|
+
expect.arrayContaining([
|
|
119
|
+
expect.objectContaining({
|
|
120
|
+
status: '400',
|
|
121
|
+
title: 'animals.cat',
|
|
122
|
+
detail: 'This field is required.',
|
|
123
|
+
meta: { rule: 'required', model: 'user', field: 'cat' }
|
|
124
|
+
}),
|
|
125
|
+
expect.objectContaining({
|
|
126
|
+
status: '400',
|
|
127
|
+
title: 'animals.dog.color',
|
|
128
|
+
detail: 'This field is required.',
|
|
129
|
+
meta: { rule: 'required', model: 'user', field: 'color' }
|
|
130
|
+
}),
|
|
131
|
+
])
|
|
132
|
+
)
|
|
113
133
|
|
|
114
|
-
//
|
|
115
|
-
await expect(user.validate({})).rejects.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
// Insert: Invalid subdocument type
|
|
135
|
+
await expect(user.validate({ animals: { dog: 1 }})).rejects.toEqual(
|
|
136
|
+
expect.arrayContaining([
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
status: '400',
|
|
139
|
+
title: 'animals.cat',
|
|
140
|
+
detail: 'This field is required.',
|
|
141
|
+
meta: { rule: 'required', model: 'user', field: 'cat' }
|
|
142
|
+
}),
|
|
143
|
+
expect.objectContaining({
|
|
144
|
+
status: '400',
|
|
145
|
+
title: 'animals.dog',
|
|
146
|
+
detail: 'Value was not an object.',
|
|
147
|
+
meta: { rule: 'isObject', model: 'user', field: 'dog' }
|
|
148
|
+
}),
|
|
149
|
+
])
|
|
150
|
+
)
|
|
121
151
|
|
|
122
|
-
//
|
|
123
|
-
await expect(user.validate({ animals: {} }, {
|
|
124
|
-
|
|
125
|
-
title: 'animals.cat',
|
|
126
|
-
detail: 'This field is required.',
|
|
127
|
-
meta: { rule: 'required', model: 'user', field: 'cat' }
|
|
152
|
+
// Insert: Ignore required subdocument property with a defined parent
|
|
153
|
+
await expect(user.validate({ animals: {} }, { validateUndefined: false })).resolves.toEqual({
|
|
154
|
+
animals: {}
|
|
128
155
|
})
|
|
129
156
|
|
|
130
|
-
// Required subdocument property
|
|
131
|
-
await expect(user.validate({ animals: {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
157
|
+
// Update: Required subdocument property when a parent/grandparent is specified
|
|
158
|
+
await expect(user.validate({ animals: {} }, { update: true })).rejects.toEqual(
|
|
159
|
+
expect.arrayContaining([
|
|
160
|
+
expect.objectContaining({
|
|
161
|
+
status: '400',
|
|
162
|
+
title: 'animals.cat',
|
|
163
|
+
detail: 'This field is required.',
|
|
164
|
+
meta: { rule: 'required', model: 'user', field: 'cat' }
|
|
165
|
+
}),
|
|
166
|
+
expect.objectContaining({
|
|
167
|
+
status: '400',
|
|
168
|
+
title: 'animals.dog.color',
|
|
169
|
+
detail: 'This field is required.',
|
|
170
|
+
meta: { rule: 'required', model: 'user', field: 'color' }
|
|
171
|
+
}),
|
|
172
|
+
])
|
|
173
|
+
)
|
|
137
174
|
|
|
138
|
-
// Required subdocument property
|
|
139
|
-
await expect(user.validate({ animals: {} }, { update: true })).rejects.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
175
|
+
// Update: Required subdocument property when a parent is specified
|
|
176
|
+
await expect(user.validate({ animals: { dog: {}} }, { update: true })).rejects.toEqual(
|
|
177
|
+
expect.arrayContaining([
|
|
178
|
+
expect.objectContaining({
|
|
179
|
+
status: '400',
|
|
180
|
+
title: 'animals.cat',
|
|
181
|
+
detail: 'This field is required.',
|
|
182
|
+
meta: { rule: 'required', model: 'user', field: 'cat' }
|
|
183
|
+
}),
|
|
184
|
+
expect.objectContaining({
|
|
185
|
+
status: '400',
|
|
186
|
+
title: 'animals.dog.color',
|
|
187
|
+
detail: 'This field is required.',
|
|
188
|
+
meta: { rule: 'required', model: 'user', field: 'color' }
|
|
189
|
+
}),
|
|
190
|
+
])
|
|
191
|
+
)
|
|
145
192
|
|
|
146
|
-
// Ignore required subdocument property
|
|
193
|
+
// Update: Ignore required subdocument property when root parent is undefined
|
|
147
194
|
await expect(user.validate({}, { update: true })).resolves.toEqual({})
|
|
148
195
|
|
|
149
|
-
// Ignore required subdocument property with a defined parent (update) (not required if ignoreUndefined set)
|
|
150
|
-
await expect(user.validate({ animals: {} }, { ignoreUndefined: true })).resolves.toEqual({
|
|
151
|
-
animals: {}
|
|
152
|
-
})
|
|
153
196
|
|
|
154
|
-
// Ignore required subdocument property with a defined parent
|
|
155
|
-
await expect(user.validate({ animals: {} }, { update: true,
|
|
197
|
+
// Update: Ignore required subdocument property with a defined parent when validateUndefined = false
|
|
198
|
+
await expect(user.validate({ animals: {} }, { update: true, validateUndefined: false })).resolves.toEqual({
|
|
156
199
|
animals: {}
|
|
157
200
|
})
|
|
158
201
|
|
|
159
|
-
// Required subdocument property
|
|
160
|
-
await expect(user.validate({ animals: { cat: '' }}, { update: true,
|
|
202
|
+
// Update: Required defined subdocument property when validateUndefined = false
|
|
203
|
+
await expect(user.validate({ animals: { cat: '' }}, { update: true, validateUndefined: false }))
|
|
161
204
|
.rejects.toContainEqual({
|
|
162
205
|
status: '400',
|
|
163
206
|
title: 'animals.cat',
|
|
@@ -215,17 +258,17 @@ module.exports = function(monastery, opendb) {
|
|
|
215
258
|
await expect(user.validate({ animals: { dogs: [] }}))
|
|
216
259
|
.resolves.toEqual({ animals: { dogs: [] }})
|
|
217
260
|
|
|
218
|
-
// No undefined item errors with
|
|
261
|
+
// No undefined item errors with validateUndefined=false
|
|
219
262
|
await expect(user.validate(
|
|
220
263
|
{ animals: { dogs: [{ name: 'sparky' }] }},
|
|
221
|
-
{ update: true,
|
|
264
|
+
{ update: true, validateUndefined: false }
|
|
222
265
|
))
|
|
223
266
|
.resolves.toEqual({ animals: { dogs: [{ name: 'sparky' }] }})
|
|
224
267
|
|
|
225
|
-
// Requried error within an array subdocument (even during update when parent defined &&
|
|
268
|
+
// Requried error within an array subdocument (even during update when parent defined && validateUndefined = false)
|
|
226
269
|
await expect(user.validate(
|
|
227
270
|
{ animals: { dogs: [{ name: 'sparky', color: '' }] }},
|
|
228
|
-
{ update: true,
|
|
271
|
+
{ update: true, validateUndefined: false }
|
|
229
272
|
))
|
|
230
273
|
.rejects.toContainEqual(error)
|
|
231
274
|
})
|
|
@@ -497,7 +540,7 @@ module.exports = function(monastery, opendb) {
|
|
|
497
540
|
},
|
|
498
541
|
rules: {
|
|
499
542
|
requiredIfNoName: {
|
|
500
|
-
|
|
543
|
+
validateUndefined: true,
|
|
501
544
|
fn: function(value, ruleArg) {
|
|
502
545
|
return value || this.name
|
|
503
546
|
}
|
|
@@ -547,7 +590,7 @@ module.exports = function(monastery, opendb) {
|
|
|
547
590
|
'title': 'age'
|
|
548
591
|
}
|
|
549
592
|
])
|
|
550
|
-
await expect(user2.validate({ }, {
|
|
593
|
+
await expect(user2.validate({ }, { validateUndefined: false })).resolves.toEqual({})
|
|
551
594
|
|
|
552
595
|
// Required rule based off another field (update)
|
|
553
596
|
await expect(user2.validate({ }, { update: true })).resolves.toEqual({})
|