monastery 1.31.2 → 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.
@@ -2,14 +2,22 @@
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.2](https://github.com/boycce/monastery/compare/v1.31.1...v1.31.2) (2022-02-15)
5
+ ### [1.31.6](https://github.com/boycce/monastery/compare/1.31.5...1.31.6) (2022-02-25)
6
6
 
7
7
 
8
8
  ### Bug Fixes
9
9
 
10
- * package scripts ([f7935af](https://github.com/boycce/monastery/commit/f7935afb0181ddb3e397bf804b34c841589dfcf0))
10
+ * added partial unqiue index tests ([ff6f193](https://github.com/boycce/monastery/commit/ff6f1938e333407ee17895873d2b42fa5263d7e3))
11
+ * scripts ([bc32680](https://github.com/boycce/monastery/commit/bc326809098ae24686158a0386fbbd6671d86c98))
11
12
 
12
- ### 1.31.1 (2022-02-15)
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
+
20
+ ### 1.31.4 (2022-02-15)
13
21
 
14
22
 
15
23
  ### Bug Fixes
@@ -24,5 +32,8 @@ All notable changes to this project will be documented in this file. See [standa
24
32
  * model-crud ([d421709](https://github.com/boycce/monastery/commit/d421709a70e6611c78e049e98268153c9bafae6d))
25
33
  * normalise afterFind ([0ab7f43](https://github.com/boycce/monastery/commit/0ab7f43f25b599e07d9ae751dc3bac8550e53c24))
26
34
  * normalised rule arguments and context ([6ba48da](https://github.com/boycce/monastery/commit/6ba48da3b9c643620cebf5442e60bd0318d6780f))
35
+ * package scripts ([f7935af](https://github.com/boycce/monastery/commit/f7935afb0181ddb3e397bf804b34c841589dfcf0))
36
+ * semver ([6f14909](https://github.com/boycce/monastery/commit/6f14909f4405cf26dc04a8603cc3bac232b96798))
37
+ * standard-version ([4627694](https://github.com/boycce/monastery/commit/46276948f76b22eae946147f488c7e734a88c023))
27
38
  * standard-version ([f553b08](https://github.com/boycce/monastery/commit/f553b08445eb7dd2e85f6bb447e2bb0bc38dda34))
28
39
  * util bug, updated tests ([bec1887](https://github.com/boycce/monastery/commit/bec1887f56cb8582b606a066c913c191362a61b0))
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 - ignore all required fields during insert, or
13
- * undefined subdocument required fields that have a defined parent/grandparent during update
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 - ignore all required fields during insert, or
202
- * undefined subdocument required fields that have a defined parent/grandparent during update
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
@@ -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 - ignore all required fields during insert, or undefined
16
- * subdocument required fields that have a defined parent/grandparent during update
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 data value is a subdocument, or when inserting, or when updating deep properties (non-root)
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
- ((path2||'').match(/\./) && (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : true))
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 : rule.validateUndefined
266
+ let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : opts.insert || isDeepProp
266
267
  if (!ruleMessage) ruleMessage = rule.message
267
268
 
268
- // Ignore undefined (if updated root property, or ignoring)
269
- if (typeof value === 'undefined' && (!validateUndefined || (opts.update && !path.match(/\./)))) return
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.2",
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
  },
@@ -46,11 +46,13 @@
46
46
  "supertest": "4.0.2"
47
47
  },
48
48
  "standard-version": {
49
- "releaseCommitMessageFormat": "v{{currentTag}}",
49
+ "infile": "changelog.md",
50
+ "releaseCommitMessageFormat": "{{currentTag}}",
50
51
  "sign": true,
51
52
  "skip": {
52
53
  "changelog": false,
53
54
  "tag": false
54
- }
55
+ },
56
+ "tag-prefix": ""
55
57
  }
56
58
  }
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 }, { validateUndefined: false })).resolves.toEqual({})
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: null })).rejects.toEqual([{
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 }, { validateUndefined: false })).rejects.toEqual([{
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 options', async () => {
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', {