@bedrockio/model 0.1.0

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +932 -0
  3. package/babel.config.cjs +41 -0
  4. package/dist/cjs/access.js +66 -0
  5. package/dist/cjs/assign.js +50 -0
  6. package/dist/cjs/const.js +16 -0
  7. package/dist/cjs/errors.js +17 -0
  8. package/dist/cjs/include.js +222 -0
  9. package/dist/cjs/index.js +62 -0
  10. package/dist/cjs/load.js +40 -0
  11. package/dist/cjs/package.json +1 -0
  12. package/dist/cjs/references.js +104 -0
  13. package/dist/cjs/schema.js +277 -0
  14. package/dist/cjs/search.js +266 -0
  15. package/dist/cjs/serialization.js +55 -0
  16. package/dist/cjs/slug.js +47 -0
  17. package/dist/cjs/soft-delete.js +192 -0
  18. package/dist/cjs/testing.js +33 -0
  19. package/dist/cjs/utils.js +73 -0
  20. package/dist/cjs/validation.js +313 -0
  21. package/dist/cjs/warn.js +13 -0
  22. package/jest-mongodb-config.js +10 -0
  23. package/jest.config.js +8 -0
  24. package/package.json +53 -0
  25. package/src/access.js +60 -0
  26. package/src/assign.js +45 -0
  27. package/src/const.js +9 -0
  28. package/src/errors.js +9 -0
  29. package/src/include.js +209 -0
  30. package/src/index.js +5 -0
  31. package/src/load.js +37 -0
  32. package/src/references.js +101 -0
  33. package/src/schema.js +286 -0
  34. package/src/search.js +263 -0
  35. package/src/serialization.js +49 -0
  36. package/src/slug.js +45 -0
  37. package/src/soft-delete.js +234 -0
  38. package/src/testing.js +29 -0
  39. package/src/utils.js +63 -0
  40. package/src/validation.js +329 -0
  41. package/src/warn.js +7 -0
  42. package/test/assign.test.js +225 -0
  43. package/test/definitions/custom-model.json +9 -0
  44. package/test/definitions/special-category.json +18 -0
  45. package/test/include.test.js +896 -0
  46. package/test/load.test.js +47 -0
  47. package/test/references.test.js +71 -0
  48. package/test/schema.test.js +919 -0
  49. package/test/search.test.js +652 -0
  50. package/test/serialization.test.js +748 -0
  51. package/test/setup.js +27 -0
  52. package/test/slug.test.js +112 -0
  53. package/test/soft-delete.test.js +333 -0
  54. package/test/validation.test.js +1925 -0
