@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,1533 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { Logger } from '../src/logging';
3
+ import { applyMutation } from '../src/mutator';
4
+ import { createField, createSchema } from './test-helpers';
5
+
6
+ describe('applyMutation', () => {
7
+ describe('basic validation', () => {
8
+ it('returns undefined for non-object query data', async () => {
9
+ const schema = createSchema({
10
+ User: {
11
+ name: 'User',
12
+ fields: {
13
+ id: createField('id', 'String'),
14
+ },
15
+ uniqueFields: {},
16
+ idFields: ['id'],
17
+ },
18
+ });
19
+
20
+ const result = await applyMutation('User', 'findMany', null, 'User', 'update', {}, schema, undefined);
21
+ expect(result).toBeUndefined();
22
+ });
23
+
24
+ it('returns undefined for primitive query data', async () => {
25
+ const schema = createSchema({
26
+ User: {
27
+ name: 'User',
28
+ fields: {
29
+ id: createField('id', 'String'),
30
+ },
31
+ uniqueFields: {},
32
+ idFields: ['id'],
33
+ },
34
+ });
35
+
36
+ const result = await applyMutation('User', 'findMany', 42, 'User', 'update', {}, schema, undefined);
37
+ expect(result).toBeUndefined();
38
+ });
39
+
40
+ it('returns undefined for non-find query operations', async () => {
41
+ const schema = createSchema({
42
+ User: {
43
+ name: 'User',
44
+ fields: {
45
+ id: createField('id', 'String'),
46
+ },
47
+ uniqueFields: {},
48
+ idFields: ['id'],
49
+ },
50
+ });
51
+
52
+ const queryData = [{ id: '1', name: 'John' }];
53
+ const result = await applyMutation('User', 'create', queryData, 'User', 'update', {}, schema, undefined);
54
+ expect(result).toBeUndefined();
55
+ });
56
+ });
57
+
58
+ describe('create mutations', () => {
59
+ it('adds new item to array with create', async () => {
60
+ const schema = createSchema({
61
+ User: {
62
+ name: 'User',
63
+ fields: {
64
+ id: createField('id', 'String'),
65
+ name: createField('name', 'String'),
66
+ },
67
+ uniqueFields: {},
68
+ idFields: ['id'],
69
+ },
70
+ });
71
+
72
+ const queryData = [
73
+ { id: '1', name: 'John' },
74
+ { id: '2', name: 'Jane' },
75
+ ];
76
+
77
+ const result = await applyMutation(
78
+ 'User',
79
+ 'findMany',
80
+ queryData,
81
+ 'User',
82
+ 'create',
83
+ { data: { name: 'Bob' } },
84
+ schema,
85
+ undefined,
86
+ );
87
+
88
+ expect(result).toBeDefined();
89
+ expect(Array.isArray(result)).toBe(true);
90
+ expect(result).toHaveLength(3);
91
+ expect(result?.[0]).toHaveProperty('name', 'Bob');
92
+ expect(result?.[0]).toHaveProperty('$optimistic', true);
93
+ });
94
+
95
+ it('generates auto-increment ID for Int type', async () => {
96
+ const schema = createSchema({
97
+ User: {
98
+ name: 'User',
99
+ fields: {
100
+ id: createField('id', 'Int'),
101
+ name: createField('name', 'String'),
102
+ },
103
+ uniqueFields: {},
104
+ idFields: ['id'],
105
+ },
106
+ });
107
+
108
+ const queryData = [
109
+ { id: 1, name: 'John' },
110
+ { id: 2, name: 'Jane' },
111
+ ];
112
+
113
+ const result = await applyMutation(
114
+ 'User',
115
+ 'findMany',
116
+ queryData,
117
+ 'User',
118
+ 'create',
119
+ { data: { name: 'Bob' } },
120
+ schema,
121
+ undefined,
122
+ );
123
+
124
+ expect(result?.[0]).toHaveProperty('id', 3);
125
+ });
126
+
127
+ it('generates UUID for String ID type', async () => {
128
+ const schema = createSchema({
129
+ User: {
130
+ name: 'User',
131
+ fields: {
132
+ id: createField('id', 'String'),
133
+ name: createField('name', 'String'),
134
+ },
135
+ uniqueFields: {},
136
+ idFields: ['id'],
137
+ },
138
+ });
139
+
140
+ const queryData = [{ id: 'uuid-1', name: 'John' }];
141
+
142
+ const result = await applyMutation(
143
+ 'User',
144
+ 'findMany',
145
+ queryData,
146
+ 'User',
147
+ 'create',
148
+ { data: { name: 'Bob' } },
149
+ schema,
150
+ undefined,
151
+ );
152
+
153
+ expect(result?.[0]).toHaveProperty('id');
154
+ expect(typeof result?.[0]?.id).toBe('string');
155
+ expect(result?.[0]?.id).toMatch(/^[0-9a-f-]+$/);
156
+ });
157
+
158
+ it('applies default values for fields', async () => {
159
+ const schema = createSchema({
160
+ User: {
161
+ name: 'User',
162
+ fields: {
163
+ id: createField('id', 'String'),
164
+ name: createField('name', 'String'),
165
+ role: {
166
+ name: 'role',
167
+ type: 'String',
168
+ optional: false,
169
+ attributes: [
170
+ {
171
+ name: '@default',
172
+ args: [{ value: { kind: 'literal', value: 'user' } }],
173
+ },
174
+ ],
175
+ },
176
+ },
177
+ uniqueFields: {},
178
+ idFields: ['id'],
179
+ },
180
+ });
181
+
182
+ const queryData: any[] = [];
183
+
184
+ const result = await applyMutation(
185
+ 'User',
186
+ 'findMany',
187
+ queryData,
188
+ 'User',
189
+ 'create',
190
+ { data: { name: 'Bob' } },
191
+ schema,
192
+ undefined,
193
+ );
194
+
195
+ expect(result?.[0]).toHaveProperty('role', 'user');
196
+ });
197
+
198
+ it('handles DateTime fields with @default', async () => {
199
+ const schema = createSchema({
200
+ User: {
201
+ name: 'User',
202
+ fields: {
203
+ id: createField('id', 'String'),
204
+ createdAt: {
205
+ name: 'createdAt',
206
+ type: 'DateTime',
207
+ optional: false,
208
+ attributes: [{ name: '@default' }],
209
+ },
210
+ },
211
+ uniqueFields: {},
212
+ idFields: ['id'],
213
+ },
214
+ });
215
+
216
+ const queryData: any[] = [];
217
+
218
+ const result = await applyMutation(
219
+ 'User',
220
+ 'findMany',
221
+ queryData,
222
+ 'User',
223
+ 'create',
224
+ { data: {} },
225
+ schema,
226
+ undefined,
227
+ );
228
+
229
+ expect(result?.[0]?.createdAt).toBeInstanceOf(Date);
230
+ });
231
+
232
+ it('handles DateTime fields with @updatedAt', async () => {
233
+ const schema = createSchema({
234
+ User: {
235
+ name: 'User',
236
+ fields: {
237
+ id: createField('id', 'String'),
238
+ updatedAt: {
239
+ name: 'updatedAt',
240
+ type: 'DateTime',
241
+ optional: false,
242
+ attributes: [{ name: '@updatedAt' }],
243
+ },
244
+ },
245
+ uniqueFields: {},
246
+ idFields: ['id'],
247
+ },
248
+ });
249
+
250
+ const queryData: any[] = [];
251
+
252
+ const result = await applyMutation(
253
+ 'User',
254
+ 'findMany',
255
+ queryData,
256
+ 'User',
257
+ 'create',
258
+ { data: {} },
259
+ schema,
260
+ undefined,
261
+ );
262
+
263
+ expect(result?.[0]?.updatedAt).toBeInstanceOf(Date);
264
+ });
265
+
266
+ it('does not apply create to non-array query data', async () => {
267
+ const schema = createSchema({
268
+ User: {
269
+ name: 'User',
270
+ fields: {
271
+ id: createField('id', 'String'),
272
+ name: createField('name', 'String'),
273
+ },
274
+ uniqueFields: {},
275
+ idFields: ['id'],
276
+ },
277
+ });
278
+
279
+ const queryData = { id: '1', name: 'John' };
280
+
281
+ const result = await applyMutation(
282
+ 'User',
283
+ 'findUnique',
284
+ queryData,
285
+ 'User',
286
+ 'create',
287
+ { data: { name: 'Bob' } },
288
+ schema,
289
+ undefined,
290
+ );
291
+
292
+ expect(result).toBeUndefined();
293
+ });
294
+
295
+ it('handles relation fields with connect', async () => {
296
+ const schema = createSchema({
297
+ Post: {
298
+ name: 'Post',
299
+ fields: {
300
+ id: createField('id', 'String'),
301
+ title: createField('title', 'String'),
302
+ userId: createField('userId', 'String'),
303
+ user: {
304
+ name: 'user',
305
+ type: 'User',
306
+ optional: false,
307
+ relation: {
308
+ fields: ['userId'],
309
+ references: ['id'],
310
+ opposite: 'posts',
311
+ },
312
+ },
313
+ },
314
+ uniqueFields: {},
315
+ idFields: ['id'],
316
+ },
317
+ User: {
318
+ name: 'User',
319
+ fields: {
320
+ id: createField('id', 'String'),
321
+ posts: {
322
+ name: 'posts',
323
+ type: 'Post',
324
+ optional: false,
325
+ relation: { opposite: 'user' },
326
+ },
327
+ },
328
+ uniqueFields: {},
329
+ idFields: ['id'],
330
+ },
331
+ });
332
+
333
+ const queryData: any[] = [];
334
+
335
+ const result = await applyMutation(
336
+ 'Post',
337
+ 'findMany',
338
+ queryData,
339
+ 'Post',
340
+ 'create',
341
+ {
342
+ data: {
343
+ title: 'New Post',
344
+ user: { connect: { id: 'user-123' } },
345
+ },
346
+ },
347
+ schema,
348
+ undefined,
349
+ );
350
+
351
+ expect(result?.[0]).toHaveProperty('userId', 'user-123');
352
+ });
353
+ });
354
+
355
+ describe('createMany mutations', () => {
356
+ it('adds multiple items to array with createMany', async () => {
357
+ const schema = createSchema({
358
+ User: {
359
+ name: 'User',
360
+ fields: {
361
+ id: createField('id', 'String'),
362
+ name: createField('name', 'String'),
363
+ },
364
+ uniqueFields: {},
365
+ idFields: ['id'],
366
+ },
367
+ });
368
+
369
+ const queryData = [{ id: '1', name: 'John' }];
370
+
371
+ const result = await applyMutation(
372
+ 'User',
373
+ 'findMany',
374
+ queryData,
375
+ 'User',
376
+ 'createMany',
377
+ {
378
+ data: [{ name: 'Bob' }, { name: 'Alice' }],
379
+ },
380
+ schema,
381
+ undefined,
382
+ );
383
+
384
+ expect(result).toBeDefined();
385
+ expect(Array.isArray(result)).toBe(true);
386
+ expect(result).toHaveLength(3);
387
+ expect(result?.[0]).toHaveProperty('name', 'Alice');
388
+ expect(result?.[1]).toHaveProperty('name', 'Bob');
389
+ });
390
+ });
391
+
392
+ describe('update mutations', () => {
393
+ it('updates matching single object', async () => {
394
+ const schema = createSchema({
395
+ User: {
396
+ name: 'User',
397
+ fields: {
398
+ id: createField('id', 'String'),
399
+ name: createField('name', 'String'),
400
+ },
401
+ uniqueFields: {},
402
+ idFields: ['id'],
403
+ },
404
+ });
405
+
406
+ const queryData = { id: '1', name: 'John' };
407
+
408
+ const result = await applyMutation(
409
+ 'User',
410
+ 'findUnique',
411
+ queryData,
412
+ 'User',
413
+ 'update',
414
+ {
415
+ where: { id: '1' },
416
+ data: { name: 'Johnny' },
417
+ },
418
+ schema,
419
+ undefined,
420
+ );
421
+
422
+ expect(result).toBeDefined();
423
+ expect(result).toHaveProperty('name', 'Johnny');
424
+ expect(result).toHaveProperty('$optimistic', true);
425
+ });
426
+
427
+ it('does not update non-matching object', async () => {
428
+ const schema = createSchema({
429
+ User: {
430
+ name: 'User',
431
+ fields: {
432
+ id: createField('id', 'String'),
433
+ name: createField('name', 'String'),
434
+ },
435
+ uniqueFields: {},
436
+ idFields: ['id'],
437
+ },
438
+ });
439
+
440
+ const queryData = { id: '1', name: 'John' };
441
+
442
+ const result = await applyMutation(
443
+ 'User',
444
+ 'findUnique',
445
+ queryData,
446
+ 'User',
447
+ 'update',
448
+ {
449
+ where: { id: '2' },
450
+ data: { name: 'Johnny' },
451
+ },
452
+ schema,
453
+ undefined,
454
+ );
455
+
456
+ expect(result).toBeUndefined();
457
+ });
458
+
459
+ it('updates items in array', async () => {
460
+ const schema = createSchema({
461
+ User: {
462
+ name: 'User',
463
+ fields: {
464
+ id: createField('id', 'String'),
465
+ name: createField('name', 'String'),
466
+ },
467
+ uniqueFields: {},
468
+ idFields: ['id'],
469
+ },
470
+ });
471
+
472
+ const queryData = [
473
+ { id: '1', name: 'John' },
474
+ { id: '2', name: 'Jane' },
475
+ ];
476
+
477
+ const result = await applyMutation(
478
+ 'User',
479
+ 'findMany',
480
+ queryData,
481
+ 'User',
482
+ 'update',
483
+ {
484
+ where: { id: '1' },
485
+ data: { name: 'Johnny' },
486
+ },
487
+ schema,
488
+ undefined,
489
+ );
490
+
491
+ expect(result).toBeDefined();
492
+ expect(Array.isArray(result)).toBe(true);
493
+ expect(result?.[0]).toHaveProperty('name', 'Johnny');
494
+ expect(result?.[0]).toHaveProperty('$optimistic', true);
495
+ expect(result?.[1]).toHaveProperty('name', 'Jane');
496
+ });
497
+
498
+ it('handles relation fields with connect in update', async () => {
499
+ const schema = createSchema({
500
+ Post: {
501
+ name: 'Post',
502
+ fields: {
503
+ id: createField('id', 'String'),
504
+ title: createField('title', 'String'),
505
+ userId: createField('userId', 'String'),
506
+ user: {
507
+ name: 'user',
508
+ type: 'User',
509
+ optional: false,
510
+ relation: {
511
+ fields: ['userId'],
512
+ references: ['id'],
513
+ opposite: 'posts',
514
+ },
515
+ },
516
+ },
517
+ uniqueFields: {},
518
+ idFields: ['id'],
519
+ },
520
+ });
521
+
522
+ const queryData = { id: '1', title: 'Post 1', userId: 'user-1' };
523
+
524
+ const result = await applyMutation(
525
+ 'Post',
526
+ 'findUnique',
527
+ queryData,
528
+ 'Post',
529
+ 'update',
530
+ {
531
+ where: { id: '1' },
532
+ data: {
533
+ user: { connect: { id: 'user-2' } },
534
+ },
535
+ },
536
+ schema,
537
+ undefined,
538
+ );
539
+
540
+ expect(result).toHaveProperty('userId', 'user-2');
541
+ });
542
+
543
+ it('skips optimistically updated items', async () => {
544
+ const schema = createSchema({
545
+ User: {
546
+ name: 'User',
547
+ fields: {
548
+ id: createField('id', 'String'),
549
+ name: createField('name', 'String'),
550
+ },
551
+ uniqueFields: {},
552
+ idFields: ['id'],
553
+ },
554
+ });
555
+
556
+ const queryData = [
557
+ { id: '1', name: 'John', $optimistic: true },
558
+ { id: '2', name: 'Jane' },
559
+ ];
560
+
561
+ const result = await applyMutation(
562
+ 'User',
563
+ 'findMany',
564
+ queryData,
565
+ 'User',
566
+ 'update',
567
+ {
568
+ where: { id: '1' },
569
+ data: { name: 'Johnny' },
570
+ },
571
+ schema,
572
+ undefined,
573
+ );
574
+
575
+ expect(result).toBeUndefined();
576
+ });
577
+
578
+ it('handles compound ID fields', async () => {
579
+ const schema = createSchema({
580
+ UserRole: {
581
+ name: 'UserRole',
582
+ fields: {
583
+ userId: createField('userId', 'String'),
584
+ roleId: createField('roleId', 'String'),
585
+ active: createField('active', 'Boolean'),
586
+ },
587
+ uniqueFields: {},
588
+ idFields: ['userId', 'roleId'],
589
+ },
590
+ });
591
+
592
+ const queryData = { userId: 'u1', roleId: 'r1', active: false };
593
+
594
+ const result = await applyMutation(
595
+ 'UserRole',
596
+ 'findUnique',
597
+ queryData,
598
+ 'UserRole',
599
+ 'update',
600
+ {
601
+ where: { userId: 'u1', roleId: 'r1' },
602
+ data: { active: true },
603
+ },
604
+ schema,
605
+ undefined,
606
+ );
607
+
608
+ expect(result).toHaveProperty('active', true);
609
+ expect(result).toHaveProperty('$optimistic', true);
610
+ });
611
+ });
612
+
613
+ describe('upsert mutations', () => {
614
+ it('updates existing item in array', async () => {
615
+ const schema = createSchema({
616
+ User: {
617
+ name: 'User',
618
+ fields: {
619
+ id: createField('id', 'String'),
620
+ name: createField('name', 'String'),
621
+ },
622
+ uniqueFields: {},
623
+ idFields: ['id'],
624
+ },
625
+ });
626
+
627
+ const queryData = [
628
+ { id: '1', name: 'John' },
629
+ { id: '2', name: 'Jane' },
630
+ ];
631
+
632
+ const result = await applyMutation(
633
+ 'User',
634
+ 'findMany',
635
+ queryData,
636
+ 'User',
637
+ 'upsert',
638
+ {
639
+ where: { id: '1' },
640
+ create: { name: 'Bob' },
641
+ update: { name: 'Johnny' },
642
+ },
643
+ schema,
644
+ undefined,
645
+ );
646
+
647
+ expect(result).toBeDefined();
648
+ expect(Array.isArray(result)).toBe(true);
649
+ expect(result?.[0]).toHaveProperty('name', 'Johnny');
650
+ expect(result?.[0]).toHaveProperty('$optimistic', true);
651
+ });
652
+
653
+ it('creates new item when not found in array', async () => {
654
+ const schema = createSchema({
655
+ User: {
656
+ name: 'User',
657
+ fields: {
658
+ id: createField('id', 'String'),
659
+ name: createField('name', 'String'),
660
+ },
661
+ uniqueFields: {},
662
+ idFields: ['id'],
663
+ },
664
+ });
665
+
666
+ const queryData = [{ id: '1', name: 'John' }];
667
+
668
+ const result = await applyMutation(
669
+ 'User',
670
+ 'findMany',
671
+ queryData,
672
+ 'User',
673
+ 'upsert',
674
+ {
675
+ where: { id: '2' },
676
+ create: { name: 'Bob' },
677
+ update: { name: 'Johnny' },
678
+ },
679
+ schema,
680
+ undefined,
681
+ );
682
+
683
+ expect(result).toBeDefined();
684
+ expect(Array.isArray(result)).toBe(true);
685
+ expect(result).toHaveLength(2);
686
+ expect(result?.[0]).toHaveProperty('name', 'Bob');
687
+ expect(result?.[0]).toHaveProperty('$optimistic', true);
688
+ });
689
+
690
+ it('updates single object when found', async () => {
691
+ const schema = createSchema({
692
+ User: {
693
+ name: 'User',
694
+ fields: {
695
+ id: createField('id', 'String'),
696
+ name: createField('name', 'String'),
697
+ },
698
+ uniqueFields: {},
699
+ idFields: ['id'],
700
+ },
701
+ });
702
+
703
+ const queryData = { id: '1', name: 'John' };
704
+
705
+ const result = await applyMutation(
706
+ 'User',
707
+ 'findUnique',
708
+ queryData,
709
+ 'User',
710
+ 'upsert',
711
+ {
712
+ where: { id: '1' },
713
+ create: { name: 'Bob' },
714
+ update: { name: 'Johnny' },
715
+ },
716
+ schema,
717
+ undefined,
718
+ );
719
+
720
+ expect(result).toBeDefined();
721
+ expect(result).toHaveProperty('name', 'Johnny');
722
+ expect(result).toHaveProperty('$optimistic', true);
723
+ });
724
+
725
+ it('does not create when single object does not match', async () => {
726
+ const schema = createSchema({
727
+ User: {
728
+ name: 'User',
729
+ fields: {
730
+ id: createField('id', 'String'),
731
+ name: createField('name', 'String'),
732
+ },
733
+ uniqueFields: {},
734
+ idFields: ['id'],
735
+ },
736
+ });
737
+
738
+ const queryData = { id: '1', name: 'John' };
739
+
740
+ const result = await applyMutation(
741
+ 'User',
742
+ 'findUnique',
743
+ queryData,
744
+ 'User',
745
+ 'upsert',
746
+ {
747
+ where: { id: '2' },
748
+ create: { name: 'Bob' },
749
+ update: { name: 'Johnny' },
750
+ },
751
+ schema,
752
+ undefined,
753
+ );
754
+
755
+ expect(result).toBeUndefined();
756
+ });
757
+ });
758
+
759
+ describe('delete mutations', () => {
760
+ it('deletes matching single object', async () => {
761
+ const schema = createSchema({
762
+ User: {
763
+ name: 'User',
764
+ fields: {
765
+ id: createField('id', 'String'),
766
+ name: createField('name', 'String'),
767
+ },
768
+ uniqueFields: {},
769
+ idFields: ['id'],
770
+ },
771
+ });
772
+
773
+ const queryData = { id: '1', name: 'John' };
774
+
775
+ const result = await applyMutation(
776
+ 'User',
777
+ 'findUnique',
778
+ queryData,
779
+ 'User',
780
+ 'delete',
781
+ { where: { id: '1' } },
782
+ schema,
783
+ undefined,
784
+ );
785
+
786
+ // Note: Currently returns undefined because null is falsy in the callback check
787
+ // This might be a bug in the implementation, but we test the current behavior
788
+ expect(result).toBeUndefined();
789
+ });
790
+
791
+ it('does not delete non-matching single object', async () => {
792
+ const schema = createSchema({
793
+ User: {
794
+ name: 'User',
795
+ fields: {
796
+ id: createField('id', 'String'),
797
+ name: createField('name', 'String'),
798
+ },
799
+ uniqueFields: {},
800
+ idFields: ['id'],
801
+ },
802
+ });
803
+
804
+ const queryData = { id: '1', name: 'John' };
805
+
806
+ const result = await applyMutation(
807
+ 'User',
808
+ 'findUnique',
809
+ queryData,
810
+ 'User',
811
+ 'delete',
812
+ { where: { id: '2' } },
813
+ schema,
814
+ undefined,
815
+ );
816
+
817
+ expect(result).toBeUndefined();
818
+ });
819
+
820
+ it('removes item from array', async () => {
821
+ const schema = createSchema({
822
+ User: {
823
+ name: 'User',
824
+ fields: {
825
+ id: createField('id', 'String'),
826
+ name: createField('name', 'String'),
827
+ },
828
+ uniqueFields: {},
829
+ idFields: ['id'],
830
+ },
831
+ });
832
+
833
+ const queryData = [
834
+ { id: '1', name: 'John' },
835
+ { id: '2', name: 'Jane' },
836
+ ];
837
+
838
+ const result = await applyMutation(
839
+ 'User',
840
+ 'findMany',
841
+ queryData,
842
+ 'User',
843
+ 'delete',
844
+ { where: { id: '1' } },
845
+ schema,
846
+ undefined,
847
+ );
848
+
849
+ expect(result).toBeDefined();
850
+ expect(Array.isArray(result)).toBe(true);
851
+ expect(result).toHaveLength(1);
852
+ expect(result?.[0]).toHaveProperty('id', '2');
853
+ });
854
+
855
+ it('deletes multiple matching items from array', async () => {
856
+ const schema = createSchema({
857
+ User: {
858
+ name: 'User',
859
+ fields: {
860
+ id: createField('id', 'String'),
861
+ name: createField('name', 'String'),
862
+ },
863
+ uniqueFields: {},
864
+ idFields: ['id'],
865
+ },
866
+ });
867
+
868
+ const queryData = [
869
+ { id: '1', name: 'John' },
870
+ { id: '1', name: 'John Duplicate' }, // duplicate ID
871
+ { id: '2', name: 'Jane' },
872
+ ];
873
+
874
+ const result = await applyMutation(
875
+ 'User',
876
+ 'findMany',
877
+ queryData,
878
+ 'User',
879
+ 'delete',
880
+ { where: { id: '1' } },
881
+ schema,
882
+ undefined,
883
+ );
884
+
885
+ expect(result).toBeDefined();
886
+ expect(Array.isArray(result)).toBe(true);
887
+ expect(result).toHaveLength(1);
888
+ expect(result?.[0]).toHaveProperty('id', '2');
889
+ });
890
+
891
+ it('does not delete from different model', async () => {
892
+ const schema = createSchema({
893
+ User: {
894
+ name: 'User',
895
+ fields: {
896
+ id: createField('id', 'String'),
897
+ },
898
+ uniqueFields: {},
899
+ idFields: ['id'],
900
+ },
901
+ Post: {
902
+ name: 'Post',
903
+ fields: {
904
+ id: createField('id', 'String'),
905
+ },
906
+ uniqueFields: {},
907
+ idFields: ['id'],
908
+ },
909
+ });
910
+
911
+ const queryData = [{ id: '1' }];
912
+
913
+ const result = await applyMutation(
914
+ 'User',
915
+ 'findMany',
916
+ queryData,
917
+ 'Post',
918
+ 'delete',
919
+ { where: { id: '1' } },
920
+ schema,
921
+ undefined,
922
+ );
923
+
924
+ expect(result).toBeUndefined();
925
+ });
926
+ });
927
+
928
+ describe('nested mutations', () => {
929
+ it('applies mutations to nested relation fields', async () => {
930
+ const schema = createSchema({
931
+ User: {
932
+ name: 'User',
933
+ fields: {
934
+ id: createField('id', 'String'),
935
+ name: createField('name', 'String'),
936
+ posts: {
937
+ name: 'posts',
938
+ type: 'Post',
939
+ optional: false,
940
+ relation: { opposite: 'user' },
941
+ },
942
+ },
943
+ uniqueFields: {},
944
+ idFields: ['id'],
945
+ },
946
+ Post: {
947
+ name: 'Post',
948
+ fields: {
949
+ id: createField('id', 'String'),
950
+ title: createField('title', 'String'),
951
+ },
952
+ uniqueFields: {},
953
+ idFields: ['id'],
954
+ },
955
+ });
956
+
957
+ const queryData = {
958
+ id: '1',
959
+ name: 'John',
960
+ posts: [
961
+ { id: 'p1', title: 'Post 1' },
962
+ { id: 'p2', title: 'Post 2' },
963
+ ],
964
+ };
965
+
966
+ const result = await applyMutation(
967
+ 'User',
968
+ 'findUnique',
969
+ queryData,
970
+ 'Post',
971
+ 'update',
972
+ {
973
+ where: { id: 'p1' },
974
+ data: { title: 'Updated Post 1' },
975
+ },
976
+ schema,
977
+ undefined,
978
+ );
979
+
980
+ expect(result).toBeDefined();
981
+ expect(result?.posts[0]).toHaveProperty('title', 'Updated Post 1');
982
+ expect(result?.posts[0]).toHaveProperty('$optimistic', true);
983
+ });
984
+
985
+ it('applies create to nested array', async () => {
986
+ const schema = createSchema({
987
+ User: {
988
+ name: 'User',
989
+ fields: {
990
+ id: createField('id', 'String'),
991
+ name: createField('name', 'String'),
992
+ posts: {
993
+ name: 'posts',
994
+ type: 'Post',
995
+ optional: false,
996
+ relation: { opposite: 'user' },
997
+ },
998
+ },
999
+ uniqueFields: {},
1000
+ idFields: ['id'],
1001
+ },
1002
+ Post: {
1003
+ name: 'Post',
1004
+ fields: {
1005
+ id: createField('id', 'String'),
1006
+ title: createField('title', 'String'),
1007
+ },
1008
+ uniqueFields: {},
1009
+ idFields: ['id'],
1010
+ },
1011
+ });
1012
+
1013
+ const queryData = {
1014
+ id: '1',
1015
+ name: 'John',
1016
+ posts: [{ id: 'p1', title: 'Post 1' }],
1017
+ };
1018
+
1019
+ const result = await applyMutation(
1020
+ 'User',
1021
+ 'findUnique',
1022
+ queryData,
1023
+ 'Post',
1024
+ 'create',
1025
+ {
1026
+ data: { title: 'New Post' },
1027
+ },
1028
+ schema,
1029
+ undefined,
1030
+ );
1031
+
1032
+ expect(result).toBeDefined();
1033
+ expect(result?.posts).toHaveLength(2);
1034
+ expect(result?.posts[0]).toHaveProperty('title', 'New Post');
1035
+ expect(result?.posts[0]).toHaveProperty('$optimistic', true);
1036
+ });
1037
+
1038
+ it('applies delete to nested array', async () => {
1039
+ const schema = createSchema({
1040
+ User: {
1041
+ name: 'User',
1042
+ fields: {
1043
+ id: createField('id', 'String'),
1044
+ name: createField('name', 'String'),
1045
+ posts: {
1046
+ name: 'posts',
1047
+ type: 'Post',
1048
+ optional: false,
1049
+ relation: { opposite: 'user' },
1050
+ },
1051
+ },
1052
+ uniqueFields: {},
1053
+ idFields: ['id'],
1054
+ },
1055
+ Post: {
1056
+ name: 'Post',
1057
+ fields: {
1058
+ id: createField('id', 'String'),
1059
+ title: createField('title', 'String'),
1060
+ },
1061
+ uniqueFields: {},
1062
+ idFields: ['id'],
1063
+ },
1064
+ });
1065
+
1066
+ const queryData = {
1067
+ id: '1',
1068
+ name: 'John',
1069
+ posts: [
1070
+ { id: 'p1', title: 'Post 1' },
1071
+ { id: 'p2', title: 'Post 2' },
1072
+ ],
1073
+ };
1074
+
1075
+ const result = await applyMutation(
1076
+ 'User',
1077
+ 'findUnique',
1078
+ queryData,
1079
+ 'Post',
1080
+ 'delete',
1081
+ { where: { id: 'p1' } },
1082
+ schema,
1083
+ undefined,
1084
+ );
1085
+
1086
+ expect(result).toBeDefined();
1087
+ expect(result?.posts).toHaveLength(1);
1088
+ expect(result?.posts[0]).toHaveProperty('id', 'p2');
1089
+ });
1090
+
1091
+ it('handles deeply nested relations', async () => {
1092
+ const schema = createSchema({
1093
+ User: {
1094
+ name: 'User',
1095
+ fields: {
1096
+ id: createField('id', 'String'),
1097
+ profile: {
1098
+ name: 'profile',
1099
+ type: 'Profile',
1100
+ optional: true,
1101
+ relation: { opposite: 'user' },
1102
+ },
1103
+ },
1104
+ uniqueFields: {},
1105
+ idFields: ['id'],
1106
+ },
1107
+ Profile: {
1108
+ name: 'Profile',
1109
+ fields: {
1110
+ id: createField('id', 'String'),
1111
+ bio: createField('bio', 'String'),
1112
+ settings: {
1113
+ name: 'settings',
1114
+ type: 'Settings',
1115
+ optional: true,
1116
+ relation: { opposite: 'profile' },
1117
+ },
1118
+ },
1119
+ uniqueFields: {},
1120
+ idFields: ['id'],
1121
+ },
1122
+ Settings: {
1123
+ name: 'Settings',
1124
+ fields: {
1125
+ id: createField('id', 'String'),
1126
+ theme: createField('theme', 'String'),
1127
+ },
1128
+ uniqueFields: {},
1129
+ idFields: ['id'],
1130
+ },
1131
+ });
1132
+
1133
+ const queryData = {
1134
+ id: 'u1',
1135
+ profile: {
1136
+ id: 'pr1',
1137
+ bio: 'Test bio',
1138
+ settings: {
1139
+ id: 's1',
1140
+ theme: 'light',
1141
+ },
1142
+ },
1143
+ };
1144
+
1145
+ const result = await applyMutation(
1146
+ 'User',
1147
+ 'findUnique',
1148
+ queryData,
1149
+ 'Settings',
1150
+ 'update',
1151
+ {
1152
+ where: { id: 's1' },
1153
+ data: { theme: 'dark' },
1154
+ },
1155
+ schema,
1156
+ undefined,
1157
+ );
1158
+
1159
+ expect(result).toBeDefined();
1160
+ expect(result?.profile?.settings).toHaveProperty('theme', 'dark');
1161
+ expect(result?.profile?.settings).toHaveProperty('$optimistic', true);
1162
+ });
1163
+ });
1164
+
1165
+ describe('logging', () => {
1166
+ it('logs create mutation when logger is provided', async () => {
1167
+ const schema = createSchema({
1168
+ User: {
1169
+ name: 'User',
1170
+ fields: {
1171
+ id: createField('id', 'String'),
1172
+ name: createField('name', 'String'),
1173
+ },
1174
+ uniqueFields: {},
1175
+ idFields: ['id'],
1176
+ },
1177
+ });
1178
+
1179
+ const logger = vi.fn() as Logger;
1180
+ const queryData: any[] = [];
1181
+
1182
+ await applyMutation(
1183
+ 'User',
1184
+ 'findMany',
1185
+ queryData,
1186
+ 'User',
1187
+ 'create',
1188
+ { data: { name: 'Bob' } },
1189
+ schema,
1190
+ logger,
1191
+ );
1192
+
1193
+ expect(logger).toHaveBeenCalledWith(expect.stringContaining('Applying optimistic create'));
1194
+ });
1195
+
1196
+ it('logs update mutation when logger is provided', async () => {
1197
+ const schema = createSchema({
1198
+ User: {
1199
+ name: 'User',
1200
+ fields: {
1201
+ id: createField('id', 'String'),
1202
+ name: createField('name', 'String'),
1203
+ },
1204
+ uniqueFields: {},
1205
+ idFields: ['id'],
1206
+ },
1207
+ });
1208
+
1209
+ const logger = vi.fn() as Logger;
1210
+ const queryData = { id: '1', name: 'John' };
1211
+
1212
+ await applyMutation(
1213
+ 'User',
1214
+ 'findUnique',
1215
+ queryData,
1216
+ 'User',
1217
+ 'update',
1218
+ {
1219
+ where: { id: '1' },
1220
+ data: { name: 'Johnny' },
1221
+ },
1222
+ schema,
1223
+ logger,
1224
+ );
1225
+
1226
+ expect(logger).toHaveBeenCalledWith(expect.stringContaining('Applying optimistic update'));
1227
+ });
1228
+
1229
+ it('logs delete mutation when logger is provided', async () => {
1230
+ const schema = createSchema({
1231
+ User: {
1232
+ name: 'User',
1233
+ fields: {
1234
+ id: createField('id', 'String'),
1235
+ name: createField('name', 'String'),
1236
+ },
1237
+ uniqueFields: {},
1238
+ idFields: ['id'],
1239
+ },
1240
+ });
1241
+
1242
+ const logger = vi.fn() as Logger;
1243
+ const queryData = { id: '1', name: 'John' };
1244
+
1245
+ await applyMutation('User', 'findUnique', queryData, 'User', 'delete', { where: { id: '1' } }, schema, logger);
1246
+
1247
+ expect(logger).toHaveBeenCalledWith(expect.stringContaining('Applying optimistic delete'));
1248
+ });
1249
+ });
1250
+
1251
+ describe('edge cases', () => {
1252
+ it('handles empty array', async () => {
1253
+ const schema = createSchema({
1254
+ User: {
1255
+ name: 'User',
1256
+ fields: {
1257
+ id: createField('id', 'String'),
1258
+ },
1259
+ uniqueFields: {},
1260
+ idFields: ['id'],
1261
+ },
1262
+ });
1263
+
1264
+ const queryData: any[] = [];
1265
+
1266
+ const result = await applyMutation(
1267
+ 'User',
1268
+ 'findMany',
1269
+ queryData,
1270
+ 'User',
1271
+ 'update',
1272
+ { where: { id: '1' }, data: {} },
1273
+ schema,
1274
+ undefined,
1275
+ );
1276
+
1277
+ expect(result).toBeUndefined();
1278
+ });
1279
+
1280
+ it('handles null nested relation', async () => {
1281
+ const schema = createSchema({
1282
+ User: {
1283
+ name: 'User',
1284
+ fields: {
1285
+ id: createField('id', 'String'),
1286
+ profile: {
1287
+ name: 'profile',
1288
+ type: 'Profile',
1289
+ optional: true,
1290
+ relation: { opposite: 'user' },
1291
+ },
1292
+ },
1293
+ uniqueFields: {},
1294
+ idFields: ['id'],
1295
+ },
1296
+ Profile: {
1297
+ name: 'Profile',
1298
+ fields: {
1299
+ id: createField('id', 'String'),
1300
+ },
1301
+ uniqueFields: {},
1302
+ idFields: ['id'],
1303
+ },
1304
+ });
1305
+
1306
+ const queryData = {
1307
+ id: 'u1',
1308
+ profile: null,
1309
+ };
1310
+
1311
+ const result = await applyMutation(
1312
+ 'User',
1313
+ 'findUnique',
1314
+ queryData,
1315
+ 'Profile',
1316
+ 'update',
1317
+ { where: { id: 'p1' }, data: {} },
1318
+ schema,
1319
+ undefined,
1320
+ );
1321
+
1322
+ expect(result).toBeUndefined();
1323
+ });
1324
+
1325
+ it('does not mutate original data', async () => {
1326
+ const schema = createSchema({
1327
+ User: {
1328
+ name: 'User',
1329
+ fields: {
1330
+ id: createField('id', 'String'),
1331
+ name: createField('name', 'String'),
1332
+ },
1333
+ uniqueFields: {},
1334
+ idFields: ['id'],
1335
+ },
1336
+ });
1337
+
1338
+ const original = { id: '1', name: 'John' };
1339
+ const queryData = { ...original };
1340
+
1341
+ await applyMutation(
1342
+ 'User',
1343
+ 'findUnique',
1344
+ queryData,
1345
+ 'User',
1346
+ 'update',
1347
+ {
1348
+ where: { id: '1' },
1349
+ data: { name: 'Johnny' },
1350
+ },
1351
+ schema,
1352
+ undefined,
1353
+ );
1354
+
1355
+ expect(queryData).toEqual(original);
1356
+ });
1357
+
1358
+ it('handles BigInt ID fields', async () => {
1359
+ const schema = createSchema({
1360
+ User: {
1361
+ name: 'User',
1362
+ fields: {
1363
+ id: createField('id', 'BigInt'),
1364
+ name: createField('name', 'String'),
1365
+ },
1366
+ uniqueFields: {},
1367
+ idFields: ['id'],
1368
+ },
1369
+ });
1370
+
1371
+ const queryData = [
1372
+ { id: 1, name: 'John' },
1373
+ { id: 2, name: 'Jane' },
1374
+ ];
1375
+
1376
+ const result = await applyMutation(
1377
+ 'User',
1378
+ 'findMany',
1379
+ queryData,
1380
+ 'User',
1381
+ 'create',
1382
+ { data: { name: 'Bob' } },
1383
+ schema,
1384
+ undefined,
1385
+ );
1386
+
1387
+ expect(result?.[0]).toHaveProperty('id', 3);
1388
+ });
1389
+
1390
+ it('handles model without id fields', async () => {
1391
+ const schema = createSchema({
1392
+ User: {
1393
+ name: 'User',
1394
+ fields: {
1395
+ name: createField('name', 'String'),
1396
+ },
1397
+ uniqueFields: {},
1398
+ idFields: [],
1399
+ },
1400
+ });
1401
+
1402
+ const queryData = { name: 'John' };
1403
+
1404
+ const result = await applyMutation(
1405
+ 'User',
1406
+ 'findFirst',
1407
+ queryData,
1408
+ 'User',
1409
+ 'update',
1410
+ {
1411
+ where: {},
1412
+ data: { name: 'Johnny' },
1413
+ },
1414
+ schema,
1415
+ undefined,
1416
+ );
1417
+
1418
+ expect(result).toBeUndefined();
1419
+ });
1420
+
1421
+ it('handles invalid mutation args', async () => {
1422
+ const schema = createSchema({
1423
+ User: {
1424
+ name: 'User',
1425
+ fields: {
1426
+ id: createField('id', 'String'),
1427
+ },
1428
+ uniqueFields: {},
1429
+ idFields: ['id'],
1430
+ },
1431
+ });
1432
+
1433
+ const queryData = { id: '1' };
1434
+
1435
+ // Missing where
1436
+ const result1 = await applyMutation(
1437
+ 'User',
1438
+ 'findUnique',
1439
+ queryData,
1440
+ 'User',
1441
+ 'update',
1442
+ { data: {} },
1443
+ schema,
1444
+ undefined,
1445
+ );
1446
+ expect(result1).toBeUndefined();
1447
+
1448
+ // Missing data
1449
+ const result2 = await applyMutation(
1450
+ 'User',
1451
+ 'findUnique',
1452
+ queryData,
1453
+ 'User',
1454
+ 'update',
1455
+ { where: { id: '1' } },
1456
+ schema,
1457
+ undefined,
1458
+ );
1459
+ expect(result2).toBeUndefined();
1460
+ });
1461
+
1462
+ it('handles unknown fields in mutation data', async () => {
1463
+ const schema = createSchema({
1464
+ User: {
1465
+ name: 'User',
1466
+ fields: {
1467
+ id: createField('id', 'String'),
1468
+ name: createField('name', 'String'),
1469
+ },
1470
+ uniqueFields: {},
1471
+ idFields: ['id'],
1472
+ },
1473
+ });
1474
+
1475
+ const queryData = { id: '1', name: 'John' };
1476
+
1477
+ const result = await applyMutation(
1478
+ 'User',
1479
+ 'findUnique',
1480
+ queryData,
1481
+ 'User',
1482
+ 'update',
1483
+ {
1484
+ where: { id: '1' },
1485
+ data: {
1486
+ name: 'Johnny',
1487
+ unknownField: 'value',
1488
+ },
1489
+ },
1490
+ schema,
1491
+ undefined,
1492
+ );
1493
+
1494
+ expect(result).toBeDefined();
1495
+ expect(result).toHaveProperty('name', 'Johnny');
1496
+ expect(result).not.toHaveProperty('unknownField');
1497
+ });
1498
+
1499
+ it('handles arrays with mixed types', async () => {
1500
+ const schema = createSchema({
1501
+ User: {
1502
+ name: 'User',
1503
+ fields: {
1504
+ id: createField('id', 'String'),
1505
+ name: createField('name', 'String'),
1506
+ },
1507
+ uniqueFields: {},
1508
+ idFields: ['id'],
1509
+ },
1510
+ });
1511
+
1512
+ const queryData = [{ id: '1', name: 'John' }, null, 'invalid', { id: '2', name: 'Jane' }];
1513
+
1514
+ const result = await applyMutation(
1515
+ 'User',
1516
+ 'findMany',
1517
+ queryData,
1518
+ 'User',
1519
+ 'update',
1520
+ {
1521
+ where: { id: '1' },
1522
+ data: { name: 'Johnny' },
1523
+ },
1524
+ schema,
1525
+ undefined,
1526
+ );
1527
+
1528
+ // Should handle only valid objects
1529
+ expect(result).toBeDefined();
1530
+ expect(result?.[0]).toHaveProperty('name', 'Johnny');
1531
+ });
1532
+ });
1533
+ });