monastery 3.3.0 → 3.4.1

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/test/crud.js CHANGED
@@ -663,7 +663,7 @@ test('update operators', async () => {
663
663
  expect(beforeValidateHookCalled).toEqual(false)
664
664
  })
665
665
 
666
- test('update mixing formData', async() => {
666
+ test('update mixing data structures (bracket notation and objects)', async() => {
667
667
  // Mixing data
668
668
  let consignment = db.model('consignment', {
669
669
  fields: {
@@ -1373,4 +1373,131 @@ test('hooks > async and next conflict', async () => {
1373
1373
  expect(logSpy).toHaveBeenCalledWith('Monastery user.afterFind error: you cannot return a promise AND call next()')
1374
1374
 
1375
1375
  db2.close()
1376
+ })
1377
+
1378
+ test('hooks > option skipHooks', async () => {
1379
+ let user = db.model('user_crud_skiphooks', {
1380
+ fields: {
1381
+ name: { type: 'string' },
1382
+ },
1383
+ beforeInsert: [async (data) => {
1384
+ data.name = 'Hooked ' + data.name
1385
+ return data
1386
+ }],
1387
+ beforeUpdate: [async (data) => {
1388
+ data.name = 'Hooked ' + data.name
1389
+ return data
1390
+ }],
1391
+ })
1392
+
1393
+ // Insert with hooks
1394
+ let inserted = await user.insert({ data: { name: 'Martin Luther' } })
1395
+ expect(inserted).toEqual({
1396
+ _id: expect.any(Object),
1397
+ name: 'Hooked Martin Luther',
1398
+ })
1399
+
1400
+ // Update with hooks (use $set in this case)
1401
+ let updated = await user.update({
1402
+ query: inserted._id,
1403
+ $set: { name: 'Updated Name' },
1404
+ })
1405
+ expect(updated).toEqual({
1406
+ name: 'Hooked Updated Name',
1407
+ })
1408
+
1409
+ // Insert with skipHooks option
1410
+ let insertedSkipHooks = await user.insert({
1411
+ data: { name: 'Skipped Hooks Name' },
1412
+ skipHooks: true,
1413
+ })
1414
+ expect(insertedSkipHooks).toEqual({
1415
+ _id: expect.any(Object),
1416
+ name: 'Skipped Hooks Name',
1417
+ })
1418
+
1419
+ // Update with skipHooks option
1420
+ let updatedSkipHooks = await user.update({
1421
+ query: inserted._id,
1422
+ $set: { name: 'Skipped Hooks Name Updated' },
1423
+ skipHooks: true,
1424
+ })
1425
+ expect(updatedSkipHooks).toEqual({
1426
+ name: 'Skipped Hooks Name Updated',
1427
+ })
1428
+ })
1429
+
1430
+ test('update set and unset with option skipValidation', async () => {
1431
+ const db2 = monastery('127.0.0.1/monastery', { timestamps: false })
1432
+ let user = db2.model('user_set_and_unset', {
1433
+ fields: {
1434
+ profile: {
1435
+ name: { type: 'string', required: true },
1436
+ age: { type: 'number' },
1437
+ },
1438
+ },
1439
+ })
1440
+
1441
+ // Insert a document to update
1442
+ let inserted = await user.insert({ data: { profile: { name: 'John Doe', age: 30 } } })
1443
+ let userId = inserted._id
1444
+ let error = (detail, title) => [{
1445
+ detail: detail,
1446
+ title: title || expect.any(String),
1447
+ meta: expect.any(Object),
1448
+ status: '400',
1449
+ }]
1450
+
1451
+ // --- $set/data ---
1452
+
1453
+ // $set with skipValidation: undefined (true)
1454
+ const u1 = { query: userId, $set: { 'profile.age': 'not a number' } }
1455
+ await expect(user.update(u1)).resolves.toEqual({ 'profile.age': 'not a number' })
1456
+ await expect(user.findOne(userId)).resolves.toEqual({ _id: userId, profile: { name: 'John Doe', age: 'not a number' } })
1457
+
1458
+ // data with skipValidation: undefined (false)
1459
+ const u11 = { query: userId, data: { 'profile.age': 'not a number' } }
1460
+ await expect(user.update(u11)).rejects.toEqual(error('Value was not a number.', 'profile.age'))
1461
+
1462
+ for (let key of ['$set', 'data']) {
1463
+ // $set with skipValidation: true
1464
+ const u2 = { query: userId, [key]: { 'profile.age': 'not a number2' }, skipValidation: true }
1465
+ await expect(user.update(u2)).resolves.toEqual({ 'profile.age': 'not a number2' })
1466
+
1467
+ // $set with skipValidation: false
1468
+ const u3 = { query: userId, [key]: { 'profile.age': '8' }, skipValidation: false }
1469
+ await expect(user.update(u3)).resolves.toEqual({ 'profile.age': 8 })
1470
+ await expect(user.findOne(userId)).resolves.toEqual({ _id: userId, profile: { name: 'John Doe', age: 8 } })
1471
+
1472
+ // $set error with skipValidation: false
1473
+ const u4 = { query: userId, [key]: { 'profile.age': 'not a number' }, skipValidation: false }
1474
+ await expect(user.update(u4)).rejects.toEqual(error('Value was not a number.', 'profile.age'))
1475
+
1476
+ // $set error with skipValidation: ['profile']
1477
+ const u6 = { query: userId, [key]: { 'profile.age': 'not a number4' }, skipValidation: ['profile'] }
1478
+ await expect(user.update(u6)).resolves.toEqual({ 'profile.age': 'not a number4' })
1479
+ }
1480
+
1481
+ // --- $unset ---
1482
+
1483
+ // $unset with skipValidation: undefined
1484
+ const u7 = { query: userId, $unset: { 'profile.age': '' } }
1485
+ await expect(user.update(u7)).resolves.toEqual({}) // returns empty object
1486
+ await expect(user.findOne(userId)).resolves.toEqual({ _id: userId, profile: { name: 'John Doe' } })
1487
+
1488
+ // $unset error with skipValidation: false
1489
+ const u9 = { query: userId, $unset: { 'profile.name': '' }, skipValidation: false }
1490
+ await expect(user.update(u9)).rejects.toEqual(error('This field is required.', 'profile.name'))
1491
+ await expect(user.findOne(userId)).resolves.toEqual({ _id: userId, profile: { name: 'John Doe' } })
1492
+
1493
+ // $unset error with skipValidation: ['profile.name']
1494
+ const u10 = { query: userId, $unset: { 'profile.name': '' }, skipValidation: ['profile.name'] }
1495
+ await expect(user.update(u10)).resolves.toEqual({}) // returns empty object
1496
+ await expect(user.findOne(userId)).resolves.toEqual({ _id: userId, profile: {} }) /////////unseeeet
1497
+
1498
+ // $unset with skipValidation: true
1499
+ await expect(user.update({ query: userId, $set: { 'profile.name': 'John Doe2' } })) // add age back
1500
+ const u8 = { query: userId, $unset: { 'profile.name': '' }, skipValidation: true }
1501
+ await expect(user.update(u8)).resolves.toEqual({})
1502
+ await expect(user.findOne(userId)).resolves.toEqual({ _id: userId, profile: {} })
1376
1503
  })
package/test/util.js CHANGED
@@ -1,32 +1,247 @@
1
1
  const util = require('../lib/util.js')
2
2
  const monastery = require('../lib/index.js')
3
3
 
4
- test('util > formdata', async () => {
5
- expect(await util.parseFormData({
4
+ test('util > parseDotNotation', async () => {
5
+ const input = {
6
6
  'name': 'Martin',
7
- 'pets[]': '',
8
- 'deep[companyLogo]': 'a',
9
- 'deep[companyLogos][0]': 'b',
10
- 'deep[companyLogos2][0][logo]':'c',
11
- 'deep[companyLogos2][1][logo]': '',
12
- 'users[0][first]': 'Martin',
13
- 'users[0][last]': 'Luther',
14
- 'users[1][first]': 'Bruce',
15
- 'users[1][last]': 'Lee',
16
- })).toEqual({
7
+ 'deep.companyLogo1': 'a',
8
+ // not dot notation
9
+ 'specialInstructions': [
10
+ {
11
+ text: 'POD added by driver',
12
+ createdAt: 1653603212886,
13
+ updatedByName: 'Paul Driver 3',
14
+ importance: 'low',
15
+ }, {
16
+ text: 'filler',
17
+ createdAt: 1653601752472,
18
+ updatedByName: 'Paul',
19
+ importance: 'low',
20
+ },
21
+ ],
22
+ // Fields below are not dot notation, but should still be kept, in order.
23
+ 'specialInstructions[0][text]': 'filler',
24
+ 'specialInstructions[0][createdAt]': 1653601752472,
25
+ 'specialInstructions[0][updatedByName]': 'Paul',
26
+ 'specialInstructions[0][importance]': 'low',
27
+ // should override above
28
+ 'deep': {
29
+ companyLogo2: 'b',
30
+ companyLogo3: 'b',
31
+ },
32
+ // should be added into above
33
+ 'deep.companyLogo3': 'c',
34
+ 'deep.companyLogos.0.logo': 'd',
35
+ 'deep.companyLogos.1.logo': 'e',
36
+ 'deep.companyLogos.2': 'f',
37
+ }
38
+ const output = {
17
39
  name: 'Martin',
18
- pets: expect.any(Array),
19
- deep: {
20
- companyLogo: 'a',
21
- companyLogos: ['b'],
22
- companyLogos2: [{ logo: 'c' }, { logo: '' }],
40
+ deep: { // object first seen here
41
+ companyLogo2: 'b',
42
+ companyLogo3: 'c',
43
+ companyLogos: [{ logo: 'd' }, { logo: 'e' }, 'f'],
23
44
  },
24
- users: [
25
- { 'first': 'Martin', 'last': 'Luther' },
26
- { 'first': 'Bruce', 'last': 'Lee' },
45
+ specialInstructions: [
46
+ {
47
+ text: 'POD added by driver',
48
+ createdAt: 1653603212886,
49
+ updatedByName: 'Paul Driver 3',
50
+ importance: 'low',
51
+ }, {
52
+ text: 'filler',
53
+ createdAt: 1653601752472,
54
+ updatedByName: 'Paul',
55
+ importance: 'low',
56
+ },
27
57
  ],
28
- })
29
- expect(() => util.parseFormData({ 'users[][\'name\']': 'Martin' })).toThrow(
58
+ 'specialInstructions[0][text]': 'filler',
59
+ 'specialInstructions[0][createdAt]': 1653601752472,
60
+ 'specialInstructions[0][updatedByName]': 'Paul',
61
+ 'specialInstructions[0][importance]': 'low',
62
+ }
63
+
64
+ // parseDotNotation output
65
+ expect(util.parseDotNotation(input)).toEqual(output)
66
+
67
+ // expected order of keys
68
+ expect(Object.keys(output)).toEqual([
69
+ 'name',
70
+ 'deep',
71
+ 'specialInstructions',
72
+ 'specialInstructions[0][text]',
73
+ 'specialInstructions[0][createdAt]',
74
+ 'specialInstructions[0][updatedByName]',
75
+ 'specialInstructions[0][importance]',
76
+ ])
77
+
78
+ })
79
+
80
+ test('util > parseBracketNotation', async () => {
81
+ const input = {
82
+ 'name': 'Martin',
83
+ // 'pets[]': '', // <-- no longer supported
84
+ 'deep[companyLogo1]': 'a',
85
+ // not dot notation
86
+ 'specialInstructions': [
87
+ {
88
+ text: 'POD added by driver',
89
+ createdAt: 1653603212886,
90
+ updatedByName: 'Paul Driver 3',
91
+ importance: 'low',
92
+ }, {
93
+ text: 'filler',
94
+ createdAt: 1653601752472,
95
+ updatedByName: 'Paul',
96
+ importance: 'low',
97
+ },
98
+ ],
99
+ // Fields below are not bracket notation, but should still be kept, in order.
100
+ 'specialInstructions.0.text': 'filler',
101
+ 'specialInstructions.0.createdAt': 1653601752472,
102
+ 'specialInstructions.0.updatedByName': 'Paul',
103
+ 'specialInstructions.0.importance': 'low',
104
+ // should override above
105
+ 'deep': {
106
+ companyLogo2: 'b',
107
+ companyLogo3: 'b',
108
+ },
109
+ // should be added into above
110
+ 'deep[companyLogo3]': 'c',
111
+ 'deep[companyLogos][0][logo]': 'd',
112
+ 'deep[companyLogos][1][logo]': 'e',
113
+ 'deep[companyLogos][2]': 'f',
114
+ }
115
+ const output = {
116
+ name: 'Martin',
117
+ // pets: expect.any(Array),
118
+ deep: { // object first seen here
119
+ companyLogo2: 'b',
120
+ companyLogo3: 'c',
121
+ companyLogos: [{ logo: 'd' }, { logo: 'e' }, 'f'],
122
+ },
123
+ specialInstructions: [
124
+ {
125
+ text: 'POD added by driver',
126
+ createdAt: 1653603212886,
127
+ updatedByName: 'Paul Driver 3',
128
+ importance: 'low',
129
+ }, {
130
+ text: 'filler',
131
+ createdAt: 1653601752472,
132
+ updatedByName: 'Paul',
133
+ importance: 'low',
134
+ },
135
+ ],
136
+ 'specialInstructions.0.text': 'filler',
137
+ 'specialInstructions.0.createdAt': 1653601752472,
138
+ 'specialInstructions.0.updatedByName': 'Paul',
139
+ 'specialInstructions.0.importance': 'low',
140
+ }
141
+
142
+ // parseBracketNotation output
143
+ expect(util.parseBracketNotation(input)).toEqual(output)
144
+
145
+ // expected order of keys
146
+ expect(Object.keys(output)).toEqual([
147
+ 'name',
148
+ 'deep',
149
+ 'specialInstructions',
150
+ 'specialInstructions.0.text',
151
+ 'specialInstructions.0.createdAt',
152
+ 'specialInstructions.0.updatedByName',
153
+ 'specialInstructions.0.importance',
154
+ ])
155
+
156
+ expect(() => util.parseBracketNotation({ 'users[][\'name\']': 'Martin' })).toThrow(
157
+ 'Monastery: Array items in bracket notation need array indexes "users[][\'name\']", e.g. users[0][name]'
158
+ )
159
+ })
160
+
161
+ test('util > parseBracketToDotNotation', async () => {
162
+ const input = {
163
+ 'name': 'Martin',
164
+ 'deep[companyLogo1]': 'a',
165
+ // not dot notation
166
+ 'specialInstructions': [
167
+ {
168
+ text: 'POD added by driver',
169
+ createdAt: 1653603212886,
170
+ updatedByName: 'Paul Driver 3',
171
+ importance: 'low',
172
+ }, {
173
+ text: 'filler',
174
+ createdAt: 1653601752472,
175
+ updatedByName: 'Paul',
176
+ importance: 'low',
177
+ },
178
+ ],
179
+ // Fields below are not bracket notation, but should still be kept, in order.
180
+ 'specialInstructions.0.text': 'filler',
181
+ 'specialInstructions.0.createdAt': 1653601752472,
182
+ 'specialInstructions.0.updatedByName': 'Paul',
183
+ 'specialInstructions.0.importance': 'low',
184
+ // should NOT override above
185
+ 'deep': {
186
+ companyLogo2: 'b',
187
+ companyLogo3: 'b',
188
+ },
189
+ // should NOT be added into above
190
+ 'deep[companyLogo3]': 'c',
191
+ 'deep[companyLogos][0][logo]': 'd',
192
+ 'deep[companyLogos][1][logo]': 'e',
193
+ 'deep[companyLogos][2]': 'f',
194
+ }
195
+ const output = {
196
+ name: 'Martin',
197
+ 'deep.companyLogo1': 'a',
198
+ specialInstructions: [
199
+ {
200
+ text: 'POD added by driver',
201
+ createdAt: 1653603212886,
202
+ updatedByName: 'Paul Driver 3',
203
+ importance: 'low',
204
+ }, {
205
+ text: 'filler',
206
+ createdAt: 1653601752472,
207
+ updatedByName: 'Paul',
208
+ importance: 'low',
209
+ },
210
+ ],
211
+ 'specialInstructions.0.text': 'filler',
212
+ 'specialInstructions.0.createdAt': 1653601752472,
213
+ 'specialInstructions.0.updatedByName': 'Paul',
214
+ 'specialInstructions.0.importance': 'low',
215
+ 'deep': {
216
+ companyLogo2: 'b',
217
+ companyLogo3: 'b',
218
+ },
219
+ 'deep.companyLogo3': 'c',
220
+ 'deep.companyLogos.0.logo': 'd',
221
+ 'deep.companyLogos.1.logo': 'e',
222
+ 'deep.companyLogos.2': 'f',
223
+ }
224
+
225
+ // parseBracketToDotNotation output
226
+ expect(util.parseBracketToDotNotation(input)).toEqual(output)
227
+
228
+ // expected order of keys
229
+ expect(Object.keys(output)).toEqual([
230
+ 'name',
231
+ 'deep.companyLogo1',
232
+ 'specialInstructions',
233
+ 'specialInstructions.0.text',
234
+ 'specialInstructions.0.createdAt',
235
+ 'specialInstructions.0.updatedByName',
236
+ 'specialInstructions.0.importance',
237
+ 'deep',
238
+ 'deep.companyLogo3',
239
+ 'deep.companyLogos.0.logo',
240
+ 'deep.companyLogos.1.logo',
241
+ 'deep.companyLogos.2',
242
+ ])
243
+
244
+ expect(() => util.parseBracketToDotNotation({ 'users[][\'name\']': 'Martin' })).toThrow(
30
245
  'Monastery: Array items in bracket notation need array indexes "users[][\'name\']", e.g. users[0][name]'
31
246
  )
32
247
  })
package/test/validate.js CHANGED
@@ -300,7 +300,7 @@ test('validation subdocument errors', async () => {
300
300
  ])
301
301
  )
302
302
 
303
- // Insert: Ignore required subdocument property with a defined parent
303
+ // Insert: Required subdocument property is ignored with a parent/grandparent specificed
304
304
  await expect(user.validate({ animals: {} }, { validateUndefined: false })).resolves.toEqual({
305
305
  animals: {},
306
306
  })
@@ -1181,8 +1181,85 @@ test('validation option validateUndefined', async () => {
1181
1181
  .resolves.toEqual({ names: [{}] })
1182
1182
  })
1183
1183
 
1184
- test('validation hooks', async () => {
1185
- let user = db.model('user', {
1184
+ test('validation update dot notation', async () => {
1185
+ // Only updates the fields that are passed in the data object, similar to $set. They don't
1186
+ // remove subdocument fields that are not present in the data object.
1187
+ const user = db.model('user_partialUpdate1', {
1188
+ fields: {
1189
+ name: { type: 'string' },
1190
+ address: {
1191
+ city: { type: 'string' },
1192
+ country: { type: 'string' },
1193
+ },
1194
+ },
1195
+ })
1196
+ const user2 = db.model('user_partialUpdate2', {
1197
+ fields: {
1198
+ name: { type: 'string' },
1199
+ address: {
1200
+ city: { type: 'string' },
1201
+ country: { type: 'string', required: true },
1202
+ },
1203
+ books: [{
1204
+ title: { type: 'string', required: true },
1205
+ pages: {
1206
+ count: { type: 'number', required: true },
1207
+ },
1208
+ }],
1209
+ },
1210
+ })
1211
+
1212
+ // Partial validate
1213
+ const validated1 = await user.validate({
1214
+ 'address.city': 'Berlin2',
1215
+ 'address': { city: 'Berlin' }, // preserved
1216
+ })
1217
+ expect(validated1).toEqual({
1218
+ 'address': { city: 'Berlin' },
1219
+ 'address.city': 'Berlin2',
1220
+ })
1221
+
1222
+ /// { partialUpdate: true }
1223
+
1224
+ // Order of fields, normal objects are ordered first
1225
+ expect(Object.keys(validated1)).toEqual(['address', 'address.city'])
1226
+
1227
+ // validate (update) throws required error for deep field (as normal)
1228
+ await expect(user2.validate({ 'address': { city: 'Berlin' }}, { update: true }))
1229
+ .rejects.toEqual(expect.any(Array))
1230
+
1231
+ // validate (update) works with dot notatation paths
1232
+ await expect(user2.validate({ 'address.city': 'Berlin2' }, { update: true }))
1233
+ .resolves.toEqual({ 'address.city': 'Berlin2' })
1234
+
1235
+ // validate (insert) still throws required errors, even with dot notatation paths
1236
+ await expect(user2.validate({ 'address.city': 'Berlin2' }))
1237
+ .rejects.toEqual(expect.any(Array))
1238
+
1239
+ // validate (update) works with dot notation array paths, and validates data
1240
+ await expect(user2.validate({ 'books.0.pages.count': '1' }, { update: true }))
1241
+ .resolves.toEqual({ 'books.0.pages.count': 1 })
1242
+
1243
+ // validate (update) works with positional array paths
1244
+ await expect(user2.validate({ 'books.$.pages.count': '2' }, { update: true }))
1245
+ .resolves.toEqual({ 'books.$.pages.count': 2 })
1246
+
1247
+ // Non-field dot notation paths removed
1248
+ await expect(user2.validate({ 'books.0.badfield': 2 }, { update: true }))
1249
+ .resolves.toEqual({})
1250
+
1251
+ // validate (update) should continue on throwing validation errors for dot notation array paths with subdocument values
1252
+ await expect(user2.validate({ 'books.$.pages': {} }, { update: true }))
1253
+ .rejects.toEqual([{
1254
+ 'detail': 'This field is required.',
1255
+ 'meta': { 'detailLong': undefined, 'field': 'count', 'model': 'user_partialUpdate2', 'rule': 'required' },
1256
+ 'status': '400',
1257
+ 'title': 'books.$.pages.count',
1258
+ }])
1259
+ })
1260
+
1261
+ test('validation hooks and option skipHooks', async () => {
1262
+ let user = db.model('user_validation_hooks', {
1186
1263
  fields: {
1187
1264
  first: { type: 'string'},
1188
1265
  last: { type: 'string'},
@@ -1204,6 +1281,7 @@ test('validation hooks', async () => {
1204
1281
  // Catch validate (a)synchronous errors thrown in function or through `next(err)`
1205
1282
  await expect(user.validate({ first: '' })).rejects.toThrow('beforeValidate error 1..')
1206
1283
  await expect(user.validate({ first: 'Martin' })).rejects.toThrow('beforeValidate error 2..')
1284
+ await expect(user.validate({ first: 'Martin' }, { skipHooks: true })).resolves.toEqual({ first: 'Martin' })
1207
1285
  await expect(user.validate({ first: 'Martin', last: 'Luther' })).resolves.toEqual({
1208
1286
  first: 'Martin',
1209
1287
  last: 'Luther',
@@ -1212,6 +1290,10 @@ test('validation hooks', async () => {
1212
1290
  // Catch insert (a)synchronous errors thrown in function or through `next(err)`
1213
1291
  await expect(user.insert({ data: { first: '' } })).rejects.toThrow('beforeValidate error 1..')
1214
1292
  await expect(user.insert({ data: { first: 'Martin' } })).rejects.toThrow('beforeValidate error 2..')
1293
+ await expect(user.insert({ data: { first: 'Martin' }, skipHooks: true })).resolves.toEqual({
1294
+ _id: expect.any(Object),
1295
+ first: 'Martin',
1296
+ })
1215
1297
  await expect(user.insert({ data: { first: 'Martin', last: 'Luther' } })).resolves.toEqual({
1216
1298
  _id: expect.any(Object),
1217
1299
  first: 'Martin',
@@ -1223,6 +1305,8 @@ test('validation hooks', async () => {
1223
1305
  .rejects.toThrow('beforeValidate error 1..')
1224
1306
  await expect(user.update({ query: userDoc._id, data: { first: 'Martin' } }))
1225
1307
  .rejects.toThrow('beforeValidate error 2..')
1308
+ await expect(user.update({ query: userDoc._id, data: { first: 'Martin' }, skipHooks: true }))
1309
+ .resolves.toEqual({ first: 'Martin' })
1226
1310
  await expect(user.update({ query: userDoc._id, data: { first: 'Martin', last: 'Luther' } }))
1227
1311
  .resolves.toEqual({
1228
1312
  first: 'Martin',