@@ -0,0 +1,896 @@
1
+ import { Types } from 'mongoose';
2
+
3
+ import { getIncludes } from '../src/include';
4
+ import { createTestModel, getTestModelName } from '../src/testing';
5
+
6
+ const userModelName = getTestModelName();
7
+
8
+ const Shop = createTestModel({
9
+ name: 'String',
10
+ email: 'String',
11
+ tags: ['String'],
12
+ user: {
13
+ ref: userModelName,
14
+ type: 'ObjectId',
15
+ },
16
+ customers: [
17
+ {
18
+ ref: userModelName,
19
+ type: 'ObjectId',
20
+ },
21
+ ],
22
+ deep: {
23
+ user: {
24
+ ref: userModelName,
25
+ type: 'ObjectId',
26
+ },
27
+ },
28
+ });
29
+
30
+ const Product = createTestModel({
31
+ name: 'String',
32
+ email: 'String',
33
+ tags: ['String'],
34
+ shop: {
35
+ ref: Shop.modelName,
36
+ type: 'ObjectId',
37
+ },
38
+ });
39
+
40
+ const userSchema = {
41
+ name: 'String',
42
+ email: 'String',
43
+ tags: ['String'],
44
+ address: {
45
+ line1: 'String',
46
+ line2: 'String',
47
+ },
48
+ likedProducts: [
49
+ {
50
+ ref: Product.modelName,
51
+ type: 'ObjectId',
52
+ },
53
+ ],
54
+ self: {
55
+ ref: userModelName,
56
+ type: 'ObjectId',
57
+ },
58
+ };
59
+
60
+ const User = createTestModel(userModelName, userSchema);
61
+
62
+ const Comment = createTestModel({
63
+ body: 'String',
64
+ product: {
65
+ ref: Product.modelName,
66
+ type: 'ObjectId',
67
+ },
68
+ });
69
+
70
+ Product.schema.virtual('comments', {
71
+ ref: Comment.modelName,
72
+ localField: '_id',
73
+ foreignField: 'product',
74
+ justOne: false,
75
+ });
76
+
77
+ describe('getIncludes', () => {
78
+ it('should have select for single field', async () => {
79
+ const data = getIncludes(Shop.modelName, 'name');
80
+ expect(data).toEqual({
81
+ select: ['name'],
82
+ populate: [],
83
+ });
84
+ });
85
+
86
+ it('should have select for multiple fields', async () => {
87
+ const data = getIncludes(Shop.modelName, ['name', 'email']);
88
+ expect(data).toEqual({
89
+ select: ['name', 'email'],
90
+ populate: [],
91
+ });
92
+ });
93
+
94
+ it('should have populate for single foreign field', async () => {
95
+ const data = getIncludes(Shop.modelName, 'user');
96
+ expect(data).toEqual({
97
+ select: [],
98
+ populate: [
99
+ {
100
+ path: 'user',
101
+ select: [],
102
+ populate: [],
103
+ },
104
+ ],
105
+ });
106
+ });
107
+
108
+ it('should have populate for multiple foreign fields', async () => {
109
+ const data = getIncludes(Shop.modelName, ['user', 'deep.user']);
110
+ expect(data).toEqual({
111
+ select: [],
112
+ populate: [
113
+ {
114
+ path: 'user',
115
+ select: [],
116
+ populate: [],
117
+ },
118
+ {
119
+ path: 'deep.user',
120
+ select: [],
121
+ populate: [],
122
+ },
123
+ ],
124
+ });
125
+ });
126
+
127
+ it('should be correct for combining selection and population', async () => {
128
+ const data = getIncludes(Shop.modelName, [
129
+ 'name',
130
+ 'email',
131
+ 'user',
132
+ 'deep.user',
133
+ ]);
134
+ expect(data).toEqual({
135
+ select: ['name', 'email'],
136
+ populate: [
137
+ {
138
+ path: 'user',
139
+ select: [],
140
+ populate: [],
141
+ },
142
+ {
143
+ path: 'deep.user',
144
+ select: [],
145
+ populate: [],
146
+ },
147
+ ],
148
+ });
149
+ });
150
+
151
+ it('should select a populated field', async () => {
152
+ const data = getIncludes(Shop.modelName, 'user.name');
153
+ expect(data).toEqual({
154
+ select: [],
155
+ populate: [
156
+ {
157
+ path: 'user',
158
+ select: ['name'],
159
+ populate: [],
160
+ },
161
+ ],
162
+ });
163
+ });
164
+
165
+ it('should select a nested populated field', async () => {
166
+ const data = getIncludes(Shop.modelName, 'user.address.line1');
167
+ expect(data).toEqual({
168
+ select: [],
169
+ populate: [
170
+ {
171
+ path: 'user',
172
+ select: ['address.line1'],
173
+ populate: [],
174
+ },
175
+ ],
176
+ });
177
+ });
178
+
179
+ it('should select double nested populated field', async () => {
180
+ const data = getIncludes(Shop.modelName, 'deep.user.address.line1');
181
+ expect(data).toEqual({
182
+ select: [],
183
+ populate: [
184
+ {
185
+ path: 'deep.user',
186
+ select: ['address.line1'],
187
+ populate: [],
188
+ },
189
+ ],
190
+ });
191
+ });
192
+
193
+ it('should not override previously selected fields', async () => {
194
+ const data = getIncludes(Shop.modelName, ['user.name', 'user.email']);
195
+ expect(data).toEqual({
196
+ select: [],
197
+ populate: [
198
+ {
199
+ path: 'user',
200
+ select: ['name', 'email'],
201
+ populate: [],
202
+ },
203
+ ],
204
+ });
205
+ });
206
+
207
+ it('should override root with select', async () => {
208
+ const data = getIncludes(Shop.modelName, ['user', 'user.name']);
209
+ expect(data).toEqual({
210
+ select: [],
211
+ populate: [
212
+ {
213
+ path: 'user',
214
+ select: ['name'],
215
+ populate: [],
216
+ },
217
+ ],
218
+ });
219
+ });
220
+
221
+ it('should override root with select reversed', async () => {
222
+ const data = getIncludes(Shop.modelName, ['user.name', 'user']);
223
+ expect(data).toEqual({
224
+ select: [],
225
+ populate: [
226
+ {
227
+ path: 'user',
228
+ select: ['name'],
229
+ populate: [],
230
+ },
231
+ ],
232
+ });
233
+ });
234
+
235
+ it('should select an array field', async () => {
236
+ const data = getIncludes(Shop.modelName, 'tags');
237
+ expect(data).toEqual({
238
+ select: ['tags'],
239
+ populate: [],
240
+ });
241
+ });
242
+
243
+ it('should select a foreign array field', async () => {
244
+ const data = getIncludes(Shop.modelName, 'user.tags');
245
+ expect(data).toEqual({
246
+ select: [],
247
+ populate: [
248
+ {
249
+ path: 'user',
250
+ select: ['tags'],
251
+ populate: [],
252
+ },
253
+ ],
254
+ });
255
+ });
256
+
257
+ it('should populate an array field', async () => {
258
+ const data = getIncludes(Shop.modelName, 'customers');
259
+ expect(data).toEqual({
260
+ select: [],
261
+ populate: [
262
+ {
263
+ path: 'customers',
264
+ select: [],
265
+ populate: [],
266
+ },
267
+ ],
268
+ });
269
+ });
270
+
271
+ it('should populate a foreign array field', async () => {
272
+ const data = getIncludes(Product.modelName, 'shop.customers');
273
+ expect(data).toEqual({
274
+ select: [],
275
+ populate: [
276
+ {
277
+ path: 'shop',
278
+ select: [],
279
+ populate: [
280
+ {
281
+ path: 'customers',
282
+ select: [],
283
+ populate: [],
284
+ },
285
+ ],
286
+ },
287
+ ],
288
+ });
289
+ });
290
+
291
+ it('should populate and select a foreign array field', async () => {
292
+ const data = getIncludes(Product.modelName, 'shop.customers.address.line1');
293
+ expect(data).toEqual({
294
+ select: [],
295
+ populate: [
296
+ {
297
+ path: 'shop',
298
+ select: [],
299
+ populate: [
300
+ {
301
+ path: 'customers',
302
+ select: ['address.line1'],
303
+ populate: [],
304
+ },
305
+ ],
306
+ },
307
+ ],
308
+ });
309
+ });
310
+
311
+ it('should select deeply populated field', async () => {
312
+ const data = getIncludes(Product.modelName, 'shop.user.address.line1');
313
+ expect(data).toEqual({
314
+ select: [],
315
+ populate: [
316
+ {
317
+ path: 'shop',
318
+ select: [],
319
+ populate: [
320
+ {
321
+ path: 'user',
322
+ select: ['address.line1'],
323
+ populate: [],
324
+ },
325
+ ],
326
+ },
327
+ ],
328
+ });
329
+ });
330
+
331
+ it('should select a shallow and deeply populated field', async () => {
332
+ const data = getIncludes(Product.modelName, [
333
+ 'name',
334
+ 'shop.user.address.line1',
335
+ ]);
336
+ expect(data).toEqual({
337
+ select: ['name'],
338
+ populate: [
339
+ {
340
+ path: 'shop',
341
+ select: [],
342
+ populate: [
343
+ {
344
+ path: 'user',
345
+ select: ['address.line1'],
346
+ populate: [],
347
+ },
348
+ ],
349
+ },
350
+ ],
351
+ });
352
+ });
353
+
354
+ it('should handle complex population of many fields', async () => {
355
+ const data = getIncludes(Product.modelName, [
356
+ 'name',
357
+ 'shop.email',
358
+ 'shop.user.name',
359
+ 'shop.user.address.line1',
360
+ 'shop.customers.self.self.tags',
361
+ ]);
362
+ expect(data).toEqual({
363
+ select: ['name'],
364
+ populate: [
365
+ {
366
+ path: 'shop',
367
+ select: ['email'],
368
+ populate: [
369
+ {
370
+ path: 'user',
371
+ select: ['name', 'address.line1'],
372
+ populate: [],
373
+ },
374
+ {
375
+ path: 'customers',
376
+ select: [],
377
+ populate: [
378
+ {
379
+ path: 'self',
380
+ select: [],
381
+ populate: [
382
+ {
383
+ path: 'self',
384
+ select: ['tags'],
385
+ populate: [],
386
+ },
387
+ ],
388
+ },
389
+ ],
390
+ },
391
+ ],
392
+ },
393
+ ],
394
+ });
395
+ });
396
+
397
+ it('should populate recursive field', async () => {
398
+ const data = getIncludes(User.modelName, 'self.self.self');
399
+ expect(data).toEqual({
400
+ select: [],
401
+ populate: [
402
+ {
403
+ path: 'self',
404
+ select: [],
405
+ populate: [
406
+ {
407
+ path: 'self',
408
+ select: [],
409
+ populate: [
410
+ {
411
+ path: 'self',
412
+ select: [],
413
+ populate: [],
414
+ },
415
+ ],
416
+ },
417
+ ],
418
+ },
419
+ ],
420
+ });
421
+ });
422
+
423
+ it('should populate a virtual', async () => {
424
+ const data = getIncludes(Product.modelName, 'comments');
425
+ expect(data).toEqual({
426
+ select: [],
427
+ populate: [
428
+ {
429
+ path: 'comments',
430
+ select: [],
431
+ populate: [],
432
+ },
433
+ ],
434
+ });
435
+ });
436
+
437
+ it('should throw an error on unknown path', async () => {
438
+ expect(() => {
439
+ getIncludes(User.modelName, 'name.foo');
440
+ }).toThrow(`Unknown path on ${User.modelName}: name.foo.`);
441
+ });
442
+
443
+ it('should error on massive recusrion', async () => {
444
+ const include = 'self'.repeat(6).match(/.{4}/g).join('.');
445
+ expect(() => {
446
+ getIncludes(User.modelName, include);
447
+ }).toThrow('Cannot populate more than 5 levels.');
448
+ });
449
+
450
+ it('should allow trailing wildcards in includes', async () => {
451
+ const User = createTestModel({
452
+ name: 'String',
453
+ name1: 'String',
454
+ name2: 'String',
455
+ nameFirst: 'String',
456
+ nameLast: 'String',
457
+ naming: 'String',
458
+ deep: {
459
+ deep: {
460
+ name: 'String',
461
+ name1: 'String',
462
+ name2: 'String',
463
+ },
464
+ },
465
+ nam: 'String',
466
+ });
467
+ const data = getIncludes(User.modelName, ['name*', 'deep.deep.name*']);
468
+ expect(data).toEqual({
469
+ select: [
470
+ 'name1',
471
+ 'name2',
472
+ 'nameFirst',
473
+ 'nameLast',
474
+ 'deep.deep.name1',
475
+ 'deep.deep.name2',
476
+ ],
477
+ populate: [],
478
+ });
479
+ });
480
+
481
+ it('should allow leading wildcards in includes', async () => {
482
+ const User = createTestModel({
483
+ nam: 'String',
484
+ name: 'String',
485
+ Name: 'String',
486
+ fName: 'String',
487
+ FName: 'String',
488
+ firstName: 'String',
489
+ lastName: 'String',
490
+ aname: 'String',
491
+ name1: 'String',
492
+ name2: 'String',
493
+ deep: {
494
+ deep: {
495
+ nam: 'String',
496
+ name: 'String',
497
+ Name: 'String',
498
+ fName: 'String',
499
+ FName: 'String',
500
+ firstName: 'String',
501
+ lastName: 'String',
502
+ aname: 'String',
503
+ name1: 'String',
504
+ name2: 'String',
505
+ },
506
+ },
507
+ });
508
+ const data = getIncludes(User.modelName, ['*Name', 'deep.deep.*Name']);
509
+ expect(data).toEqual({
510
+ select: [
511
+ 'fName',
512
+ 'FName',
513
+ 'firstName',
514
+ 'lastName',
515
+ 'deep.deep.fName',
516
+ 'deep.deep.FName',
517
+ 'deep.deep.firstName',
518
+ 'deep.deep.lastName',
519
+ ],
520
+ populate: [],
521
+ });
522
+ });
523
+
524
+ it('should not choke on recursion', async () => {
525
+ const data = getIncludes(User.modelName, '**');
526
+ expect(data).toEqual({
527
+ select: [
528
+ 'name',
529
+ 'email',
530
+ 'tags',
531
+ 'address.line1',
532
+ 'address.line2',
533
+ 'createdAt',
534
+ 'updatedAt',
535
+ 'deletedAt',
536
+ 'deleted',
537
+ ],
538
+ populate: [
539
+ {
540
+ path: 'likedProducts',
541
+ select: [],
542
+ populate: [],
543
+ },
544
+ {
545
+ path: 'self',
546
+ select: [],
547
+ populate: [],
548
+ },
549
+ ],
550
+ });
551
+ });
552
+
553
+ it('should allow wildcards across deep paths', async () => {
554
+ const data = getIncludes(Product.modelName, 'shop.**');
555
+ expect(data).toEqual({
556
+ select: [],
557
+ populate: [
558
+ {
559
+ path: 'shop',
560
+ select: [
561
+ 'name',
562
+ 'email',
563
+ 'tags',
564
+ 'createdAt',
565
+ 'updatedAt',
566
+ 'deletedAt',
567
+ 'deleted',
568
+ ],
569
+ populate: [
570
+ {
571
+ path: 'user',
572
+ populate: [],
573
+ select: [],
574
+ },
575
+ {
576
+ path: 'customers',
577
+ populate: [],
578
+ select: [],
579
+ },
580
+ {
581
+ path: 'deep.user',
582
+ populate: [],
583
+ select: [],
584
+ },
585
+ ],
586
+ },
587
+ ],
588
+ });
589
+ });
590
+ });
591
+
592
+ describe('query includes', () => {
593
+ it('should allow query chaining with include', async () => {
594
+ const user = await User.create({
595
+ name: 'Bob',
596
+ });
597
+ let shop = await Shop.create({
598
+ name: 'foo',
599
+ user: user.id,
600
+ });
601
+
602
+ shop = await Shop.findById(shop.id);
603
+ expect(shop.user).toBeInstanceOf(Types.ObjectId);
604
+
605
+ shop = await Shop.findById(shop.id).include('user');
606
+ expect(shop.user.id).toBe(user.id);
607
+ });
608
+
609
+ it('should allow includes by filter', async () => {
610
+ const user = await User.create({
611
+ name: 'Bob',
612
+ });
613
+ let shop = await Shop.create({
614
+ name: 'foo',
615
+ user: user.id,
616
+ });
617
+
618
+ [shop] = await Shop.find({
619
+ _id: shop.id,
620
+ });
621
+ expect(shop.user).toBeInstanceOf(Types.ObjectId);
622
+
623
+ [shop] = await Shop.find({
624
+ _id: shop.id,
625
+ include: 'user',
626
+ });
627
+ expect(shop.user.id).toBe(user.id);
628
+ });
629
+ });
630
+
631
+ describe('document includes', () => {
632
+ it('should include after create', async () => {
633
+ const user = await User.create({
634
+ name: 'Bob',
635
+ });
636
+ const shop = await Shop.create({
637
+ name: 'foo',
638
+ user: user.id,
639
+ include: ['user'],
640
+ });
641
+ expect(shop.user.name).toBe('Bob');
642
+ });
643
+
644
+ it('should include after save assign', async () => {
645
+ const user = await User.create({
646
+ name: 'Bob',
647
+ });
648
+ const shop = new Shop({
649
+ name: 'foo',
650
+ });
651
+ shop.assign({
652
+ user: user.id,
653
+ include: 'user',
654
+ });
655
+ await shop.save();
656
+ expect(shop.user.name).toBe('Bob');
657
+ });
658
+
659
+ it('should include after manual update', async () => {
660
+ const user = await User.create({
661
+ name: 'Bob',
662
+ });
663
+ const shop = await Shop.create({
664
+ name: 'foo',
665
+ });
666
+ shop.name = 'butts';
667
+ shop.user = user.id;
668
+ shop.include = 'user';
669
+ await shop.save();
670
+ expect(shop.user.name).toBe('Bob');
671
+ });
672
+
673
+ it('should populate after insert', async () => {
674
+ const user = await User.create({
675
+ name: 'Bob',
676
+ });
677
+ const shop = new Shop({
678
+ name: 'foo',
679
+ user: user.id,
680
+ include: 'user',
681
+ });
682
+ await shop.save();
683
+ expect(shop.user.name).toBe('Bob');
684
+ });
685
+
686
+ it('should perform complex populate after insert', async () => {
687
+ const user = await User.create({
688
+ name: 'Bob',
689
+ });
690
+ const shop = await Shop.create({
691
+ name: 'Shop',
692
+ user: user.id,
693
+ });
694
+ const product = await Product.create({
695
+ name: 'Product',
696
+ shop: shop.id,
697
+ include: ['name', 'shop.user'],
698
+ });
699
+ const data = JSON.parse(JSON.stringify(product));
700
+ expect(data).toEqual({
701
+ id: product.id,
702
+ name: 'Product',
703
+ shop: {
704
+ id: shop.id,
705
+ name: 'Shop',
706
+ tags: [],
707
+ customers: [],
708
+ user: {
709
+ id: user.id,
710
+ name: 'Bob',
711
+ tags: [],
712
+ likedProducts: [],
713
+ createdAt: user.createdAt.toISOString(),
714
+ updatedAt: user.updatedAt.toISOString(),
715
+ },
716
+ createdAt: shop.createdAt.toISOString(),
717
+ updatedAt: shop.updatedAt.toISOString(),
718
+ },
719
+ });
720
+ });
721
+
722
+ it('should virtually project fields on a created document', async () => {
723
+ const user = await User.create({
724
+ name: 'Bob',
725
+ email: 'bob@bar.com',
726
+ });
727
+ const shop = await Shop.create({
728
+ name: 'foo',
729
+ email: 'foo@bar.com',
730
+ user: user.id,
731
+ include: ['name', 'user'],
732
+ });
733
+ const data = JSON.parse(JSON.stringify(shop));
734
+ expect(data).toEqual({
735
+ id: shop.id,
736
+ name: 'foo',
737
+ user: {
738
+ id: user.id,
739
+ name: 'Bob',
740
+ email: 'bob@bar.com',
741
+ tags: [],
742
+ likedProducts: [],
743
+ createdAt: user.createdAt.toISOString(),
744
+ updatedAt: user.updatedAt.toISOString(),
745
+ },
746
+ });
747
+ });
748
+
749
+ it('should exclude fields', async () => {
750
+ const user = await User.create({
751
+ name: 'Bob',
752
+ email: 'foo@bar.com',
753
+ tags: ['a', 'b', 'c'],
754
+ include: ['-name', '-tags'],
755
+ });
756
+ const data = JSON.parse(JSON.stringify(user));
757
+ expect(data).toEqual({
758
+ id: user.id,
759
+ email: 'foo@bar.com',
760
+ likedProducts: [],
761
+ createdAt: user.createdAt.toISOString(),
762
+ updatedAt: user.updatedAt.toISOString(),
763
+ });
764
+ });
765
+
766
+ it('should exclude a populated field', async () => {
767
+ const user = await User.create({
768
+ name: 'Bob',
769
+ email: 'foo@bar.com',
770
+ });
771
+ const shop = await Shop.create({
772
+ name: 'Shop',
773
+ email: 'shop@bar.com',
774
+ user: user.id,
775
+ include: ['-name', '-user'],
776
+ });
777
+ const data = JSON.parse(JSON.stringify(shop));
778
+ expect(data).toEqual({
779
+ id: shop.id,
780
+ email: 'shop@bar.com',
781
+ tags: [],
782
+ customers: [],
783
+ createdAt: shop.createdAt.toISOString(),
784
+ updatedAt: shop.updatedAt.toISOString(),
785
+ });
786
+ });
787
+
788
+ it('should exclude a deep field', async () => {
789
+ const user = await User.create({
790
+ name: 'Bob',
791
+ email: 'foo@bar.com',
792
+ address: {
793
+ line1: 'line1',
794
+ line2: 'line2',
795
+ },
796
+ });
797
+ const shop = await Shop.create({
798
+ name: 'Shop',
799
+ user: user.id,
800
+ include: ['user', '-user.name', '-user.address.line1'],
801
+ });
802
+ const data = JSON.parse(JSON.stringify(shop));
803
+ expect(data).toEqual({
804
+ id: shop.id,
805
+ name: 'Shop',
806
+ user: {
807
+ id: user.id,
808
+ tags: [],
809
+ email: 'foo@bar.com',
810
+ address: {
811
+ line2: 'line2',
812
+ },
813
+ likedProducts: [],
814
+ createdAt: user.createdAt.toISOString(),
815
+ updatedAt: user.updatedAt.toISOString(),
816
+ },
817
+ tags: [],
818
+ customers: [],
819
+ createdAt: shop.createdAt.toISOString(),
820
+ updatedAt: shop.updatedAt.toISOString(),
821
+ });
822
+ });
823
+ });
824
+
825
+ describe('access control', () => {
826
+ const User = createTestModel({
827
+ name: 'String',
828
+ password: {
829
+ type: 'String',
830
+ readAccess: 'none',
831
+ },
832
+ });
833
+
834
+ const Shop = createTestModel({
835
+ name: 'String',
836
+ user: {
837
+ ref: User.modelName,
838
+ type: 'ObjectId',
839
+ },
840
+ });
841
+
842
+ it('should not allow read access', async () => {
843
+ let user = await User.create({
844
+ password: 'fake password',
845
+ });
846
+ user = await User.findById(user.id).include('password');
847
+ expect(user.password).toBe('fake password');
848
+ expect(user.toObject().password).toBeUndefined();
849
+ });
850
+
851
+ it('should not allow read access with wildcard', async () => {
852
+ let user = await User.create({
853
+ password: 'fake password',
854
+ });
855
+ user = await User.findById(user.id).include('*');
856
+ expect(user.password).toBe('fake password');
857
+ expect(user.toObject().password).toBeUndefined();
858
+ });
859
+
860
+ it('should not allow read access with exclusion', async () => {
861
+ let user = await User.create({
862
+ password: 'fake password',
863
+ });
864
+ user = await User.findById(user.id).include('-name');
865
+ expect(user.password).toBe('fake password');
866
+ expect(user.toObject().password).toBeUndefined();
867
+ });
868
+
869
+ it('should not allow deep read access', async () => {
870
+ const user = await User.create({
871
+ password: 'fake password',
872
+ });
873
+ let shop = await Shop.create({
874
+ name: 'foo',
875
+ user,
876
+ });
877
+ shop = await Shop.findById(shop.id).include('user');
878
+ expect(shop.user.password).toBe('fake password');
879
+ expect(shop.toObject().user.password).toBeUndefined();
880
+ });
881
+ });
882
+
883
+ describe('other', () => {
884
+ it('should not populate a deleted document', async () => {
885
+ const user = await User.create({
886
+ password: 'fake password',
887
+ });
888
+ let shop = await Shop.create({
889
+ name: 'foo',
890
+ user,
891
+ });
892
+ await user.delete();
893
+ shop = await Shop.findById(shop.id).include('user');
894
+ expect(shop.user).toBe(null);
895
+ });
896
+ });