monastery 1.31.4 → 1.31.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +15 -0
- package/docs/schema.md +18 -0
- package/lib/model-crud.js +4 -4
- package/lib/model-validate.js +9 -8
- package/package.json +2 -2
- package/test/model.js +49 -0
- package/test/validate.js +104 -9
package/changelog.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [1.31.6](https://github.com/boycce/monastery/compare/1.31.5...1.31.6) (2022-02-25)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* added partial unqiue index tests ([ff6f193](https://github.com/boycce/monastery/commit/ff6f1938e333407ee17895873d2b42fa5263d7e3))
|
|
11
|
+
* scripts ([bc32680](https://github.com/boycce/monastery/commit/bc326809098ae24686158a0386fbbd6671d86c98))
|
|
12
|
+
|
|
13
|
+
### [1.31.5](https://github.com/boycce/monastery/compare/1.31.4...1.31.5) (2022-02-15)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* scripts ([417ba13](https://github.com/boycce/monastery/commit/417ba13c1a0862f76fadf97d6d6d063a74e196bd))
|
|
19
|
+
|
|
5
20
|
### 1.31.4 (2022-02-15)
|
|
6
21
|
|
|
7
22
|
|
package/docs/schema.md
CHANGED
|
@@ -166,6 +166,24 @@ await db.user.insert({
|
|
|
166
166
|
}
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
+
Since unique indexes by default don't allow mutliple documents with `null`, you use a partial index (less performant), e.g.
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
|
|
173
|
+
schema.fields = {
|
|
174
|
+
index: {
|
|
175
|
+
name: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
index: {
|
|
178
|
+
type: 'unique',
|
|
179
|
+
partialFilterExpression: {
|
|
180
|
+
email: { $type: 'string' }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
169
187
|
### Custom validation rules
|
|
170
188
|
|
|
171
189
|
You are able to define custom validation rules to use. (`this` will refer to the data object passed in)
|
package/lib/model-crud.js
CHANGED
|
@@ -9,8 +9,8 @@ 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|false} validateUndefined -
|
|
13
|
-
*
|
|
12
|
+
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
|
|
13
|
+
* default, but false on 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
|
|
16
16
|
* @param {any} <opts.any> - any mongodb option
|
|
@@ -198,8 +198,8 @@ module.exports = {
|
|
|
198
198
|
* @param {object} <opts.query> - mongodb query object
|
|
199
199
|
* @param {object|array} <opts.data> - mongodb document update object(s)
|
|
200
200
|
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
201
|
-
* @param {array|string|false} validateUndefined -
|
|
202
|
-
*
|
|
201
|
+
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
|
|
202
|
+
* default, but false on update
|
|
203
203
|
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
204
204
|
* @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
|
|
205
205
|
* @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove all blacklisting
|
package/lib/model-validate.js
CHANGED
|
@@ -12,8 +12,8 @@ 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|false} validateUndefined -
|
|
16
|
-
*
|
|
15
|
+
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
|
|
16
|
+
* default, but false on 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
|
|
19
19
|
* updated, depending on the `options.update` value
|
|
@@ -176,11 +176,11 @@ module.exports = {
|
|
|
176
176
|
} else if (util.isSubdocument(field)) {
|
|
177
177
|
// Object schema errors
|
|
178
178
|
errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
|
|
179
|
-
// Recurse if
|
|
179
|
+
// Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
|
|
180
180
|
if (
|
|
181
|
-
util.isObject(value) ||
|
|
182
181
|
opts.insert ||
|
|
183
|
-
|
|
182
|
+
util.isObject(value) ||
|
|
183
|
+
(util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path2||'').match(/\./))
|
|
184
184
|
) {
|
|
185
185
|
var res = this._validateFields(dataRoot, field, value, opts, path2)
|
|
186
186
|
errors.push(...res[0])
|
|
@@ -260,13 +260,14 @@ module.exports = {
|
|
|
260
260
|
ruleArg = ruleArg === true? undefined : ruleArg
|
|
261
261
|
let rule = this.rules[ruleName] || rules[ruleName]
|
|
262
262
|
let fieldName = path.match(/[^.]+$/)[0]
|
|
263
|
+
let isDeepProp = path.match(/\./) // todo: not dot-notation
|
|
263
264
|
let ruleMessageKey = this._getMostSpecificKeyMatchingPath(this.messages, path)
|
|
264
265
|
let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
|
|
265
|
-
let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined :
|
|
266
|
+
let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : opts.insert || isDeepProp
|
|
266
267
|
if (!ruleMessage) ruleMessage = rule.message
|
|
267
268
|
|
|
268
|
-
//
|
|
269
|
-
if (typeof value === 'undefined' && (!validateUndefined
|
|
269
|
+
// Undefined value
|
|
270
|
+
if (typeof value === 'undefined' && (!validateUndefined || !rule.validateUndefined)) return
|
|
270
271
|
|
|
271
272
|
// Ignore null (if nullObject is set on objects or arrays)
|
|
272
273
|
if (value === null && (field.isObject || field.isArray) && field.nullObject) return
|
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.31.
|
|
5
|
+
"version": "1.31.6",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:boycce/monastery",
|
|
8
8
|
"homepage": "https://boycce.github.io/monastery/",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"mong": "nodemon resources/mong.js",
|
|
24
24
|
"minor": "standard-version --release-as minor && npm publish",
|
|
25
25
|
"patch": "standard-version --release-as patch && npm publish",
|
|
26
|
-
"release": "standard-version && npm publish",
|
|
26
|
+
"release": "standard-version && npm publish && git push --tags",
|
|
27
27
|
"test": "npm run lint && jest",
|
|
28
28
|
"test-one-example": "jest -t images"
|
|
29
29
|
},
|
package/test/model.js
CHANGED
|
@@ -189,6 +189,55 @@ module.exports = function(monastery, opendb) {
|
|
|
189
189
|
db.close()
|
|
190
190
|
})
|
|
191
191
|
|
|
192
|
+
test('model unique indexes', async () => {
|
|
193
|
+
let db = (await opendb(null)).db
|
|
194
|
+
// Setup: Drop previously tested collections
|
|
195
|
+
if ((await db._db.listCollections().toArray()).find(o => o.name == 'userUniqueIndex')) {
|
|
196
|
+
await db._db.collection('userUniqueIndex').drop()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Partial unique indexes (allows mulitple null values)
|
|
200
|
+
await db.model('userUniqueIndex', {
|
|
201
|
+
waitForIndexes: true,
|
|
202
|
+
fields: {
|
|
203
|
+
email: {
|
|
204
|
+
type: 'string',
|
|
205
|
+
index: {
|
|
206
|
+
type: 'unique',
|
|
207
|
+
partialFilterExpression: {
|
|
208
|
+
email: { $type: 'string' }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
let indexes2 = await db._db.collection('userUniqueIndex').indexes()
|
|
216
|
+
expect(indexes2[0]).toMatchObject({ v: 2, key: { _id: 1 }, name: '_id_' })
|
|
217
|
+
expect(indexes2[1]).toMatchObject({ v: 2, unique: true, key: { email: 1 }, name: 'email_1' })
|
|
218
|
+
|
|
219
|
+
await expect(db.userUniqueIndex.insert({ data: { 'email': 'ricky@orchid.co.nz' }})).resolves.toEqual({
|
|
220
|
+
_id: expect.any(Object),
|
|
221
|
+
email: 'ricky@orchid.co.nz'
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
await expect(db.userUniqueIndex.insert({ data: { 'email': 'ricky@orchid.co.nz' }})).rejects.toThrow(
|
|
225
|
+
/E11000 duplicate key error collection: monastery.userUniqueIndex index: email_1 dup key: {/
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
await expect(db.userUniqueIndex.insert({ data: { 'email': null }})).resolves.toEqual({
|
|
229
|
+
_id: expect.any(Object),
|
|
230
|
+
email: null
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
await expect(db.userUniqueIndex.insert({ data: { 'email': null }})).resolves.toEqual({
|
|
234
|
+
_id: expect.any(Object),
|
|
235
|
+
email: null
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
db.close()
|
|
239
|
+
})
|
|
240
|
+
|
|
192
241
|
test('model subdocument indexes', async () => {
|
|
193
242
|
// Setup: Need to test different types of indexes
|
|
194
243
|
let db = (await opendb(null)).db
|
package/test/validate.js
CHANGED
|
@@ -26,17 +26,18 @@ module.exports = function(monastery, opendb) {
|
|
|
26
26
|
meta: { rule: 'required', model: 'user', field: 'name' }
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
// Required error (insert, and with ignoreRequired)
|
|
30
|
-
await expect(user.validate({}, { validateUndefined: false })).resolves.toEqual({})
|
|
31
|
-
await expect(user.validate({}, { validateUndefined: false, update: true })).resolves.toEqual({})
|
|
32
|
-
|
|
33
29
|
// No required error (update)
|
|
34
30
|
await expect(user.validate({}, { update: true })).resolves.toEqual({})
|
|
35
31
|
|
|
36
32
|
// Type error (string)
|
|
37
33
|
await expect(user.validate({ name: 1 })).resolves.toEqual({ name: '1' })
|
|
38
34
|
await expect(user.validate({ name: 1.123 })).resolves.toEqual({ name: '1.123' })
|
|
39
|
-
await expect(user.validate({ name: undefined }
|
|
35
|
+
await expect(user.validate({ name: undefined })).rejects.toContainEqual({
|
|
36
|
+
status: '400',
|
|
37
|
+
title: 'name',
|
|
38
|
+
detail: 'This field is required.',
|
|
39
|
+
meta: { rule: 'required', model: 'user', field: 'name' }
|
|
40
|
+
})
|
|
40
41
|
await expect(user.validate({ name: null })).rejects.toContainEqual({
|
|
41
42
|
status: '400',
|
|
42
43
|
title: 'name',
|
|
@@ -65,20 +66,19 @@ module.exports = function(monastery, opendb) {
|
|
|
65
66
|
await expect(usernum.validate({ amount: 0 })).resolves.toEqual({ amount: 0 })
|
|
66
67
|
await expect(usernum.validate({ amount: '0' })).resolves.toEqual({ amount: 0 })
|
|
67
68
|
await expect(usernum2.validate({ amount: '' })).resolves.toEqual({ amount: null })
|
|
68
|
-
await expect(usernum.validate({ amount: undefined }, { validateUndefined: false })).resolves.toEqual({})
|
|
69
69
|
await expect(usernum.validate({ amount: false })).rejects.toEqual([{
|
|
70
70
|
status: '400',
|
|
71
71
|
title: 'amount',
|
|
72
72
|
detail: 'Value was not a number.',
|
|
73
73
|
meta: { rule: 'isNumber', model: 'usernum', field: 'amount' }
|
|
74
74
|
}])
|
|
75
|
-
await expect(usernum.validate({ amount:
|
|
75
|
+
await expect(usernum.validate({ amount: undefined })).rejects.toEqual([{
|
|
76
76
|
status: '400',
|
|
77
77
|
title: 'amount',
|
|
78
78
|
detail: 'This field is required.',
|
|
79
79
|
meta: { rule: 'required', model: 'usernum', field: 'amount' },
|
|
80
80
|
}])
|
|
81
|
-
await expect(usernum.validate({ amount: null }
|
|
81
|
+
await expect(usernum.validate({ amount: null })).rejects.toEqual([{
|
|
82
82
|
status: '400',
|
|
83
83
|
title: 'amount',
|
|
84
84
|
detail: 'This field is required.',
|
|
@@ -309,6 +309,41 @@ module.exports = function(monastery, opendb) {
|
|
|
309
309
|
.rejects.toContainEqual(error)
|
|
310
310
|
})
|
|
311
311
|
|
|
312
|
+
test('validation array schema errors', async () => {
|
|
313
|
+
// Setup
|
|
314
|
+
let db = (await opendb(false)).db
|
|
315
|
+
function arrayWithSchema(array, schema) {
|
|
316
|
+
array.schema = schema
|
|
317
|
+
return array
|
|
318
|
+
}
|
|
319
|
+
let user = db.model('user', { fields: {
|
|
320
|
+
animals: arrayWithSchema(
|
|
321
|
+
[{ type: 'string' }],
|
|
322
|
+
{ required: true, minLength: 2 },
|
|
323
|
+
)
|
|
324
|
+
}})
|
|
325
|
+
|
|
326
|
+
// MinLength error
|
|
327
|
+
await expect(user.validate({
|
|
328
|
+
animals: [],
|
|
329
|
+
})).rejects.toContainEqual({
|
|
330
|
+
status: '400',
|
|
331
|
+
title: 'animals',
|
|
332
|
+
detail: 'This field is required.',
|
|
333
|
+
meta: { rule: 'required', model: 'user', field: 'animals' }
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
// MinLength error
|
|
337
|
+
await expect(user.validate({
|
|
338
|
+
animals: ['dog'],
|
|
339
|
+
})).rejects.toContainEqual({
|
|
340
|
+
status: '400',
|
|
341
|
+
title: 'animals',
|
|
342
|
+
detail: 'Value needs to contain a minimum of 2 items.',
|
|
343
|
+
meta: { rule: 'minLength', model: 'user', field: 'animals' }
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
312
347
|
test('validation getMostSpecificKeyMatchingPath', async () => {
|
|
313
348
|
let fn = validate._getMostSpecificKeyMatchingPath
|
|
314
349
|
let mock = {
|
|
@@ -870,7 +905,7 @@ module.exports = function(monastery, opendb) {
|
|
|
870
905
|
db.close()
|
|
871
906
|
})
|
|
872
907
|
|
|
873
|
-
test('validation
|
|
908
|
+
test('validation option skipValidation', async () => {
|
|
874
909
|
let db = (await opendb(false)).db
|
|
875
910
|
let user = db.model('user', { fields: {
|
|
876
911
|
name: { type: 'string', required: true }
|
|
@@ -955,6 +990,66 @@ module.exports = function(monastery, opendb) {
|
|
|
955
990
|
})
|
|
956
991
|
})
|
|
957
992
|
|
|
993
|
+
test('validation option validateUndefined', async () => {
|
|
994
|
+
// ValidateUndefined runs required rules on all fields, `true` for insert, `false` for update.
|
|
995
|
+
|
|
996
|
+
// Setup
|
|
997
|
+
let db = (await opendb(false)).db
|
|
998
|
+
let user = db.model('user', { fields: {
|
|
999
|
+
date: { type: 'number' },
|
|
1000
|
+
name: { type: 'string', required: true },
|
|
1001
|
+
}})
|
|
1002
|
+
let usernum = db.model('usernum', { fields: {
|
|
1003
|
+
amount: { type: 'number', required: true }
|
|
1004
|
+
}})
|
|
1005
|
+
let userdeep = db.model('userdeep', { fields: {
|
|
1006
|
+
date: { type: 'number' },
|
|
1007
|
+
name: {
|
|
1008
|
+
first: { type: 'string', required: true },
|
|
1009
|
+
},
|
|
1010
|
+
names: [{
|
|
1011
|
+
first: { type: 'string', required: true },
|
|
1012
|
+
}]
|
|
1013
|
+
}})
|
|
1014
|
+
let errorRequired = {
|
|
1015
|
+
status: '400',
|
|
1016
|
+
title: 'name',
|
|
1017
|
+
detail: 'This field is required.',
|
|
1018
|
+
meta: expect.any(Object),
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Required error for undefined
|
|
1022
|
+
await expect(user.validate({}))
|
|
1023
|
+
.rejects.toEqual([errorRequired])
|
|
1024
|
+
await expect(user.validate({}, { update: true, validateUndefined: true }))
|
|
1025
|
+
.rejects.toEqual([errorRequired])
|
|
1026
|
+
await expect(userdeep.validate({}))
|
|
1027
|
+
.rejects.toEqual([{ ...errorRequired, title: 'name.first' }])
|
|
1028
|
+
await expect(userdeep.validate({ name: {} }, { update: true }))
|
|
1029
|
+
.rejects.toEqual([{ ...errorRequired, title: 'name.first' }])
|
|
1030
|
+
await expect(userdeep.validate({ names: [{}] }, { update: true }))
|
|
1031
|
+
.rejects.toEqual([{ ...errorRequired, title: 'names.0.first' }])
|
|
1032
|
+
|
|
1033
|
+
// Required error for null
|
|
1034
|
+
await expect(user.validate({ name: null }, { update: true }))
|
|
1035
|
+
.rejects.toEqual([errorRequired])
|
|
1036
|
+
await expect(usernum.validate({ amount: null }, { update: true }))
|
|
1037
|
+
.rejects.toEqual([{ ...errorRequired, title: 'amount' }])
|
|
1038
|
+
await expect(user.validate({ name: null }, { update: true, validateUndefined: true }))
|
|
1039
|
+
.rejects.toEqual([errorRequired])
|
|
1040
|
+
|
|
1041
|
+
// Skip required error
|
|
1042
|
+
await expect(user.validate({ name: undefined }, { validateUndefined: false })).resolves.toEqual({})
|
|
1043
|
+
await expect(user.validate({}, { validateUndefined: false })).resolves.toEqual({})
|
|
1044
|
+
await expect(user.validate({}, { update: true })).resolves.toEqual({})
|
|
1045
|
+
await expect(user.validate({}, { update: true, validateUndefined: false })).resolves.toEqual({})
|
|
1046
|
+
await expect(userdeep.validate({}, { update: true })).resolves.toEqual({})
|
|
1047
|
+
await expect(userdeep.validate({ name: {} }, { update: true, validateUndefined: false }))
|
|
1048
|
+
.resolves.toEqual({ name: {} })
|
|
1049
|
+
await expect(userdeep.validate({ names: [{}] }, { update: true, validateUndefined: false }))
|
|
1050
|
+
.resolves.toEqual({ names: [{}] })
|
|
1051
|
+
})
|
|
1052
|
+
|
|
958
1053
|
test('validation hooks', async () => {
|
|
959
1054
|
let db = (await opendb(null)).db
|
|
960
1055
|
let user = db.model('user', {
|