@zenstackhq/client-helpers 3.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.
@@ -0,0 +1,949 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { NestedReadVisitor, type NestedReadVisitorCallback } from '../src/nested-read-visitor';
3
+ import { createField, createRelationField, createSchema } from './test-helpers';
4
+
5
+ describe('NestedReadVisitor tests', () => {
6
+ describe('basic visiting', () => {
7
+ it('visits simple model without select or include', () => {
8
+ const schema = createSchema({
9
+ User: {
10
+ name: 'User',
11
+ fields: {
12
+ id: createField('id', 'String'),
13
+ name: createField('name', 'String'),
14
+ },
15
+ uniqueFields: {},
16
+ idFields: ['id'],
17
+ },
18
+ });
19
+
20
+ const callback = vi.fn();
21
+ const visitor = new NestedReadVisitor(schema, { field: callback });
22
+
23
+ visitor.visit('User', { where: { id: '1' } });
24
+
25
+ // Should be called once for the root with undefined field
26
+ expect(callback).toHaveBeenCalledTimes(1);
27
+ expect(callback).toHaveBeenCalledWith('User', undefined, undefined, { where: { id: '1' } });
28
+ });
29
+
30
+ it('handles null or undefined args', () => {
31
+ const schema = createSchema({
32
+ User: {
33
+ name: 'User',
34
+ fields: {},
35
+ uniqueFields: {},
36
+ idFields: ['id'],
37
+ },
38
+ });
39
+
40
+ const callback = vi.fn();
41
+ const visitor = new NestedReadVisitor(schema, { field: callback });
42
+
43
+ visitor.visit('User', null);
44
+ expect(callback).toHaveBeenCalledWith('User', undefined, undefined, null);
45
+
46
+ visitor.visit('User', undefined);
47
+ expect(callback).toHaveBeenCalledWith('User', undefined, undefined, undefined);
48
+ });
49
+ });
50
+
51
+ describe('include visits', () => {
52
+ it('visits fields with include', () => {
53
+ const schema = createSchema({
54
+ User: {
55
+ name: 'User',
56
+ fields: {
57
+ id: createField('id', 'String'),
58
+ posts: createRelationField('posts', 'Post'),
59
+ },
60
+ uniqueFields: {},
61
+ idFields: ['id'],
62
+ },
63
+ Post: {
64
+ name: 'Post',
65
+ fields: {
66
+ id: createField('id', 'String'),
67
+ title: createField('title', 'String'),
68
+ },
69
+ uniqueFields: {},
70
+ idFields: ['id'],
71
+ },
72
+ });
73
+
74
+ const callback = vi.fn();
75
+ const visitor = new NestedReadVisitor(schema, { field: callback });
76
+
77
+ visitor.visit('User', {
78
+ include: {
79
+ posts: true,
80
+ },
81
+ });
82
+
83
+ expect(callback).toHaveBeenCalledTimes(2);
84
+ expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, {
85
+ include: { posts: true },
86
+ });
87
+ expect(callback).toHaveBeenNthCalledWith(
88
+ 2,
89
+ 'Post',
90
+ expect.objectContaining({ name: 'posts' }),
91
+ 'include',
92
+ true,
93
+ );
94
+ });
95
+
96
+ it('visits nested includes', () => {
97
+ const schema = createSchema({
98
+ User: {
99
+ name: 'User',
100
+ fields: {
101
+ id: createField('id', 'String'),
102
+ posts: createRelationField('posts', 'Post'),
103
+ },
104
+ uniqueFields: {},
105
+ idFields: ['id'],
106
+ },
107
+ Post: {
108
+ name: 'Post',
109
+ fields: {
110
+ id: createField('id', 'String'),
111
+ title: createField('title', 'String'),
112
+ comments: createRelationField('comments', 'Comment'),
113
+ },
114
+ uniqueFields: {},
115
+ idFields: ['id'],
116
+ },
117
+ Comment: {
118
+ name: 'Comment',
119
+ fields: {
120
+ id: createField('id', 'String'),
121
+ text: createField('text', 'String'),
122
+ },
123
+ uniqueFields: {},
124
+ idFields: ['id'],
125
+ },
126
+ });
127
+
128
+ const callback = vi.fn();
129
+ const visitor = new NestedReadVisitor(schema, { field: callback });
130
+
131
+ visitor.visit('User', {
132
+ include: {
133
+ posts: {
134
+ include: {
135
+ comments: true,
136
+ },
137
+ },
138
+ },
139
+ });
140
+
141
+ expect(callback).toHaveBeenCalledTimes(3);
142
+ expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, expect.any(Object));
143
+ expect(callback).toHaveBeenNthCalledWith(
144
+ 2,
145
+ 'Post',
146
+ expect.objectContaining({ name: 'posts' }),
147
+ 'include',
148
+ expect.any(Object),
149
+ );
150
+ expect(callback).toHaveBeenNthCalledWith(
151
+ 3,
152
+ 'Comment',
153
+ expect.objectContaining({ name: 'comments' }),
154
+ 'include',
155
+ true,
156
+ );
157
+ });
158
+
159
+ it('visits multiple includes at same level', () => {
160
+ const schema = createSchema({
161
+ User: {
162
+ name: 'User',
163
+ fields: {
164
+ id: createField('id', 'String'),
165
+ posts: createRelationField('posts', 'Post'),
166
+ profile: createRelationField('profile', 'Profile'),
167
+ },
168
+ uniqueFields: {},
169
+ idFields: ['id'],
170
+ },
171
+ Post: {
172
+ name: 'Post',
173
+ fields: {
174
+ id: createField('id', 'String'),
175
+ },
176
+ uniqueFields: {},
177
+ idFields: ['id'],
178
+ },
179
+ Profile: {
180
+ name: 'Profile',
181
+ fields: {
182
+ id: createField('id', 'String'),
183
+ },
184
+ uniqueFields: {},
185
+ idFields: ['id'],
186
+ },
187
+ });
188
+
189
+ const callback = vi.fn();
190
+ const visitor = new NestedReadVisitor(schema, { field: callback });
191
+
192
+ visitor.visit('User', {
193
+ include: {
194
+ posts: true,
195
+ profile: true,
196
+ },
197
+ });
198
+
199
+ expect(callback).toHaveBeenCalledTimes(3);
200
+ expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, expect.any(Object));
201
+ expect(callback).toHaveBeenNthCalledWith(
202
+ 2,
203
+ 'Post',
204
+ expect.objectContaining({ name: 'posts' }),
205
+ 'include',
206
+ true,
207
+ );
208
+ expect(callback).toHaveBeenNthCalledWith(
209
+ 3,
210
+ 'Profile',
211
+ expect.objectContaining({ name: 'profile' }),
212
+ 'include',
213
+ true,
214
+ );
215
+ });
216
+ });
217
+
218
+ describe('select visits', () => {
219
+ it('visits fields with select', () => {
220
+ const schema = createSchema({
221
+ User: {
222
+ name: 'User',
223
+ fields: {
224
+ id: createField('id', 'String'),
225
+ posts: createRelationField('posts', 'Post'),
226
+ },
227
+ uniqueFields: {},
228
+ idFields: ['id'],
229
+ },
230
+ Post: {
231
+ name: 'Post',
232
+ fields: {
233
+ id: createField('id', 'String'),
234
+ title: createField('title', 'String'),
235
+ },
236
+ uniqueFields: {},
237
+ idFields: ['id'],
238
+ },
239
+ });
240
+
241
+ const callback = vi.fn();
242
+ const visitor = new NestedReadVisitor(schema, { field: callback });
243
+
244
+ visitor.visit('User', {
245
+ select: {
246
+ posts: true,
247
+ },
248
+ });
249
+
250
+ expect(callback).toHaveBeenCalledTimes(2);
251
+ expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, {
252
+ select: { posts: true },
253
+ });
254
+ expect(callback).toHaveBeenNthCalledWith(
255
+ 2,
256
+ 'Post',
257
+ expect.objectContaining({ name: 'posts' }),
258
+ 'select',
259
+ true,
260
+ );
261
+ });
262
+
263
+ it('visits nested selects', () => {
264
+ const schema = createSchema({
265
+ User: {
266
+ name: 'User',
267
+ fields: {
268
+ id: createField('id', 'String'),
269
+ posts: createRelationField('posts', 'Post'),
270
+ },
271
+ uniqueFields: {},
272
+ idFields: ['id'],
273
+ },
274
+ Post: {
275
+ name: 'Post',
276
+ fields: {
277
+ id: createField('id', 'String'),
278
+ title: createField('title', 'String'),
279
+ comments: createRelationField('comments', 'Comment'),
280
+ },
281
+ uniqueFields: {},
282
+ idFields: ['id'],
283
+ },
284
+ Comment: {
285
+ name: 'Comment',
286
+ fields: {
287
+ id: createField('id', 'String'),
288
+ text: createField('text', 'String'),
289
+ },
290
+ uniqueFields: {},
291
+ idFields: ['id'],
292
+ },
293
+ });
294
+
295
+ const callback = vi.fn();
296
+ const visitor = new NestedReadVisitor(schema, { field: callback });
297
+
298
+ visitor.visit('User', {
299
+ select: {
300
+ posts: {
301
+ select: {
302
+ comments: true,
303
+ },
304
+ },
305
+ },
306
+ });
307
+
308
+ expect(callback).toHaveBeenCalledTimes(3);
309
+ expect(callback).toHaveBeenNthCalledWith(
310
+ 3,
311
+ 'Comment',
312
+ expect.objectContaining({ name: 'comments' }),
313
+ 'select',
314
+ true,
315
+ );
316
+ });
317
+
318
+ it('visits scalar fields in select', () => {
319
+ const schema = createSchema({
320
+ User: {
321
+ name: 'User',
322
+ fields: {
323
+ id: createField('id', 'String'),
324
+ name: createField('name', 'String'),
325
+ email: createField('email', 'String'),
326
+ },
327
+ uniqueFields: {},
328
+ idFields: ['id'],
329
+ },
330
+ });
331
+
332
+ const callback = vi.fn();
333
+ const visitor = new NestedReadVisitor(schema, { field: callback });
334
+
335
+ visitor.visit('User', {
336
+ select: {
337
+ id: true,
338
+ name: true,
339
+ },
340
+ });
341
+
342
+ expect(callback).toHaveBeenCalledTimes(3);
343
+ expect(callback).toHaveBeenNthCalledWith(
344
+ 2,
345
+ 'String',
346
+ expect.objectContaining({ name: 'id' }),
347
+ 'select',
348
+ true,
349
+ );
350
+ expect(callback).toHaveBeenNthCalledWith(
351
+ 3,
352
+ 'String',
353
+ expect.objectContaining({ name: 'name' }),
354
+ 'select',
355
+ true,
356
+ );
357
+ });
358
+ });
359
+
360
+ describe('_count handling', () => {
361
+ it('visits _count field', () => {
362
+ const schema = createSchema({
363
+ User: {
364
+ name: 'User',
365
+ fields: {
366
+ id: createField('id', 'String'),
367
+ posts: createRelationField('posts', 'Post'),
368
+ },
369
+ uniqueFields: {},
370
+ idFields: ['id'],
371
+ },
372
+ Post: {
373
+ name: 'Post',
374
+ fields: {
375
+ id: createField('id', 'String'),
376
+ },
377
+ uniqueFields: {},
378
+ idFields: ['id'],
379
+ },
380
+ });
381
+
382
+ const callback = vi.fn();
383
+ const visitor = new NestedReadVisitor(schema, { field: callback });
384
+
385
+ visitor.visit('User', {
386
+ include: {
387
+ _count: {
388
+ select: {
389
+ posts: true,
390
+ },
391
+ },
392
+ },
393
+ });
394
+
395
+ // Should visit root, _count recursion (same model, undefined kind), and posts within _count select
396
+ expect(callback).toHaveBeenCalledTimes(3);
397
+ expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, expect.any(Object));
398
+ // _count causes recursion on same model with undefined kind
399
+ expect(callback).toHaveBeenNthCalledWith(
400
+ 2,
401
+ 'User',
402
+ undefined,
403
+ undefined,
404
+ expect.objectContaining({ select: { posts: true } }),
405
+ );
406
+ // Then visits posts field
407
+ expect(callback).toHaveBeenNthCalledWith(
408
+ 3,
409
+ 'Post',
410
+ expect.objectContaining({ name: 'posts' }),
411
+ 'select',
412
+ true,
413
+ );
414
+ });
415
+
416
+ it('handles _count with nested structure', () => {
417
+ const schema = createSchema({
418
+ User: {
419
+ name: 'User',
420
+ fields: {
421
+ id: createField('id', 'String'),
422
+ posts: createRelationField('posts', 'Post'),
423
+ comments: createRelationField('comments', 'Comment'),
424
+ },
425
+ uniqueFields: {},
426
+ idFields: ['id'],
427
+ },
428
+ Post: {
429
+ name: 'Post',
430
+ fields: {
431
+ id: createField('id', 'String'),
432
+ },
433
+ uniqueFields: {},
434
+ idFields: ['id'],
435
+ },
436
+ Comment: {
437
+ name: 'Comment',
438
+ fields: {
439
+ id: createField('id', 'String'),
440
+ },
441
+ uniqueFields: {},
442
+ idFields: ['id'],
443
+ },
444
+ });
445
+
446
+ const callback = vi.fn();
447
+ const visitor = new NestedReadVisitor(schema, { field: callback });
448
+
449
+ visitor.visit('User', {
450
+ select: {
451
+ _count: {
452
+ select: {
453
+ posts: true,
454
+ comments: true,
455
+ },
456
+ },
457
+ },
458
+ });
459
+
460
+ expect(callback).toHaveBeenCalled();
461
+ });
462
+ });
463
+
464
+ describe('callback return value handling', () => {
465
+ it('stops visiting when callback returns false', () => {
466
+ const schema = createSchema({
467
+ User: {
468
+ name: 'User',
469
+ fields: {
470
+ id: createField('id', 'String'),
471
+ posts: createRelationField('posts', 'Post'),
472
+ },
473
+ uniqueFields: {},
474
+ idFields: ['id'],
475
+ },
476
+ Post: {
477
+ name: 'Post',
478
+ fields: {
479
+ id: createField('id', 'String'),
480
+ comments: createRelationField('comments', 'Comment'),
481
+ },
482
+ uniqueFields: {},
483
+ idFields: ['id'],
484
+ },
485
+ Comment: {
486
+ name: 'Comment',
487
+ fields: {
488
+ id: createField('id', 'String'),
489
+ },
490
+ uniqueFields: {},
491
+ idFields: ['id'],
492
+ },
493
+ });
494
+
495
+ const callback = vi.fn((_model, field) => {
496
+ // Return false when visiting posts to stop recursion
497
+ if (field?.name === 'posts') {
498
+ return false;
499
+ }
500
+ return true;
501
+ });
502
+
503
+ const visitor = new NestedReadVisitor(schema, { field: callback });
504
+
505
+ visitor.visit('User', {
506
+ include: {
507
+ posts: {
508
+ include: {
509
+ comments: true,
510
+ },
511
+ },
512
+ },
513
+ });
514
+
515
+ // Should visit User and posts, but not comments (stopped by returning false)
516
+ expect(callback).toHaveBeenCalledTimes(2);
517
+ expect(callback).toHaveBeenNthCalledWith(1, 'User', undefined, undefined, expect.any(Object));
518
+ expect(callback).toHaveBeenNthCalledWith(
519
+ 2,
520
+ 'Post',
521
+ expect.objectContaining({ name: 'posts' }),
522
+ 'include',
523
+ expect.any(Object),
524
+ );
525
+ });
526
+
527
+ it('continues visiting when callback returns undefined or true', () => {
528
+ const schema = createSchema({
529
+ User: {
530
+ name: 'User',
531
+ fields: {
532
+ id: createField('id', 'String'),
533
+ posts: createRelationField('posts', 'Post'),
534
+ },
535
+ uniqueFields: {},
536
+ idFields: ['id'],
537
+ },
538
+ Post: {
539
+ name: 'Post',
540
+ fields: {
541
+ id: createField('id', 'String'),
542
+ comments: createRelationField('comments', 'Comment'),
543
+ },
544
+ uniqueFields: {},
545
+ idFields: ['id'],
546
+ },
547
+ Comment: {
548
+ name: 'Comment',
549
+ fields: {
550
+ id: createField('id', 'String'),
551
+ },
552
+ uniqueFields: {},
553
+ idFields: ['id'],
554
+ },
555
+ });
556
+
557
+ const callback = vi.fn((_model, field) => {
558
+ if (field?.name === 'posts') {
559
+ return true; // Explicitly continue
560
+ }
561
+ return undefined;
562
+ });
563
+
564
+ const visitor = new NestedReadVisitor(schema, { field: callback });
565
+
566
+ visitor.visit('User', {
567
+ include: {
568
+ posts: {
569
+ include: {
570
+ comments: true,
571
+ },
572
+ },
573
+ },
574
+ });
575
+
576
+ // Should visit all three levels
577
+ expect(callback).toHaveBeenCalledTimes(3);
578
+ });
579
+ });
580
+
581
+ describe('mixed include and select', () => {
582
+ it('handles select inside include', () => {
583
+ const schema = createSchema({
584
+ User: {
585
+ name: 'User',
586
+ fields: {
587
+ id: createField('id', 'String'),
588
+ posts: createRelationField('posts', 'Post'),
589
+ },
590
+ uniqueFields: {},
591
+ idFields: ['id'],
592
+ },
593
+ Post: {
594
+ name: 'Post',
595
+ fields: {
596
+ id: createField('id', 'String'),
597
+ title: createField('title', 'String'),
598
+ content: createField('content', 'String'),
599
+ },
600
+ uniqueFields: {},
601
+ idFields: ['id'],
602
+ },
603
+ });
604
+
605
+ const callback = vi.fn();
606
+ const visitor = new NestedReadVisitor(schema, { field: callback });
607
+
608
+ visitor.visit('User', {
609
+ include: {
610
+ posts: {
611
+ select: {
612
+ title: true,
613
+ },
614
+ },
615
+ },
616
+ });
617
+
618
+ expect(callback).toHaveBeenCalledWith('User', undefined, undefined, expect.any(Object));
619
+ expect(callback).toHaveBeenCalledWith(
620
+ 'Post',
621
+ expect.objectContaining({ name: 'posts' }),
622
+ 'include',
623
+ expect.any(Object),
624
+ );
625
+ expect(callback).toHaveBeenCalledWith('String', expect.objectContaining({ name: 'title' }), 'select', true);
626
+ });
627
+
628
+ it('handles include inside select', () => {
629
+ const schema = createSchema({
630
+ User: {
631
+ name: 'User',
632
+ fields: {
633
+ id: createField('id', 'String'),
634
+ posts: createRelationField('posts', 'Post'),
635
+ },
636
+ uniqueFields: {},
637
+ idFields: ['id'],
638
+ },
639
+ Post: {
640
+ name: 'Post',
641
+ fields: {
642
+ id: createField('id', 'String'),
643
+ comments: createRelationField('comments', 'Comment'),
644
+ },
645
+ uniqueFields: {},
646
+ idFields: ['id'],
647
+ },
648
+ Comment: {
649
+ name: 'Comment',
650
+ fields: {
651
+ id: createField('id', 'String'),
652
+ },
653
+ uniqueFields: {},
654
+ idFields: ['id'],
655
+ },
656
+ });
657
+
658
+ const callback = vi.fn();
659
+ const visitor = new NestedReadVisitor(schema, { field: callback });
660
+
661
+ visitor.visit('User', {
662
+ select: {
663
+ posts: {
664
+ include: {
665
+ comments: true,
666
+ },
667
+ },
668
+ },
669
+ });
670
+
671
+ expect(callback).toHaveBeenCalledWith('User', undefined, undefined, expect.any(Object));
672
+ expect(callback).toHaveBeenCalledWith(
673
+ 'Post',
674
+ expect.objectContaining({ name: 'posts' }),
675
+ 'select',
676
+ expect.any(Object),
677
+ );
678
+ expect(callback).toHaveBeenCalledWith(
679
+ 'Comment',
680
+ expect.objectContaining({ name: 'comments' }),
681
+ 'include',
682
+ true,
683
+ );
684
+ });
685
+ });
686
+
687
+ describe('edge cases', () => {
688
+ it('handles fields not in schema gracefully', () => {
689
+ const schema = createSchema({
690
+ User: {
691
+ name: 'User',
692
+ fields: {
693
+ id: createField('id', 'String'),
694
+ },
695
+ uniqueFields: {},
696
+ idFields: ['id'],
697
+ },
698
+ });
699
+
700
+ const callback = vi.fn();
701
+ const visitor = new NestedReadVisitor(schema, { field: callback });
702
+
703
+ // Try to include a field that doesn't exist
704
+ visitor.visit('User', {
705
+ include: {
706
+ nonExistentField: true,
707
+ },
708
+ });
709
+
710
+ // Should only visit the root, not the non-existent field
711
+ expect(callback).toHaveBeenCalledTimes(1);
712
+ });
713
+
714
+ it('handles empty include object', () => {
715
+ const schema = createSchema({
716
+ User: {
717
+ name: 'User',
718
+ fields: {
719
+ id: createField('id', 'String'),
720
+ },
721
+ uniqueFields: {},
722
+ idFields: ['id'],
723
+ },
724
+ });
725
+
726
+ const callback = vi.fn();
727
+ const visitor = new NestedReadVisitor(schema, { field: callback });
728
+
729
+ visitor.visit('User', {
730
+ include: {},
731
+ });
732
+
733
+ expect(callback).toHaveBeenCalledTimes(1);
734
+ });
735
+
736
+ it('handles empty select object', () => {
737
+ const schema = createSchema({
738
+ User: {
739
+ name: 'User',
740
+ fields: {
741
+ id: createField('id', 'String'),
742
+ },
743
+ uniqueFields: {},
744
+ idFields: ['id'],
745
+ },
746
+ });
747
+
748
+ const callback = vi.fn();
749
+ const visitor = new NestedReadVisitor(schema, { field: callback });
750
+
751
+ visitor.visit('User', {
752
+ select: {},
753
+ });
754
+
755
+ expect(callback).toHaveBeenCalledTimes(1);
756
+ });
757
+
758
+ it('handles visitor with no callback', () => {
759
+ const schema = createSchema({
760
+ User: {
761
+ name: 'User',
762
+ fields: {
763
+ id: createField('id', 'String'),
764
+ posts: createRelationField('posts', 'Post'),
765
+ },
766
+ uniqueFields: {},
767
+ idFields: ['id'],
768
+ },
769
+ Post: {
770
+ name: 'Post',
771
+ fields: {
772
+ id: createField('id', 'String'),
773
+ },
774
+ uniqueFields: {},
775
+ idFields: ['id'],
776
+ },
777
+ });
778
+
779
+ const visitor = new NestedReadVisitor(schema, {});
780
+
781
+ // Should not throw
782
+ expect(() => {
783
+ visitor.visit('User', {
784
+ include: {
785
+ posts: true,
786
+ },
787
+ });
788
+ }).not.toThrow();
789
+ });
790
+
791
+ it('handles non-object select/include values', () => {
792
+ const schema = createSchema({
793
+ User: {
794
+ name: 'User',
795
+ fields: {
796
+ id: createField('id', 'String'),
797
+ },
798
+ uniqueFields: {},
799
+ idFields: ['id'],
800
+ },
801
+ });
802
+
803
+ const callback = vi.fn();
804
+ const visitor = new NestedReadVisitor(schema, { field: callback });
805
+
806
+ visitor.visit('User', {
807
+ include: 'not an object',
808
+ });
809
+
810
+ visitor.visit('User', {
811
+ select: null,
812
+ });
813
+
814
+ // Should handle gracefully
815
+ expect(callback).toHaveBeenCalledTimes(2);
816
+ });
817
+ });
818
+
819
+ describe('complex real-world scenarios', () => {
820
+ it('handles deeply nested blog post structure', () => {
821
+ const schema = createSchema({
822
+ User: {
823
+ name: 'User',
824
+ fields: {
825
+ id: createField('id', 'String'),
826
+ name: createField('name', 'String'),
827
+ posts: createRelationField('posts', 'Post'),
828
+ profile: createRelationField('profile', 'Profile'),
829
+ },
830
+ uniqueFields: {},
831
+ idFields: ['id'],
832
+ },
833
+ Post: {
834
+ name: 'Post',
835
+ fields: {
836
+ id: createField('id', 'String'),
837
+ title: createField('title', 'String'),
838
+ comments: createRelationField('comments', 'Comment'),
839
+ author: createRelationField('author', 'User'),
840
+ },
841
+ uniqueFields: {},
842
+ idFields: ['id'],
843
+ },
844
+ Comment: {
845
+ name: 'Comment',
846
+ fields: {
847
+ id: createField('id', 'String'),
848
+ text: createField('text', 'String'),
849
+ author: createRelationField('author', 'User'),
850
+ },
851
+ uniqueFields: {},
852
+ idFields: ['id'],
853
+ },
854
+ Profile: {
855
+ name: 'Profile',
856
+ fields: {
857
+ id: createField('id', 'String'),
858
+ bio: createField('bio', 'String'),
859
+ },
860
+ uniqueFields: {},
861
+ idFields: ['id'],
862
+ },
863
+ });
864
+
865
+ const visitedModels: string[] = [];
866
+ const callback: NestedReadVisitorCallback['field'] = (model) => {
867
+ visitedModels.push(model);
868
+ };
869
+
870
+ const visitor = new NestedReadVisitor(schema, { field: callback });
871
+
872
+ visitor.visit('User', {
873
+ include: {
874
+ posts: {
875
+ include: {
876
+ comments: {
877
+ include: {
878
+ author: {
879
+ select: {
880
+ name: true,
881
+ },
882
+ },
883
+ },
884
+ },
885
+ },
886
+ },
887
+ profile: true,
888
+ },
889
+ });
890
+
891
+ expect(visitedModels).toContain('User');
892
+ expect(visitedModels).toContain('Post');
893
+ expect(visitedModels).toContain('Comment');
894
+ expect(visitedModels).toContain('Profile');
895
+ expect(visitedModels.filter((m) => m === 'User').length).toBeGreaterThan(1); // User visited multiple times
896
+ });
897
+
898
+ it('collects all visited field names', () => {
899
+ const schema = createSchema({
900
+ User: {
901
+ name: 'User',
902
+ fields: {
903
+ id: createField('id', 'String'),
904
+ email: createField('email', 'String'),
905
+ posts: createRelationField('posts', 'Post'),
906
+ },
907
+ uniqueFields: {},
908
+ idFields: ['id'],
909
+ },
910
+ Post: {
911
+ name: 'Post',
912
+ fields: {
913
+ id: createField('id', 'String'),
914
+ title: createField('title', 'String'),
915
+ published: createField('published', 'Boolean'),
916
+ },
917
+ uniqueFields: {},
918
+ idFields: ['id'],
919
+ },
920
+ });
921
+
922
+ const fieldNames: string[] = [];
923
+ const callback: NestedReadVisitorCallback['field'] = (_model, field) => {
924
+ if (field) {
925
+ fieldNames.push(field.name);
926
+ }
927
+ };
928
+
929
+ const visitor = new NestedReadVisitor(schema, { field: callback });
930
+
931
+ visitor.visit('User', {
932
+ select: {
933
+ email: true,
934
+ posts: {
935
+ select: {
936
+ title: true,
937
+ published: true,
938
+ },
939
+ },
940
+ },
941
+ });
942
+
943
+ expect(fieldNames).toContain('email');
944
+ expect(fieldNames).toContain('posts');
945
+ expect(fieldNames).toContain('title');
946
+ expect(fieldNames).toContain('published');
947
+ });
948
+ });
949
+ });