@zenstackhq/client-helpers 3.1.1 → 3.2.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.
@@ -1,743 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import type { Logger } from '../src/logging';
3
- import { createOptimisticUpdater } from '../src/optimistic';
4
- import type { QueryInfo } from '../src/types';
5
- import { createField, createRelationField, createSchema } from './test-helpers';
6
-
7
- describe('Optimistic update tests', () => {
8
- describe('createOptimisticUpdater', () => {
9
- it('applies default optimistic update to matching queries', async () => {
10
- const schema = createSchema({
11
- User: {
12
- name: 'User',
13
- fields: {
14
- id: createField('id', 'String'),
15
- name: createField('name', 'String'),
16
- },
17
- uniqueFields: {},
18
- idFields: ['id'],
19
- },
20
- });
21
-
22
- const updateDataMock = vi.fn();
23
- const queries: QueryInfo[] = [
24
- {
25
- model: 'User',
26
- operation: 'findMany',
27
- args: {},
28
- data: [
29
- { id: '1', name: 'John' },
30
- { id: '2', name: 'Jane' },
31
- ],
32
- optimisticUpdate: true,
33
- updateData: updateDataMock,
34
- },
35
- ];
36
-
37
- const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined);
38
-
39
- await updater({ where: { id: '1' }, data: { name: 'Johnny' } });
40
-
41
- // Should update the cache with the optimistic data
42
- expect(updateDataMock).toHaveBeenCalledTimes(1);
43
- const updatedData = updateDataMock.mock.calls[0]?.[0];
44
- expect(updatedData).toBeDefined();
45
- expect(Array.isArray(updatedData)).toBe(true);
46
- });
47
-
48
- it('skips queries with optimisticUpdate set to false', async () => {
49
- const schema = createSchema({
50
- User: {
51
- name: 'User',
52
- fields: {
53
- id: createField('id', 'String'),
54
- name: createField('name', 'String'),
55
- },
56
- uniqueFields: {},
57
- idFields: ['id'],
58
- },
59
- });
60
-
61
- const updateDataMock = vi.fn();
62
- const queries: QueryInfo[] = [
63
- {
64
- model: 'User',
65
- operation: 'findMany',
66
- args: {},
67
- data: [{ id: '1', name: 'John' }],
68
- optimisticUpdate: false, // opted out
69
- updateData: updateDataMock,
70
- },
71
- ];
72
-
73
- const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined);
74
-
75
- await updater({ where: { id: '1' }, data: { name: 'Johnny' } });
76
-
77
- // Should not update the cache
78
- expect(updateDataMock).not.toHaveBeenCalled();
79
- });
80
-
81
- it('uses custom optimisticDataProvider when provided', async () => {
82
- const schema = createSchema({
83
- User: {
84
- name: 'User',
85
- fields: {
86
- id: createField('id', 'String'),
87
- name: createField('name', 'String'),
88
- },
89
- uniqueFields: {},
90
- idFields: ['id'],
91
- },
92
- });
93
-
94
- const customData = [{ id: '1', name: 'Custom', $optimistic: true }];
95
- const optimisticDataProvider = vi.fn(() => ({
96
- kind: 'Update' as const,
97
- data: customData,
98
- }));
99
-
100
- const updateDataMock = vi.fn();
101
- const queries: QueryInfo[] = [
102
- {
103
- model: 'User',
104
- operation: 'findMany',
105
- args: {},
106
- data: [{ id: '1', name: 'John' }],
107
- optimisticUpdate: true,
108
- updateData: updateDataMock,
109
- },
110
- ];
111
-
112
- const updater = createOptimisticUpdater(
113
- 'User',
114
- 'update',
115
- schema,
116
- { optimisticDataProvider },
117
- () => queries,
118
- undefined,
119
- );
120
-
121
- await updater({ where: { id: '1' }, data: { name: 'Johnny' } });
122
-
123
- // Provider should be called
124
- expect(optimisticDataProvider).toHaveBeenCalledWith({
125
- queryModel: 'User',
126
- queryOperation: 'findMany',
127
- queryArgs: {},
128
- currentData: [{ id: '1', name: 'John' }],
129
- mutationArgs: { where: { id: '1' }, data: { name: 'Johnny' } },
130
- });
131
-
132
- // Should update with custom data
133
- expect(updateDataMock).toHaveBeenCalledWith(customData, true);
134
- });
135
-
136
- it('skips update when provider returns Skip', async () => {
137
- const schema = createSchema({
138
- User: {
139
- name: 'User',
140
- fields: {
141
- id: createField('id', 'String'),
142
- },
143
- uniqueFields: {},
144
- idFields: ['id'],
145
- },
146
- });
147
-
148
- const optimisticDataProvider = vi.fn(() => ({
149
- kind: 'Skip' as const,
150
- }));
151
-
152
- const updateDataMock = vi.fn();
153
- const queries: QueryInfo[] = [
154
- {
155
- model: 'User',
156
- operation: 'findMany',
157
- args: {},
158
- data: [{ id: '1' }],
159
- optimisticUpdate: true,
160
- updateData: updateDataMock,
161
- },
162
- ];
163
-
164
- const updater = createOptimisticUpdater(
165
- 'User',
166
- 'update',
167
- schema,
168
- { optimisticDataProvider },
169
- () => queries,
170
- undefined,
171
- );
172
-
173
- await updater({ where: { id: '1' }, data: {} });
174
-
175
- // Provider should be called
176
- expect(optimisticDataProvider).toHaveBeenCalled();
177
-
178
- // Should not update
179
- expect(updateDataMock).not.toHaveBeenCalled();
180
- });
181
-
182
- it('proceeds with default update when provider returns ProceedDefault', async () => {
183
- const schema = createSchema({
184
- User: {
185
- name: 'User',
186
- fields: {
187
- id: createField('id', 'String'),
188
- name: createField('name', 'String'),
189
- },
190
- uniqueFields: {},
191
- idFields: ['id'],
192
- },
193
- });
194
-
195
- const optimisticDataProvider = vi.fn(() => ({
196
- kind: 'ProceedDefault' as const,
197
- }));
198
-
199
- const updateDataMock = vi.fn();
200
- const queries: QueryInfo[] = [
201
- {
202
- model: 'User',
203
- operation: 'findMany',
204
- args: {},
205
- data: [{ id: '1', name: 'John' }],
206
- optimisticUpdate: true,
207
- updateData: updateDataMock,
208
- },
209
- ];
210
-
211
- const updater = createOptimisticUpdater(
212
- 'User',
213
- 'update',
214
- schema,
215
- { optimisticDataProvider },
216
- () => queries,
217
- undefined,
218
- );
219
-
220
- await updater({ where: { id: '1' }, data: { name: 'Johnny' } });
221
-
222
- // Provider should be called
223
- expect(optimisticDataProvider).toHaveBeenCalled();
224
-
225
- // Should proceed with default update
226
- expect(updateDataMock).toHaveBeenCalled();
227
- });
228
-
229
- it('handles async optimisticDataProvider', async () => {
230
- const schema = createSchema({
231
- User: {
232
- name: 'User',
233
- fields: {
234
- id: createField('id', 'String'),
235
- },
236
- uniqueFields: {},
237
- idFields: ['id'],
238
- },
239
- });
240
-
241
- const optimisticDataProvider = vi.fn(async () => {
242
- await new Promise((resolve) => setTimeout(resolve, 10));
243
- return {
244
- kind: 'Update' as const,
245
- data: [{ id: '1', $optimistic: true }],
246
- };
247
- });
248
-
249
- const updateDataMock = vi.fn();
250
- const queries: QueryInfo[] = [
251
- {
252
- model: 'User',
253
- operation: 'findMany',
254
- args: {},
255
- data: [],
256
- optimisticUpdate: true,
257
- updateData: updateDataMock,
258
- },
259
- ];
260
-
261
- const updater = createOptimisticUpdater(
262
- 'User',
263
- 'update',
264
- schema,
265
- { optimisticDataProvider },
266
- () => queries,
267
- undefined,
268
- );
269
-
270
- await updater({ where: { id: '1' }, data: {} });
271
-
272
- expect(optimisticDataProvider).toHaveBeenCalled();
273
- expect(updateDataMock).toHaveBeenCalled();
274
- });
275
-
276
- it('processes multiple queries', async () => {
277
- const schema = createSchema({
278
- User: {
279
- name: 'User',
280
- fields: {
281
- id: createField('id', 'String'),
282
- name: createField('name', 'String'),
283
- },
284
- uniqueFields: {},
285
- idFields: ['id'],
286
- },
287
- });
288
-
289
- const updateData1 = vi.fn();
290
- const updateData2 = vi.fn();
291
- const queries: QueryInfo[] = [
292
- {
293
- model: 'User',
294
- operation: 'findMany',
295
- args: {},
296
- data: [{ id: '1', name: 'John' }],
297
- optimisticUpdate: true,
298
- updateData: updateData1,
299
- },
300
- {
301
- model: 'User',
302
- operation: 'findUnique',
303
- args: { where: { id: '1' } },
304
- data: { id: '1', name: 'John' },
305
- optimisticUpdate: true,
306
- updateData: updateData2,
307
- },
308
- ];
309
-
310
- const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined);
311
-
312
- await updater({ where: { id: '1' }, data: { name: 'Johnny' } });
313
-
314
- // Both queries should be updated
315
- expect(updateData1).toHaveBeenCalled();
316
- expect(updateData2).toHaveBeenCalled();
317
- });
318
-
319
- it('logs when logging is enabled', async () => {
320
- const schema = createSchema({
321
- User: {
322
- name: 'User',
323
- fields: {
324
- id: createField('id', 'String'),
325
- name: createField('name', 'String'),
326
- },
327
- uniqueFields: {},
328
- idFields: ['id'],
329
- },
330
- });
331
-
332
- const logger = vi.fn() as Logger;
333
- const updateDataMock = vi.fn();
334
- const queries: QueryInfo[] = [
335
- {
336
- model: 'User',
337
- operation: 'findMany',
338
- args: {},
339
- data: [{ id: '1', name: 'John' }],
340
- optimisticUpdate: true,
341
- updateData: updateDataMock,
342
- },
343
- ];
344
-
345
- const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, logger);
346
-
347
- await updater({ where: { id: '1' }, data: { name: 'Johnny' } });
348
-
349
- // Logger should be called
350
- expect(logger).toHaveBeenCalled();
351
- expect(logger).toHaveBeenCalledWith(expect.stringContaining('Optimistically updating'));
352
- });
353
-
354
- it('logs when skipping due to opt-out', async () => {
355
- const schema = createSchema({
356
- User: {
357
- name: 'User',
358
- fields: {
359
- id: createField('id', 'String'),
360
- },
361
- uniqueFields: {},
362
- idFields: ['id'],
363
- },
364
- });
365
-
366
- const logger = vi.fn() as Logger;
367
- const updateDataMock = vi.fn();
368
- const queries: QueryInfo[] = [
369
- {
370
- model: 'User',
371
- operation: 'findMany',
372
- args: {},
373
- data: [],
374
- optimisticUpdate: false,
375
- updateData: updateDataMock,
376
- },
377
- ];
378
-
379
- const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, logger);
380
-
381
- await updater({ where: { id: '1' }, data: {} });
382
-
383
- // Logger should be called with skip message
384
- expect(logger).toHaveBeenCalledWith(expect.stringContaining('Skipping optimistic update'));
385
- expect(logger).toHaveBeenCalledWith(expect.stringContaining('opt-out'));
386
- });
387
-
388
- it('logs when skipping due to provider', async () => {
389
- const schema = createSchema({
390
- User: {
391
- name: 'User',
392
- fields: {
393
- id: createField('id', 'String'),
394
- },
395
- uniqueFields: {},
396
- idFields: ['id'],
397
- },
398
- });
399
-
400
- const logger = vi.fn() as Logger;
401
- const optimisticDataProvider = vi.fn(() => ({
402
- kind: 'Skip' as const,
403
- }));
404
-
405
- const updateDataMock = vi.fn();
406
- const queries: QueryInfo[] = [
407
- {
408
- model: 'User',
409
- operation: 'findMany',
410
- args: {},
411
- data: [],
412
- optimisticUpdate: true,
413
- updateData: updateDataMock,
414
- },
415
- ];
416
-
417
- const updater = createOptimisticUpdater(
418
- 'User',
419
- 'update',
420
- schema,
421
- { optimisticDataProvider },
422
- () => queries,
423
- logger,
424
- );
425
-
426
- await updater({ where: { id: '1' }, data: {} });
427
-
428
- // Logger should be called with skip message
429
- expect(logger).toHaveBeenCalledWith(expect.stringContaining('Skipping optimistic updating'));
430
- expect(logger).toHaveBeenCalledWith(expect.stringContaining('provider'));
431
- });
432
-
433
- it('logs when updating due to provider', async () => {
434
- const schema = createSchema({
435
- User: {
436
- name: 'User',
437
- fields: {
438
- id: createField('id', 'String'),
439
- },
440
- uniqueFields: {},
441
- idFields: ['id'],
442
- },
443
- });
444
-
445
- const logger = vi.fn() as Logger;
446
- const optimisticDataProvider = vi.fn(() => ({
447
- kind: 'Update' as const,
448
- data: [],
449
- }));
450
-
451
- const updateDataMock = vi.fn();
452
- const queries: QueryInfo[] = [
453
- {
454
- model: 'User',
455
- operation: 'findMany',
456
- args: {},
457
- data: [],
458
- optimisticUpdate: true,
459
- updateData: updateDataMock,
460
- },
461
- ];
462
-
463
- const updater = createOptimisticUpdater(
464
- 'User',
465
- 'update',
466
- schema,
467
- { optimisticDataProvider },
468
- () => queries,
469
- logger,
470
- );
471
-
472
- await updater({ where: { id: '1' }, data: {} });
473
-
474
- // Logger should be called with update message
475
- expect(logger).toHaveBeenCalledWith(expect.stringContaining('Optimistically updating'));
476
- expect(logger).toHaveBeenCalledWith(expect.stringContaining('provider'));
477
- });
478
-
479
- it('handles empty query list', async () => {
480
- const schema = createSchema({
481
- User: {
482
- name: 'User',
483
- fields: {
484
- id: createField('id', 'String'),
485
- },
486
- uniqueFields: {},
487
- idFields: ['id'],
488
- },
489
- });
490
-
491
- const queries: QueryInfo[] = [];
492
-
493
- const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined);
494
-
495
- // Should not throw
496
- await expect(updater({ where: { id: '1' }, data: {} })).resolves.toBeUndefined();
497
- });
498
-
499
- it('handles mutations on related models', async () => {
500
- const schema = createSchema({
501
- User: {
502
- name: 'User',
503
- fields: {
504
- id: createField('id', 'String'),
505
- posts: createRelationField('posts', 'Post'),
506
- },
507
- uniqueFields: {},
508
- idFields: ['id'],
509
- },
510
- Post: {
511
- name: 'Post',
512
- fields: {
513
- id: createField('id', 'String'),
514
- title: createField('title', 'String'),
515
- userId: createField('userId', 'String'),
516
- },
517
- uniqueFields: {},
518
- idFields: ['id'],
519
- },
520
- });
521
-
522
- const updateDataMock = vi.fn();
523
- const queries: QueryInfo[] = [
524
- {
525
- model: 'Post',
526
- operation: 'findMany',
527
- args: {},
528
- data: [
529
- { id: '1', title: 'Post 1', userId: '1' },
530
- { id: '2', title: 'Post 2', userId: '2' },
531
- ],
532
- optimisticUpdate: true,
533
- updateData: updateDataMock,
534
- },
535
- ];
536
-
537
- const updater = createOptimisticUpdater('Post', 'update', schema, {}, () => queries, undefined);
538
-
539
- await updater({ where: { id: '1' }, data: { title: 'Updated Post 1' } });
540
-
541
- // Should update the cache
542
- expect(updateDataMock).toHaveBeenCalled();
543
- });
544
-
545
- it('extracts mutation args from first argument', async () => {
546
- const schema = createSchema({
547
- User: {
548
- name: 'User',
549
- fields: {
550
- id: createField('id', 'String'),
551
- name: createField('name', 'String'),
552
- },
553
- uniqueFields: {},
554
- idFields: ['id'],
555
- },
556
- });
557
-
558
- let capturedMutationArgs: any;
559
- const optimisticDataProvider = vi.fn((args) => {
560
- capturedMutationArgs = args.mutationArgs;
561
- return { kind: 'Skip' as const };
562
- });
563
-
564
- const queries: QueryInfo[] = [
565
- {
566
- model: 'User',
567
- operation: 'findMany',
568
- args: {},
569
- data: [],
570
- optimisticUpdate: true,
571
- updateData: vi.fn(),
572
- },
573
- ];
574
-
575
- const updater = createOptimisticUpdater(
576
- 'User',
577
- 'update',
578
- schema,
579
- { optimisticDataProvider },
580
- () => queries,
581
- undefined,
582
- );
583
-
584
- const mutationArgs = { where: { id: '1' }, data: { name: 'Test' } };
585
- await updater(mutationArgs);
586
-
587
- // Should extract mutation args from first argument
588
- expect(capturedMutationArgs).toEqual(mutationArgs);
589
- });
590
- });
591
-
592
- describe('real-world scenarios', () => {
593
- it('handles user list update optimistically', async () => {
594
- const schema = createSchema({
595
- User: {
596
- name: 'User',
597
- fields: {
598
- id: createField('id', 'String'),
599
- name: createField('name', 'String'),
600
- email: createField('email', 'String'),
601
- },
602
- uniqueFields: {},
603
- idFields: ['id'],
604
- },
605
- });
606
-
607
- const updateDataMock = vi.fn();
608
- const queries: QueryInfo[] = [
609
- {
610
- model: 'User',
611
- operation: 'findMany',
612
- args: {},
613
- data: [
614
- { id: '1', name: 'John', email: 'john@example.com' },
615
- { id: '2', name: 'Jane', email: 'jane@example.com' },
616
- ],
617
- optimisticUpdate: true,
618
- updateData: updateDataMock,
619
- },
620
- ];
621
-
622
- const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined);
623
-
624
- await updater({ where: { id: '1' }, data: { name: 'Johnny' } });
625
-
626
- expect(updateDataMock).toHaveBeenCalled();
627
- const updatedData = updateDataMock.mock.calls[0]?.[0];
628
- expect(Array.isArray(updatedData)).toBe(true);
629
- });
630
-
631
- it('handles custom provider for complex business logic', async () => {
632
- const schema = createSchema({
633
- Post: {
634
- name: 'Post',
635
- fields: {
636
- id: createField('id', 'String'),
637
- title: createField('title', 'String'),
638
- published: createField('published', 'Boolean'),
639
- },
640
- uniqueFields: {},
641
- idFields: ['id'],
642
- },
643
- });
644
-
645
- // Custom provider that only updates published posts
646
- const optimisticDataProvider = vi.fn(({ currentData, mutationArgs }) => {
647
- const posts = currentData as any[];
648
- const updatedPosts = posts.map((post: any) => {
649
- if (post.id === mutationArgs.where.id && post.published) {
650
- return { ...post, ...mutationArgs.data, $optimistic: true };
651
- }
652
- return post;
653
- });
654
- return { kind: 'Update' as const, data: updatedPosts };
655
- });
656
-
657
- const updateDataMock = vi.fn();
658
- const queries: QueryInfo[] = [
659
- {
660
- model: 'Post',
661
- operation: 'findMany',
662
- args: { where: { published: true } },
663
- data: [
664
- { id: '1', title: 'Post 1', published: true },
665
- { id: '2', title: 'Post 2', published: true },
666
- ],
667
- optimisticUpdate: true,
668
- updateData: updateDataMock,
669
- },
670
- ];
671
-
672
- const updater = createOptimisticUpdater(
673
- 'Post',
674
- 'update',
675
- schema,
676
- { optimisticDataProvider },
677
- () => queries,
678
- undefined,
679
- );
680
-
681
- await updater({ where: { id: '1' }, data: { title: 'Updated Post 1' } });
682
-
683
- expect(optimisticDataProvider).toHaveBeenCalled();
684
- expect(updateDataMock).toHaveBeenCalled();
685
- const updatedData = updateDataMock.mock.calls[0]?.[0];
686
- expect(updatedData[0]).toHaveProperty('$optimistic', true);
687
- });
688
-
689
- it('handles mixed queries with different opt-in settings', async () => {
690
- const schema = createSchema({
691
- User: {
692
- name: 'User',
693
- fields: {
694
- id: createField('id', 'String'),
695
- name: createField('name', 'String'),
696
- },
697
- uniqueFields: {},
698
- idFields: ['id'],
699
- },
700
- });
701
-
702
- const updateData1 = vi.fn();
703
- const updateData2 = vi.fn();
704
- const updateData3 = vi.fn();
705
-
706
- const queries: QueryInfo[] = [
707
- {
708
- model: 'User',
709
- operation: 'findMany',
710
- args: {},
711
- data: [{ id: '1', name: 'John' }],
712
- optimisticUpdate: true, // opted in
713
- updateData: updateData1,
714
- },
715
- {
716
- model: 'User',
717
- operation: 'findUnique',
718
- args: { where: { id: '1' } },
719
- data: { id: '1', name: 'John' },
720
- optimisticUpdate: false, // opted out
721
- updateData: updateData2,
722
- },
723
- {
724
- model: 'User',
725
- operation: 'findUnique',
726
- args: { where: { id: '2' } },
727
- data: { id: '2', name: 'Jane' },
728
- optimisticUpdate: true, // opted in but different ID so won't be updated
729
- updateData: updateData3,
730
- },
731
- ];
732
-
733
- const updater = createOptimisticUpdater('User', 'update', schema, {}, () => queries, undefined);
734
-
735
- await updater({ where: { id: '1' }, data: { name: 'Johnny' } });
736
-
737
- // Only opted-in queries matching the mutation should be updated
738
- expect(updateData1).toHaveBeenCalled(); // opted in and matches
739
- expect(updateData2).not.toHaveBeenCalled(); // opted out
740
- expect(updateData3).not.toHaveBeenCalled(); // opted in but different ID
741
- });
742
- });
743
- });