@zenstackhq/tanstack-query 2.21.0 → 3.0.0-beta.16

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 (94) hide show
  1. package/.turbo/turbo-build.log +32 -0
  2. package/.turbo/turbo-lint.log +5 -0
  3. package/LICENSE +1 -1
  4. package/dist/react.cjs +1238 -0
  5. package/dist/react.cjs.map +1 -0
  6. package/dist/react.d.cts +696 -0
  7. package/dist/react.d.ts +696 -0
  8. package/dist/react.js +1195 -0
  9. package/dist/react.js.map +1 -0
  10. package/eslint.config.js +4 -0
  11. package/package.json +56 -109
  12. package/scripts/generate.ts +27 -0
  13. package/src/react.ts +531 -0
  14. package/src/utils/common.ts +457 -0
  15. package/src/utils/mutator.ts +441 -0
  16. package/src/utils/nested-read-visitor.ts +61 -0
  17. package/src/utils/nested-write-visitor.ts +359 -0
  18. package/src/utils/query-analysis.ts +116 -0
  19. package/src/utils/serialization.ts +39 -0
  20. package/src/utils/types.ts +19 -0
  21. package/test/react-query.test.tsx +1787 -0
  22. package/test/schemas/basic/input.ts +70 -0
  23. package/test/schemas/basic/models.ts +12 -0
  24. package/test/schemas/basic/schema-lite.ts +124 -0
  25. package/test/schemas/basic/schema.zmodel +25 -0
  26. package/tsconfig.json +7 -0
  27. package/tsconfig.test.json +8 -0
  28. package/tsup.config.ts +13 -0
  29. package/vitest.config.ts +11 -0
  30. package/README.md +0 -5
  31. package/generator.d.ts +0 -6
  32. package/generator.js +0 -578
  33. package/generator.js.map +0 -1
  34. package/index.d.ts +0 -4
  35. package/index.js +0 -22
  36. package/index.js.map +0 -1
  37. package/runtime/common-CXlL7vTW.d.mts +0 -121
  38. package/runtime/common-CXlL7vTW.d.ts +0 -121
  39. package/runtime/index.d.mts +0 -20
  40. package/runtime/index.d.ts +0 -20
  41. package/runtime/index.js +0 -44
  42. package/runtime/index.js.map +0 -1
  43. package/runtime/index.mjs +0 -21
  44. package/runtime/index.mjs.map +0 -1
  45. package/runtime/react.d.mts +0 -322
  46. package/runtime/react.d.ts +0 -322
  47. package/runtime/react.js +0 -408
  48. package/runtime/react.js.map +0 -1
  49. package/runtime/react.mjs +0 -380
  50. package/runtime/react.mjs.map +0 -1
  51. package/runtime/svelte.d.mts +0 -322
  52. package/runtime/svelte.d.ts +0 -322
  53. package/runtime/svelte.js +0 -407
  54. package/runtime/svelte.js.map +0 -1
  55. package/runtime/svelte.mjs +0 -379
  56. package/runtime/svelte.mjs.map +0 -1
  57. package/runtime/vue.d.mts +0 -330
  58. package/runtime/vue.d.ts +0 -330
  59. package/runtime/vue.js +0 -418
  60. package/runtime/vue.js.map +0 -1
  61. package/runtime/vue.mjs +0 -390
  62. package/runtime/vue.mjs.map +0 -1
  63. package/runtime-v5/angular.d.mts +0 -59
  64. package/runtime-v5/angular.d.ts +0 -59
  65. package/runtime-v5/angular.js +0 -425
  66. package/runtime-v5/angular.js.map +0 -1
  67. package/runtime-v5/angular.mjs +0 -397
  68. package/runtime-v5/angular.mjs.map +0 -1
  69. package/runtime-v5/common-CXlL7vTW.d.mts +0 -121
  70. package/runtime-v5/common-CXlL7vTW.d.ts +0 -121
  71. package/runtime-v5/index.d.mts +0 -20
  72. package/runtime-v5/index.d.ts +0 -20
  73. package/runtime-v5/index.js +0 -44
  74. package/runtime-v5/index.js.map +0 -1
  75. package/runtime-v5/index.mjs +0 -21
  76. package/runtime-v5/index.mjs.map +0 -1
  77. package/runtime-v5/react.d.mts +0 -474
  78. package/runtime-v5/react.d.ts +0 -474
  79. package/runtime-v5/react.js +0 -440
  80. package/runtime-v5/react.js.map +0 -1
  81. package/runtime-v5/react.mjs +0 -412
  82. package/runtime-v5/react.mjs.map +0 -1
  83. package/runtime-v5/svelte.d.mts +0 -386
  84. package/runtime-v5/svelte.d.ts +0 -386
  85. package/runtime-v5/svelte.js +0 -436
  86. package/runtime-v5/svelte.js.map +0 -1
  87. package/runtime-v5/svelte.mjs +0 -408
  88. package/runtime-v5/svelte.mjs.map +0 -1
  89. package/runtime-v5/vue.d.mts +0 -330
  90. package/runtime-v5/vue.d.ts +0 -330
  91. package/runtime-v5/vue.js +0 -420
  92. package/runtime-v5/vue.js.map +0 -1
  93. package/runtime-v5/vue.mjs +0 -392
  94. package/runtime-v5/vue.mjs.map +0 -1
@@ -0,0 +1,1787 @@
1
+ /**
2
+ * @vitest-environment happy-dom
3
+ */
4
+
5
+ import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
6
+ import { act, cleanup, renderHook, waitFor } from '@testing-library/react';
7
+ import nock from 'nock';
8
+ import React from 'react';
9
+ import { afterEach, describe, expect, it } from 'vitest';
10
+ import { QuerySettingsProvider, useClientQueries } from '../src/react';
11
+ import { getQueryKey } from '../src/utils/common';
12
+ import { schema } from './schemas/basic/schema-lite';
13
+
14
+ const BASE_URL = 'http://localhost';
15
+
16
+ describe('React Query Test', () => {
17
+ function createWrapper() {
18
+ const queryClient = new QueryClient({
19
+ defaultOptions: {
20
+ queries: {
21
+ retry: false,
22
+ },
23
+ },
24
+ });
25
+ const Provider = QuerySettingsProvider;
26
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
27
+ <QueryClientProvider client={queryClient}>
28
+ <Provider value={{ endpoint: `${BASE_URL}/api/model`, logging: true }}>{children}</Provider>
29
+ </QueryClientProvider>
30
+ );
31
+ return { queryClient, wrapper };
32
+ }
33
+
34
+ function makeUrl(model: string, operation: string, args?: unknown) {
35
+ let r = `${BASE_URL}/api/model/${model}/${operation}`;
36
+ if (args) {
37
+ r += `?q=${encodeURIComponent(JSON.stringify(args))}`;
38
+ }
39
+ return r;
40
+ }
41
+
42
+ afterEach(() => {
43
+ nock.cleanAll();
44
+ cleanup();
45
+ });
46
+
47
+ it('works with simple query', async () => {
48
+ const { queryClient, wrapper } = createWrapper();
49
+
50
+ const queryArgs = { where: { id: '1' } };
51
+ const data = { id: '1', name: 'foo' };
52
+
53
+ nock(makeUrl('User', 'findUnique', queryArgs))
54
+ .get(/.*/)
55
+ .reply(200, {
56
+ data,
57
+ });
58
+
59
+ const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), {
60
+ wrapper,
61
+ });
62
+
63
+ await waitFor(() => {
64
+ expect(result.current.isSuccess).toBe(true);
65
+ expect(result.current.data).toMatchObject(data);
66
+ const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs));
67
+ expect(cacheData).toMatchObject(data);
68
+ });
69
+
70
+ nock(makeUrl('User', 'findFirst', queryArgs))
71
+ .get(/.*/)
72
+ .reply(404, () => {
73
+ return { error: 'Not Found' };
74
+ });
75
+ const { result: errorResult } = renderHook(() => useClientQueries(schema).user.useFindFirst(queryArgs), {
76
+ wrapper,
77
+ });
78
+ await waitFor(() => {
79
+ expect(errorResult.current.isError).toBe(true);
80
+ });
81
+ });
82
+
83
+ it('works with suspense query', async () => {
84
+ const { queryClient, wrapper } = createWrapper();
85
+
86
+ const queryArgs = { where: { id: '1' } };
87
+ const data = { id: '1', name: 'foo' };
88
+
89
+ nock(makeUrl('User', 'findUnique', queryArgs))
90
+ .get(/.*/)
91
+ .reply(200, {
92
+ data,
93
+ });
94
+
95
+ const { result } = renderHook(() => useClientQueries(schema).user.useSuspenseFindUnique(queryArgs), {
96
+ wrapper,
97
+ });
98
+
99
+ await waitFor(() => {
100
+ expect(result.current.isSuccess).toBe(true);
101
+ expect(result.current.data).toMatchObject(data);
102
+ const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs));
103
+ expect(cacheData).toMatchObject(data);
104
+ });
105
+ });
106
+
107
+ it('works with infinite query', async () => {
108
+ const { queryClient, wrapper } = createWrapper();
109
+
110
+ const queryArgs = { where: { id: '1' } };
111
+ const data = [{ id: '1', name: 'foo' }];
112
+
113
+ nock(makeUrl('User', 'findMany', queryArgs))
114
+ .get(/.*/)
115
+ .reply(200, () => ({
116
+ data,
117
+ }));
118
+
119
+ const { result } = renderHook(
120
+ () =>
121
+ useClientQueries(schema).user.useInfiniteFindMany(queryArgs, {
122
+ getNextPageParam: () => null,
123
+ }),
124
+ {
125
+ wrapper,
126
+ },
127
+ );
128
+ await waitFor(() => {
129
+ expect(result.current.isSuccess).toBe(true);
130
+ const resultData = result.current.data!;
131
+ expect(resultData.pages).toHaveLength(1);
132
+ expect(resultData.pages[0]).toMatchObject(data);
133
+ expect(resultData?.pageParams).toHaveLength(1);
134
+ expect(resultData?.pageParams[0]).toMatchObject(queryArgs);
135
+ expect(result.current.hasNextPage).toBe(false);
136
+ const cacheData: any = queryClient.getQueryData(
137
+ getQueryKey('User', 'findMany', queryArgs, { infinite: true, optimisticUpdate: false }),
138
+ );
139
+ expect(cacheData.pages[0]).toMatchObject(data);
140
+ });
141
+ });
142
+
143
+ it('works with suspense infinite query', async () => {
144
+ const { queryClient, wrapper } = createWrapper();
145
+
146
+ const queryArgs = { where: { id: '1' } };
147
+ const data = [{ id: '1', name: 'foo' }];
148
+
149
+ nock(makeUrl('User', 'findMany', queryArgs))
150
+ .get(/.*/)
151
+ .reply(200, () => ({
152
+ data,
153
+ }));
154
+
155
+ const { result } = renderHook(
156
+ () =>
157
+ useClientQueries(schema).user.useSuspenseInfiniteFindMany(queryArgs, {
158
+ getNextPageParam: () => null,
159
+ }),
160
+ {
161
+ wrapper,
162
+ },
163
+ );
164
+ await waitFor(() => {
165
+ expect(result.current.isSuccess).toBe(true);
166
+ const resultData = result.current.data!;
167
+ expect(resultData.pages).toHaveLength(1);
168
+ expect(resultData.pages[0]).toMatchObject(data);
169
+ expect(resultData?.pageParams).toHaveLength(1);
170
+ expect(resultData?.pageParams[0]).toMatchObject(queryArgs);
171
+ expect(result.current.hasNextPage).toBe(false);
172
+ const cacheData: any = queryClient.getQueryData(
173
+ getQueryKey('User', 'findMany', queryArgs, { infinite: true, optimisticUpdate: false }),
174
+ );
175
+ expect(cacheData.pages[0]).toMatchObject(data);
176
+ });
177
+ });
178
+
179
+ it('works with independent mutation and query', async () => {
180
+ const { wrapper } = createWrapper();
181
+
182
+ const queryArgs = { where: { id: '1' } };
183
+ const data = { id: '1', name: 'foo' };
184
+
185
+ let queryCount = 0;
186
+ nock(makeUrl('User', 'findUnique', queryArgs))
187
+ .get(/.*/)
188
+ .reply(200, () => {
189
+ queryCount++;
190
+ return { data };
191
+ })
192
+ .persist();
193
+
194
+ const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), {
195
+ wrapper,
196
+ });
197
+ await waitFor(() => {
198
+ expect(result.current.data).toMatchObject({ name: 'foo' });
199
+ });
200
+
201
+ nock(makeUrl('Post', 'create'))
202
+ .post(/.*/)
203
+ .reply(200, () => ({
204
+ data: { id: '1', title: 'post1' },
205
+ }));
206
+
207
+ const { result: mutationResult } = renderHook(() => useClientQueries(schema).post.useCreate(), {
208
+ wrapper,
209
+ });
210
+
211
+ act(() => mutationResult.current.mutate({ data: { title: 'post1' } }));
212
+
213
+ await waitFor(() => {
214
+ // no refetch caused by invalidation
215
+ expect(queryCount).toBe(1);
216
+ });
217
+ });
218
+
219
+ it('works with create and invalidation', async () => {
220
+ const { queryClient, wrapper } = createWrapper();
221
+
222
+ const data: any[] = [];
223
+
224
+ nock(makeUrl('User', 'findMany'))
225
+ .get(/.*/)
226
+ .reply(200, () => ({ data }))
227
+ .persist();
228
+
229
+ const { result } = renderHook(() => useClientQueries(schema).user.useFindMany(), {
230
+ wrapper,
231
+ });
232
+ await waitFor(() => {
233
+ expect(result.current.data).toHaveLength(0);
234
+ });
235
+
236
+ nock(makeUrl('User', 'create'))
237
+ .post(/.*/)
238
+ .reply(200, () => {
239
+ data.push({ id: '1', email: 'foo' });
240
+ return { data: data[0] };
241
+ });
242
+
243
+ const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useCreate(), {
244
+ wrapper,
245
+ });
246
+
247
+ act(() => mutationResult.current.mutate({ data: { email: 'foo' } }));
248
+
249
+ await waitFor(() => {
250
+ const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined));
251
+ expect(cacheData).toHaveLength(1);
252
+ });
253
+ });
254
+
255
+ it('works with create and no invalidation', async () => {
256
+ const { queryClient, wrapper } = createWrapper();
257
+
258
+ const data: any[] = [];
259
+
260
+ nock(makeUrl('User', 'findMany'))
261
+ .get(/.*/)
262
+ .reply(200, () => ({ data }))
263
+ .persist();
264
+
265
+ const { result } = renderHook(() => useClientQueries(schema).user.useFindMany(), {
266
+ wrapper,
267
+ });
268
+ await waitFor(() => {
269
+ expect(result.current.data).toHaveLength(0);
270
+ });
271
+
272
+ nock(makeUrl('User', 'create'))
273
+ .post(/.*/)
274
+ .reply(200, () => {
275
+ data.push({ id: '1', email: 'foo' });
276
+ return { data: data[0] };
277
+ });
278
+
279
+ const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useCreate(), {
280
+ wrapper,
281
+ });
282
+
283
+ act(() => mutationResult.current.mutate({ data: { email: 'foo' } }));
284
+
285
+ await waitFor(() => {
286
+ const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined));
287
+ expect(cacheData).toHaveLength(0);
288
+ });
289
+ });
290
+
291
+ it('works with optimistic create single', async () => {
292
+ const { queryClient, wrapper } = createWrapper();
293
+
294
+ const data: any[] = [];
295
+
296
+ nock(makeUrl('User', 'findMany'))
297
+ .get(/.*/)
298
+ .reply(200, () => ({ data }))
299
+ .persist();
300
+
301
+ const { result } = renderHook(
302
+ () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }),
303
+ {
304
+ wrapper,
305
+ },
306
+ );
307
+ await waitFor(() => {
308
+ expect(result.current.data).toHaveLength(0);
309
+ });
310
+
311
+ nock(makeUrl('User', 'create'))
312
+ .post(/.*/)
313
+ .reply(200, () => ({
314
+ data: null,
315
+ }));
316
+
317
+ const { result: mutationResult } = renderHook(
318
+ () =>
319
+ useClientQueries(schema).user.useCreate({
320
+ optimisticUpdate: true,
321
+ invalidateQueries: false,
322
+ }),
323
+ {
324
+ wrapper,
325
+ },
326
+ );
327
+
328
+ act(() => mutationResult.current.mutate({ data: { email: 'foo' } }));
329
+
330
+ await waitFor(() => {
331
+ const cacheData: any = queryClient.getQueryData(
332
+ getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }),
333
+ );
334
+ expect(cacheData).toHaveLength(1);
335
+ expect(cacheData[0].$optimistic).toBe(true);
336
+ expect(cacheData[0].id).toBeTruthy();
337
+ expect(cacheData[0].email).toBe('foo');
338
+ });
339
+ });
340
+
341
+ it('works with optimistic create updating nested query', async () => {
342
+ const { queryClient, wrapper } = createWrapper();
343
+
344
+ const data: any[] = [{ id: '1', name: 'user1', posts: [] }];
345
+
346
+ nock(makeUrl('User', 'findMany'))
347
+ .get(/.*/)
348
+ .reply(200, () => ({ data }))
349
+ .persist();
350
+
351
+ const { result } = renderHook(
352
+ () =>
353
+ useClientQueries(schema).user.useFindMany(
354
+ {
355
+ include: { posts: true },
356
+ },
357
+ { optimisticUpdate: true },
358
+ ),
359
+ {
360
+ wrapper,
361
+ },
362
+ );
363
+ await waitFor(() => {
364
+ expect(result.current.data).toHaveLength(1);
365
+ });
366
+
367
+ nock(makeUrl('Post', 'create'))
368
+ .post(/.*/)
369
+ .reply(200, () => ({
370
+ data: null,
371
+ }));
372
+
373
+ const { result: mutationResult } = renderHook(
374
+ () =>
375
+ useClientQueries(schema).post.useCreate({
376
+ optimisticUpdate: true,
377
+ invalidateQueries: false,
378
+ }),
379
+ {
380
+ wrapper,
381
+ },
382
+ );
383
+
384
+ act(() => mutationResult.current.mutate({ data: { title: 'post1', owner: { connect: { id: '1' } } } }));
385
+
386
+ await waitFor(() => {
387
+ const cacheData: any = queryClient.getQueryData(
388
+ getQueryKey(
389
+ 'User',
390
+ 'findMany',
391
+ { include: { posts: true } },
392
+ { infinite: false, optimisticUpdate: true },
393
+ ),
394
+ );
395
+ const posts = cacheData[0].posts;
396
+ expect(posts).toHaveLength(1);
397
+ expect(posts[0]).toMatchObject({ $optimistic: true, id: expect.any(String), title: 'post1', ownerId: '1' });
398
+ });
399
+ });
400
+
401
+ it('works with optimistic create updating deeply nested query', async () => {
402
+ const { queryClient, wrapper } = createWrapper();
403
+
404
+ // populate the cache with a user
405
+
406
+ const userData: any[] = [{ id: '1', email: 'user1', posts: [] }];
407
+
408
+ nock(BASE_URL)
409
+ .get('/api/model/user/findMany')
410
+ .query(true)
411
+ .reply(200, () => ({ data: userData }))
412
+ .persist();
413
+
414
+ const { result: userResult } = renderHook(
415
+ () =>
416
+ useClientQueries(schema).user.useFindMany(
417
+ {
418
+ include: {
419
+ posts: {
420
+ include: {
421
+ category: true,
422
+ },
423
+ },
424
+ },
425
+ },
426
+ { optimisticUpdate: true },
427
+ ),
428
+ {
429
+ wrapper,
430
+ },
431
+ );
432
+ await waitFor(() => {
433
+ expect(userResult.current.data).toHaveLength(1);
434
+ });
435
+
436
+ // populate the cache with a category
437
+ const categoryData: any[] = [{ id: '1', name: 'category1', posts: [] }];
438
+
439
+ nock(BASE_URL)
440
+ .get('/api/model/category/findMany')
441
+ .query(true)
442
+ .reply(200, () => ({ data: categoryData }))
443
+ .persist();
444
+
445
+ const { result: categoryResult } = renderHook(
446
+ () =>
447
+ useClientQueries(schema).category.useFindMany(
448
+ {
449
+ include: {
450
+ posts: true,
451
+ },
452
+ },
453
+ { optimisticUpdate: true },
454
+ ),
455
+ {
456
+ wrapper,
457
+ },
458
+ );
459
+ await waitFor(() => {
460
+ expect(categoryResult.current.data).toHaveLength(1);
461
+ });
462
+
463
+ // create a post and connect it to the category
464
+ nock(BASE_URL)
465
+ .post('/api/model/post/create')
466
+ .reply(200, () => ({
467
+ data: null,
468
+ }));
469
+
470
+ const { result: mutationResult } = renderHook(
471
+ () =>
472
+ useClientQueries(schema).post.useCreate({
473
+ optimisticUpdate: true,
474
+ invalidateQueries: false,
475
+ }),
476
+ {
477
+ wrapper,
478
+ },
479
+ );
480
+
481
+ act(() =>
482
+ mutationResult.current.mutate({
483
+ data: { title: 'post1', owner: { connect: { id: '1' } }, category: { connect: { id: '1' } } },
484
+ }),
485
+ );
486
+
487
+ // assert that the post was created and connected to the category
488
+ await waitFor(() => {
489
+ const cacheData: any = queryClient.getQueryData(
490
+ getQueryKey(
491
+ 'Category',
492
+ 'findMany',
493
+ {
494
+ include: {
495
+ posts: true,
496
+ },
497
+ },
498
+ { infinite: false, optimisticUpdate: true },
499
+ ),
500
+ );
501
+ const posts = cacheData[0].posts;
502
+ expect(posts).toHaveLength(1);
503
+ expect(posts[0]).toMatchObject({
504
+ $optimistic: true,
505
+ id: expect.any(String),
506
+ title: 'post1',
507
+ ownerId: '1',
508
+ });
509
+ });
510
+
511
+ // assert that the post was created and connected to the user, and included the category
512
+ await waitFor(() => {
513
+ const cacheData: any = queryClient.getQueryData(
514
+ getQueryKey(
515
+ 'User',
516
+ 'findMany',
517
+ {
518
+ include: {
519
+ posts: {
520
+ include: {
521
+ category: true,
522
+ },
523
+ },
524
+ },
525
+ },
526
+ { infinite: false, optimisticUpdate: true },
527
+ ),
528
+ );
529
+ const posts = cacheData[0].posts;
530
+ expect(posts).toHaveLength(1);
531
+ expect(posts[0]).toMatchObject({
532
+ $optimistic: true,
533
+ id: expect.any(String),
534
+ title: 'post1',
535
+ ownerId: '1',
536
+ categoryId: '1',
537
+ // TODO: should this include the category object and not just the foreign key?
538
+ // category: { $optimistic: true, id: '1', name: 'category1' },
539
+ });
540
+ });
541
+ });
542
+
543
+ it('works with optimistic update with optional one-to-many relationship', async () => {
544
+ const { queryClient, wrapper } = createWrapper();
545
+
546
+ // populate the cache with a post, with an optional category relationship
547
+ const postData: any = {
548
+ id: '1',
549
+ title: 'post1',
550
+ ownerId: '1',
551
+ categoryId: null,
552
+ category: null,
553
+ };
554
+
555
+ const data: any[] = [postData];
556
+
557
+ nock(makeUrl('Post', 'findMany'))
558
+ .get(/.*/)
559
+ .query(true)
560
+ .reply(200, () => ({
561
+ data,
562
+ }))
563
+ .persist();
564
+
565
+ const { result: postResult } = renderHook(
566
+ () =>
567
+ useClientQueries(schema).post.useFindMany(
568
+ {
569
+ include: {
570
+ category: true,
571
+ },
572
+ },
573
+ { optimisticUpdate: true },
574
+ ),
575
+ {
576
+ wrapper,
577
+ },
578
+ );
579
+ await waitFor(() => {
580
+ expect(postResult.current.data).toHaveLength(1);
581
+ });
582
+
583
+ // mock a put request to update the post title
584
+ nock(makeUrl('Post', 'update'))
585
+ .put(/.*/)
586
+ .reply(200, () => {
587
+ postData.title = 'postA';
588
+ return { data: postData };
589
+ });
590
+
591
+ const { result: mutationResult } = renderHook(
592
+ () =>
593
+ useClientQueries(schema).post.useUpdate({
594
+ optimisticUpdate: true,
595
+ invalidateQueries: false,
596
+ }),
597
+ {
598
+ wrapper,
599
+ },
600
+ );
601
+
602
+ act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'postA' } }));
603
+
604
+ // assert that the post was updated despite the optional (null) category relationship
605
+ await waitFor(() => {
606
+ const cacheData: any = queryClient.getQueryData(
607
+ getQueryKey(
608
+ 'Post',
609
+ 'findMany',
610
+ {
611
+ include: {
612
+ category: true,
613
+ },
614
+ },
615
+ { infinite: false, optimisticUpdate: true },
616
+ ),
617
+ );
618
+ const posts = cacheData;
619
+ expect(posts).toHaveLength(1);
620
+ expect(posts[0]).toMatchObject({
621
+ $optimistic: true,
622
+ id: expect.any(String),
623
+ title: 'postA',
624
+ ownerId: '1',
625
+ categoryId: null,
626
+ category: null,
627
+ });
628
+ });
629
+ });
630
+
631
+ it('works with optimistic update with nested optional one-to-many relationship', async () => {
632
+ const { queryClient, wrapper } = createWrapper();
633
+
634
+ // populate the cache with a user and a post, with an optional category
635
+ const postData: any = {
636
+ id: '1',
637
+ title: 'post1',
638
+ ownerId: '1',
639
+ categoryId: null,
640
+ category: null,
641
+ };
642
+
643
+ const userData: any[] = [{ id: '1', name: 'user1', posts: [postData] }];
644
+
645
+ nock(BASE_URL)
646
+ .get('/api/model/user/findMany')
647
+ .query(true)
648
+ .reply(200, () => {
649
+ return { data: userData };
650
+ })
651
+ .persist();
652
+
653
+ const { result: userResult } = renderHook(
654
+ () =>
655
+ useClientQueries(schema).user.useFindMany(
656
+ {
657
+ include: {
658
+ posts: {
659
+ include: {
660
+ category: true,
661
+ },
662
+ },
663
+ },
664
+ },
665
+ { optimisticUpdate: true },
666
+ ),
667
+ {
668
+ wrapper,
669
+ },
670
+ );
671
+ await waitFor(() => {
672
+ expect(userResult.current.data).toHaveLength(1);
673
+ });
674
+
675
+ // mock a put request to update the post title
676
+ nock(BASE_URL)
677
+ .put('/api/model/post/update')
678
+ .reply(200, () => {
679
+ postData.title = 'postA';
680
+ return { data: postData };
681
+ });
682
+
683
+ const { result: mutationResult } = renderHook(
684
+ () =>
685
+ useClientQueries(schema).post.useUpdate({
686
+ optimisticUpdate: true,
687
+ invalidateQueries: false,
688
+ }),
689
+ {
690
+ wrapper,
691
+ },
692
+ );
693
+
694
+ act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'postA' } }));
695
+
696
+ // assert that the post was updated
697
+ await waitFor(() => {
698
+ const cacheData: any = queryClient.getQueryData(
699
+ getQueryKey(
700
+ 'User',
701
+ 'findMany',
702
+ {
703
+ include: {
704
+ posts: {
705
+ include: {
706
+ category: true,
707
+ },
708
+ },
709
+ },
710
+ },
711
+ { infinite: false, optimisticUpdate: true },
712
+ ),
713
+ );
714
+ const posts = cacheData[0].posts;
715
+ expect(posts).toHaveLength(1);
716
+ expect(posts[0]).toMatchObject({
717
+ $optimistic: true,
718
+ id: expect.any(String),
719
+ title: 'postA',
720
+ ownerId: '1',
721
+ categoryId: null,
722
+ category: null,
723
+ });
724
+ });
725
+ });
726
+
727
+ it('works with optimistic nested create updating query', async () => {
728
+ const { queryClient, wrapper } = createWrapper();
729
+
730
+ const data: any[] = [];
731
+
732
+ nock(makeUrl('Post', 'findMany'))
733
+ .get(/.*/)
734
+ .reply(200, () => ({
735
+ data,
736
+ }))
737
+ .persist();
738
+
739
+ const { result } = renderHook(
740
+ () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }),
741
+ {
742
+ wrapper,
743
+ },
744
+ );
745
+ await waitFor(() => {
746
+ expect(result.current.data).toHaveLength(0);
747
+ });
748
+
749
+ nock(makeUrl('User', 'create'))
750
+ .post(/.*/)
751
+ .reply(200, () => ({
752
+ data: null,
753
+ }));
754
+
755
+ const { result: mutationResult } = renderHook(
756
+ () =>
757
+ useClientQueries(schema).user.useCreate({
758
+ optimisticUpdate: true,
759
+ invalidateQueries: false,
760
+ }),
761
+ {
762
+ wrapper,
763
+ },
764
+ );
765
+
766
+ act(() => mutationResult.current.mutate({ data: { email: 'user1', posts: { create: { title: 'post1' } } } }));
767
+
768
+ await waitFor(() => {
769
+ const cacheData: any = queryClient.getQueryData(
770
+ getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }),
771
+ );
772
+ expect(cacheData).toHaveLength(1);
773
+ expect(cacheData[0].$optimistic).toBe(true);
774
+ expect(cacheData[0].id).toBeTruthy();
775
+ expect(cacheData[0].title).toBe('post1');
776
+ });
777
+ });
778
+
779
+ it('works with optimistic create many', async () => {
780
+ const { queryClient, wrapper } = createWrapper();
781
+
782
+ const data: any[] = [];
783
+
784
+ nock(makeUrl('User', 'findMany'))
785
+ .get(/.*/)
786
+ .reply(200, () => ({
787
+ data,
788
+ }))
789
+ .persist();
790
+
791
+ const { result } = renderHook(
792
+ () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }),
793
+ {
794
+ wrapper,
795
+ },
796
+ );
797
+ await waitFor(() => {
798
+ expect(result.current.data).toHaveLength(0);
799
+ });
800
+
801
+ nock(makeUrl('User', 'createMany'))
802
+ .post(/.*/)
803
+ .reply(200, () => ({
804
+ data: null,
805
+ }));
806
+
807
+ const { result: mutationResult } = renderHook(
808
+ () =>
809
+ useClientQueries(schema).user.useCreateMany({
810
+ optimisticUpdate: true,
811
+ invalidateQueries: false,
812
+ }),
813
+ {
814
+ wrapper,
815
+ },
816
+ );
817
+
818
+ act(() => mutationResult.current.mutate({ data: [{ email: 'foo' }, { email: 'bar' }] }));
819
+
820
+ await waitFor(() => {
821
+ const cacheData: any = queryClient.getQueryData(
822
+ getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }),
823
+ );
824
+ expect(cacheData).toHaveLength(2);
825
+ });
826
+ });
827
+
828
+ it('works with update and invalidation', async () => {
829
+ const { queryClient, wrapper } = createWrapper();
830
+
831
+ const queryArgs = { where: { id: '1' } };
832
+ const data = { id: '1', name: 'foo' };
833
+
834
+ nock(makeUrl('User', 'findUnique', queryArgs))
835
+ .get(/.*/)
836
+ .reply(200, () => ({
837
+ data,
838
+ }))
839
+ .persist();
840
+
841
+ const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), {
842
+ wrapper,
843
+ });
844
+ await waitFor(() => {
845
+ expect(result.current.data).toMatchObject({ name: 'foo' });
846
+ });
847
+
848
+ nock(makeUrl('User', 'update'))
849
+ .put(/.*/)
850
+ .reply(200, () => {
851
+ data.name = 'bar';
852
+ return data;
853
+ });
854
+
855
+ const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useUpdate(), {
856
+ wrapper,
857
+ });
858
+
859
+ act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } }));
860
+
861
+ await waitFor(() => {
862
+ const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs));
863
+ expect(cacheData).toMatchObject({ name: 'bar' });
864
+ });
865
+ });
866
+
867
+ it('works with update and no invalidation', async () => {
868
+ const { queryClient, wrapper } = createWrapper();
869
+
870
+ const queryArgs = { where: { id: '1' } };
871
+ const data = { id: '1', name: 'foo' };
872
+
873
+ nock(makeUrl('User', 'findUnique', queryArgs))
874
+ .get(/.*/)
875
+ .reply(200, () => ({ data }))
876
+ .persist();
877
+
878
+ const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), {
879
+ wrapper,
880
+ });
881
+ await waitFor(() => {
882
+ expect(result.current.data).toMatchObject({ name: 'foo' });
883
+ });
884
+
885
+ nock(makeUrl('User', 'update'))
886
+ .put(/.*/)
887
+ .reply(200, () => {
888
+ data.name = 'bar';
889
+ return data;
890
+ });
891
+
892
+ const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useUpdate(), {
893
+ wrapper,
894
+ });
895
+
896
+ act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } }));
897
+
898
+ await waitFor(() => {
899
+ const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs));
900
+ expect(cacheData).toMatchObject({ name: 'foo' });
901
+ });
902
+ });
903
+
904
+ it('works with optimistic update simple', async () => {
905
+ const { queryClient, wrapper } = createWrapper();
906
+
907
+ const queryArgs = { where: { id: '1' } };
908
+ const data = { id: '1', name: 'foo' };
909
+
910
+ nock(makeUrl('User', 'findUnique', queryArgs))
911
+ .get(/.*/)
912
+ .reply(200, () => ({
913
+ data,
914
+ }))
915
+ .persist();
916
+
917
+ const { result } = renderHook(
918
+ () => useClientQueries(schema).user.useFindUnique(queryArgs, { optimisticUpdate: true }),
919
+ {
920
+ wrapper,
921
+ },
922
+ );
923
+ await waitFor(() => {
924
+ expect(result.current.data).toMatchObject({ name: 'foo' });
925
+ });
926
+
927
+ nock(makeUrl('User', 'update'))
928
+ .put(/.*/)
929
+ .reply(200, () => data);
930
+
931
+ const { result: mutationResult } = renderHook(
932
+ () =>
933
+ useClientQueries(schema).user.useUpdate({
934
+ optimisticUpdate: true,
935
+ invalidateQueries: false,
936
+ }),
937
+ {
938
+ wrapper,
939
+ },
940
+ );
941
+
942
+ act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } }));
943
+
944
+ await waitFor(() => {
945
+ const cacheData = queryClient.getQueryData(
946
+ getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }),
947
+ );
948
+ expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true });
949
+ });
950
+ });
951
+
952
+ it('works with optimistic update updating nested query', async () => {
953
+ const { queryClient, wrapper } = createWrapper();
954
+
955
+ const queryArgs = { where: { id: '1' }, include: { posts: true } };
956
+ const data = { id: '1', name: 'foo', posts: [{ id: 'p1', title: 'post1' }] };
957
+
958
+ nock(makeUrl('User', 'findUnique', queryArgs))
959
+ .get(/.*/)
960
+ .reply(200, () => ({ data }))
961
+ .persist();
962
+
963
+ const { result } = renderHook(
964
+ () => useClientQueries(schema).user.useFindUnique(queryArgs, { optimisticUpdate: true }),
965
+ {
966
+ wrapper,
967
+ },
968
+ );
969
+ await waitFor(() => {
970
+ expect(result.current.data).toMatchObject({ name: 'foo' });
971
+ });
972
+
973
+ nock(makeUrl('Post', 'update'))
974
+ .put(/.*/)
975
+ .reply(200, () => data);
976
+
977
+ const { result: mutationResult } = renderHook(
978
+ () =>
979
+ useClientQueries(schema).post.useUpdate({
980
+ optimisticUpdate: true,
981
+ invalidateQueries: false,
982
+ }),
983
+ {
984
+ wrapper,
985
+ },
986
+ );
987
+
988
+ act(() =>
989
+ mutationResult.current.mutate({
990
+ where: { id: 'p1' },
991
+ data: { title: 'post2', owner: { connect: { id: '2' } } },
992
+ }),
993
+ );
994
+
995
+ await waitFor(() => {
996
+ const cacheData: any = queryClient.getQueryData(
997
+ getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }),
998
+ );
999
+ expect(cacheData.posts[0]).toMatchObject({ title: 'post2', $optimistic: true, ownerId: '2' });
1000
+ });
1001
+ });
1002
+
1003
+ it('works with optimistic nested update updating query', async () => {
1004
+ const { queryClient, wrapper } = createWrapper();
1005
+
1006
+ const queryArgs = { where: { id: 'p1' } };
1007
+ const data = { id: 'p1', title: 'post1' };
1008
+
1009
+ nock(makeUrl('Post', 'findUnique', queryArgs))
1010
+ .get(/.*/)
1011
+ .reply(200, () => ({ data }))
1012
+ .persist();
1013
+
1014
+ const { result } = renderHook(
1015
+ () => useClientQueries(schema).post.useFindUnique(queryArgs, { optimisticUpdate: true }),
1016
+ {
1017
+ wrapper,
1018
+ },
1019
+ );
1020
+ await waitFor(() => {
1021
+ expect(result.current.data).toMatchObject({ title: 'post1' });
1022
+ });
1023
+
1024
+ nock(makeUrl('User', 'update'))
1025
+ .put(/.*/)
1026
+ .reply(200, () => data);
1027
+
1028
+ const { result: mutationResult } = renderHook(
1029
+ () =>
1030
+ useClientQueries(schema).user.useUpdate({
1031
+ optimisticUpdate: true,
1032
+ invalidateQueries: false,
1033
+ }),
1034
+ {
1035
+ wrapper,
1036
+ },
1037
+ );
1038
+
1039
+ act(() =>
1040
+ mutationResult.current.mutate({
1041
+ where: { id: '1' },
1042
+ data: { posts: { update: { where: { id: 'p1' }, data: { title: 'post2' } } } },
1043
+ }),
1044
+ );
1045
+
1046
+ await waitFor(() => {
1047
+ const cacheData: any = queryClient.getQueryData(
1048
+ getQueryKey('Post', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }),
1049
+ );
1050
+ expect(cacheData).toMatchObject({ title: 'post2', $optimistic: true });
1051
+ });
1052
+ });
1053
+
1054
+ it('works with optimistic upsert - create simple', async () => {
1055
+ const { queryClient, wrapper } = createWrapper();
1056
+
1057
+ const data: any[] = [];
1058
+
1059
+ nock(makeUrl('User', 'findMany'))
1060
+ .get(/.*/)
1061
+ .reply(200, () => ({ data }))
1062
+ .persist();
1063
+
1064
+ const { result } = renderHook(
1065
+ () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }),
1066
+ {
1067
+ wrapper,
1068
+ },
1069
+ );
1070
+ await waitFor(() => {
1071
+ expect(result.current.data).toHaveLength(0);
1072
+ });
1073
+
1074
+ nock(makeUrl('User', 'upsert'))
1075
+ .post(/.*/)
1076
+ .reply(200, () => ({ data: null }));
1077
+
1078
+ const { result: mutationResult } = renderHook(
1079
+ () =>
1080
+ useClientQueries(schema).user.useUpsert({
1081
+ optimisticUpdate: true,
1082
+ invalidateQueries: false,
1083
+ }),
1084
+ {
1085
+ wrapper,
1086
+ },
1087
+ );
1088
+
1089
+ act(() =>
1090
+ mutationResult.current.mutate({
1091
+ where: { id: '1' },
1092
+ create: { id: '1', email: 'foo' },
1093
+ update: { email: 'bar' },
1094
+ }),
1095
+ );
1096
+
1097
+ await waitFor(() => {
1098
+ const cacheData: any = queryClient.getQueryData(
1099
+ getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }),
1100
+ );
1101
+ expect(cacheData).toHaveLength(1);
1102
+ expect(cacheData[0]).toMatchObject({ id: '1', email: 'foo', $optimistic: true });
1103
+ });
1104
+ });
1105
+
1106
+ it('works with optimistic upsert - create updating nested query', async () => {
1107
+ const { queryClient, wrapper } = createWrapper();
1108
+
1109
+ const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] };
1110
+
1111
+ nock(makeUrl('User', 'findUnique'))
1112
+ .get(/.*/)
1113
+ .reply(200, () => ({ data }))
1114
+ .persist();
1115
+
1116
+ const { result } = renderHook(
1117
+ () => useClientQueries(schema).user.useFindUnique({ where: { id: '1' } }, { optimisticUpdate: true }),
1118
+ {
1119
+ wrapper,
1120
+ },
1121
+ );
1122
+ await waitFor(() => {
1123
+ expect(result.current.data).toMatchObject({ id: '1' });
1124
+ });
1125
+
1126
+ nock(makeUrl('Post', 'upsert'))
1127
+ .post(/.*/)
1128
+ .reply(200, () => ({ data: null }));
1129
+
1130
+ const { result: mutationResult } = renderHook(
1131
+ () =>
1132
+ useClientQueries(schema).post.useUpsert({
1133
+ optimisticUpdate: true,
1134
+ invalidateQueries: false,
1135
+ }),
1136
+ {
1137
+ wrapper,
1138
+ },
1139
+ );
1140
+
1141
+ act(() =>
1142
+ mutationResult.current.mutate({
1143
+ where: { id: 'p2' },
1144
+ create: { id: 'p2', title: 'post2', owner: { connect: { id: '1' } } },
1145
+ update: { title: 'post3' },
1146
+ }),
1147
+ );
1148
+
1149
+ await waitFor(() => {
1150
+ const cacheData: any = queryClient.getQueryData(
1151
+ getQueryKey('User', 'findUnique', { where: { id: '1' } }, { infinite: false, optimisticUpdate: true }),
1152
+ );
1153
+ const posts = cacheData.posts;
1154
+ expect(posts).toHaveLength(2);
1155
+ expect(posts[0]).toMatchObject({ id: 'p2', title: 'post2', ownerId: '1', $optimistic: true });
1156
+ });
1157
+ });
1158
+
1159
+ it('works with optimistic upsert - nested create updating query', async () => {
1160
+ const { queryClient, wrapper } = createWrapper();
1161
+
1162
+ const data: any = [{ id: 'p1', title: 'post1' }];
1163
+
1164
+ nock(makeUrl('Post', 'findMany'))
1165
+ .get(/.*/)
1166
+ .reply(200, () => ({ data }))
1167
+ .persist();
1168
+
1169
+ const { result } = renderHook(
1170
+ () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }),
1171
+ {
1172
+ wrapper,
1173
+ },
1174
+ );
1175
+ await waitFor(() => {
1176
+ expect(result.current.data).toHaveLength(1);
1177
+ });
1178
+
1179
+ nock(makeUrl('User', 'update'))
1180
+ .post(/.*/)
1181
+ .reply(200, () => ({ data: null }));
1182
+
1183
+ const { result: mutationResult } = renderHook(
1184
+ () =>
1185
+ useClientQueries(schema).user.useUpdate({
1186
+ optimisticUpdate: true,
1187
+ invalidateQueries: false,
1188
+ }),
1189
+ {
1190
+ wrapper,
1191
+ },
1192
+ );
1193
+
1194
+ act(() =>
1195
+ mutationResult.current.mutate({
1196
+ where: { id: '1' },
1197
+ data: {
1198
+ posts: {
1199
+ upsert: {
1200
+ where: { id: 'p2' },
1201
+ create: { id: 'p2', title: 'post2' },
1202
+ update: { title: 'post3' },
1203
+ },
1204
+ },
1205
+ },
1206
+ }),
1207
+ );
1208
+
1209
+ await waitFor(() => {
1210
+ const cacheData: any = queryClient.getQueryData(
1211
+ getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }),
1212
+ );
1213
+ expect(cacheData).toHaveLength(2);
1214
+ expect(cacheData[0]).toMatchObject({ id: 'p2', title: 'post2', $optimistic: true });
1215
+ });
1216
+ });
1217
+
1218
+ it('works with optimistic upsert - update simple', async () => {
1219
+ const { queryClient, wrapper } = createWrapper();
1220
+
1221
+ const queryArgs = { where: { id: '1' } };
1222
+ const data = { id: '1', name: 'foo' };
1223
+
1224
+ nock(makeUrl('User', 'findUnique', queryArgs))
1225
+ .get(/.*/)
1226
+ .reply(200, () => ({ data }))
1227
+ .persist();
1228
+
1229
+ const { result } = renderHook(
1230
+ () => useClientQueries(schema).user.useFindUnique(queryArgs, { optimisticUpdate: true }),
1231
+ {
1232
+ wrapper,
1233
+ },
1234
+ );
1235
+ await waitFor(() => {
1236
+ expect(result.current.data).toMatchObject({ name: 'foo' });
1237
+ });
1238
+
1239
+ nock(makeUrl('User', 'upsert'))
1240
+ .post(/.*/)
1241
+ .reply(200, () => data);
1242
+
1243
+ const { result: mutationResult } = renderHook(
1244
+ () =>
1245
+ useClientQueries(schema).user.useUpsert({
1246
+ optimisticUpdate: true,
1247
+ invalidateQueries: false,
1248
+ }),
1249
+ {
1250
+ wrapper,
1251
+ },
1252
+ );
1253
+
1254
+ act(() => mutationResult.current.mutate({ ...queryArgs, update: { email: 'bar' }, create: { email: 'zee' } }));
1255
+
1256
+ await waitFor(() => {
1257
+ const cacheData = queryClient.getQueryData(
1258
+ getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }),
1259
+ );
1260
+ expect(cacheData).toMatchObject({ email: 'bar', $optimistic: true });
1261
+ });
1262
+ });
1263
+
1264
+ it('works with optimistic upsert - update updating nested query', async () => {
1265
+ const { queryClient, wrapper } = createWrapper();
1266
+
1267
+ const data: any = { id: '1', name: 'user1', posts: [{ id: 'p1', title: 'post1' }] };
1268
+
1269
+ nock(makeUrl('User', 'findUnique'))
1270
+ .get(/.*/)
1271
+ .reply(200, () => ({ data }))
1272
+ .persist();
1273
+
1274
+ const { result } = renderHook(
1275
+ () => useClientQueries(schema).user.useFindUnique({ where: { id: '1' } }, { optimisticUpdate: true }),
1276
+ {
1277
+ wrapper,
1278
+ },
1279
+ );
1280
+ await waitFor(() => {
1281
+ expect(result.current.data).toMatchObject({ id: '1' });
1282
+ });
1283
+
1284
+ nock(makeUrl('Post', 'upsert'))
1285
+ .post(/.*/)
1286
+ .reply(200, () => ({ data: null }));
1287
+
1288
+ const { result: mutationResult } = renderHook(
1289
+ () =>
1290
+ useClientQueries(schema).post.useUpsert({
1291
+ optimisticUpdate: true,
1292
+ invalidateQueries: false,
1293
+ }),
1294
+ {
1295
+ wrapper,
1296
+ },
1297
+ );
1298
+
1299
+ act(() =>
1300
+ mutationResult.current.mutate({
1301
+ where: { id: 'p1' },
1302
+ create: { id: 'p1', title: 'post1' },
1303
+ update: { title: 'post2' },
1304
+ }),
1305
+ );
1306
+
1307
+ await waitFor(() => {
1308
+ const cacheData: any = queryClient.getQueryData(
1309
+ getQueryKey('User', 'findUnique', { where: { id: '1' } }, { infinite: false, optimisticUpdate: true }),
1310
+ );
1311
+ const posts = cacheData.posts;
1312
+ expect(posts).toHaveLength(1);
1313
+ expect(posts[0]).toMatchObject({ id: 'p1', title: 'post2', $optimistic: true });
1314
+ });
1315
+ });
1316
+
1317
+ it('works with optimistic upsert - nested update updating query', async () => {
1318
+ const { queryClient, wrapper } = createWrapper();
1319
+
1320
+ const data: any = [{ id: 'p1', title: 'post1' }];
1321
+
1322
+ nock(makeUrl('Post', 'findMany'))
1323
+ .get(/.*/)
1324
+ .reply(200, () => ({ data }))
1325
+ .persist();
1326
+
1327
+ const { result } = renderHook(
1328
+ () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }),
1329
+ {
1330
+ wrapper,
1331
+ },
1332
+ );
1333
+ await waitFor(() => {
1334
+ expect(result.current.data).toHaveLength(1);
1335
+ });
1336
+
1337
+ nock(makeUrl('User', 'update'))
1338
+ .post(/.*/)
1339
+ .reply(200, () => ({ data: null }));
1340
+
1341
+ const { result: mutationResult } = renderHook(
1342
+ () =>
1343
+ useClientQueries(schema).user.useUpdate({
1344
+ optimisticUpdate: true,
1345
+ invalidateQueries: false,
1346
+ }),
1347
+ {
1348
+ wrapper,
1349
+ },
1350
+ );
1351
+
1352
+ act(() =>
1353
+ mutationResult.current.mutate({
1354
+ where: { id: '1' },
1355
+ data: {
1356
+ posts: {
1357
+ upsert: {
1358
+ where: { id: 'p1' },
1359
+ create: { id: 'p1', title: 'post1' },
1360
+ update: { title: 'post2' },
1361
+ },
1362
+ },
1363
+ },
1364
+ }),
1365
+ );
1366
+
1367
+ await waitFor(() => {
1368
+ const cacheData: any = queryClient.getQueryData(
1369
+ getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }),
1370
+ );
1371
+ expect(cacheData).toHaveLength(1);
1372
+ expect(cacheData[0]).toMatchObject({ id: 'p1', title: 'post2', $optimistic: true });
1373
+ });
1374
+ });
1375
+
1376
+ it('works with delete and invalidation', async () => {
1377
+ const { queryClient, wrapper } = createWrapper();
1378
+
1379
+ const data: any[] = [{ id: '1', name: 'foo' }];
1380
+
1381
+ nock(makeUrl('User', 'findMany'))
1382
+ .get(/.*/)
1383
+ .reply(200, () => ({ data }))
1384
+ .persist();
1385
+
1386
+ const { result } = renderHook(() => useClientQueries(schema).user.useFindMany(), {
1387
+ wrapper,
1388
+ });
1389
+ await waitFor(() => {
1390
+ expect(result.current.data).toHaveLength(1);
1391
+ });
1392
+
1393
+ nock(makeUrl('User', 'delete'))
1394
+ .delete(/.*/)
1395
+ .reply(200, () => {
1396
+ data.splice(0, 1);
1397
+ return { data: [] };
1398
+ });
1399
+
1400
+ const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useDelete(), {
1401
+ wrapper,
1402
+ });
1403
+
1404
+ act(() => mutationResult.current.mutate({ where: { id: '1' } }));
1405
+
1406
+ await waitFor(() => {
1407
+ const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined));
1408
+ expect(cacheData).toHaveLength(0);
1409
+ });
1410
+ });
1411
+
1412
+ it('works with optimistic delete simple', async () => {
1413
+ const { queryClient, wrapper } = createWrapper();
1414
+
1415
+ const data: any[] = [{ id: '1', name: 'foo' }];
1416
+
1417
+ nock(makeUrl('User', 'findMany'))
1418
+ .get(/.*/)
1419
+ .reply(200, () => ({ data }))
1420
+ .persist();
1421
+
1422
+ const { result } = renderHook(
1423
+ () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }),
1424
+ {
1425
+ wrapper,
1426
+ },
1427
+ );
1428
+ await waitFor(() => {
1429
+ expect(result.current.data).toHaveLength(1);
1430
+ });
1431
+
1432
+ nock(makeUrl('User', 'delete'))
1433
+ .delete(/.*/)
1434
+ .reply(200, () => ({ data }));
1435
+
1436
+ const { result: mutationResult } = renderHook(
1437
+ () =>
1438
+ useClientQueries(schema).user.useDelete({
1439
+ optimisticUpdate: true,
1440
+ invalidateQueries: false,
1441
+ }),
1442
+ {
1443
+ wrapper,
1444
+ },
1445
+ );
1446
+
1447
+ act(() => mutationResult.current.mutate({ where: { id: '1' } }));
1448
+
1449
+ await waitFor(() => {
1450
+ const cacheData = queryClient.getQueryData(
1451
+ getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }),
1452
+ );
1453
+ expect(cacheData).toHaveLength(0);
1454
+ });
1455
+ });
1456
+
1457
+ it('works with optimistic delete nested query', async () => {
1458
+ const { queryClient, wrapper } = createWrapper();
1459
+
1460
+ const data: any = { id: '1', name: 'foo', posts: [{ id: 'p1', title: 'post1' }] };
1461
+
1462
+ nock(makeUrl('User', 'findFirst'))
1463
+ .get(/.*/)
1464
+ .reply(200, () => ({ data }))
1465
+ .persist();
1466
+
1467
+ const { result } = renderHook(
1468
+ () =>
1469
+ useClientQueries(schema).user.useFindFirst(
1470
+ {
1471
+ include: { posts: true },
1472
+ },
1473
+ { optimisticUpdate: true },
1474
+ ),
1475
+ {
1476
+ wrapper,
1477
+ },
1478
+ );
1479
+ await waitFor(() => {
1480
+ expect(result.current.data).toMatchObject({ id: '1' });
1481
+ });
1482
+
1483
+ nock(makeUrl('Post', 'delete'))
1484
+ .delete(/.*/)
1485
+ .reply(200, () => ({ data }));
1486
+
1487
+ const { result: mutationResult } = renderHook(
1488
+ () =>
1489
+ useClientQueries(schema).post.useDelete({
1490
+ optimisticUpdate: true,
1491
+ invalidateQueries: false,
1492
+ }),
1493
+ {
1494
+ wrapper,
1495
+ },
1496
+ );
1497
+
1498
+ act(() => mutationResult.current.mutate({ where: { id: 'p1' } }));
1499
+
1500
+ await waitFor(() => {
1501
+ const cacheData: any = queryClient.getQueryData(
1502
+ getQueryKey(
1503
+ 'User',
1504
+ 'findFirst',
1505
+ { include: { posts: true } },
1506
+ { infinite: false, optimisticUpdate: true },
1507
+ ),
1508
+ );
1509
+ expect(cacheData.posts).toHaveLength(0);
1510
+ });
1511
+ });
1512
+
1513
+ it('works with optimistic nested delete update query', async () => {
1514
+ const { queryClient, wrapper } = createWrapper();
1515
+
1516
+ const data: any = [
1517
+ { id: 'p1', title: 'post1' },
1518
+ { id: 'p2', title: 'post2' },
1519
+ ];
1520
+
1521
+ nock(makeUrl('Post', 'findMany'))
1522
+ .get(/.*/)
1523
+ .reply(200, () => ({ data }))
1524
+ .persist();
1525
+
1526
+ const { result } = renderHook(
1527
+ () => useClientQueries(schema).post.useFindMany(undefined, { optimisticUpdate: true }),
1528
+ {
1529
+ wrapper,
1530
+ },
1531
+ );
1532
+ await waitFor(() => {
1533
+ expect(result.current.data).toHaveLength(2);
1534
+ });
1535
+
1536
+ nock(makeUrl('User', 'update'))
1537
+ .put(/.*/)
1538
+ .reply(200, () => ({ data }));
1539
+
1540
+ const { result: mutationResult } = renderHook(
1541
+ () =>
1542
+ useClientQueries(schema).user.useUpdate({
1543
+ optimisticUpdate: true,
1544
+ invalidateQueries: false,
1545
+ }),
1546
+ {
1547
+ wrapper,
1548
+ },
1549
+ );
1550
+
1551
+ act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { posts: { delete: { id: 'p1' } } } }));
1552
+
1553
+ await waitFor(() => {
1554
+ const cacheData: any = queryClient.getQueryData(
1555
+ getQueryKey('Post', 'findMany', undefined, { infinite: false, optimisticUpdate: true }),
1556
+ );
1557
+ expect(cacheData).toHaveLength(1);
1558
+ });
1559
+ });
1560
+
1561
+ it('top-level mutation and nested-read invalidation', async () => {
1562
+ const { queryClient, wrapper } = createWrapper();
1563
+
1564
+ const queryArgs = { where: { id: '1' }, include: { posts: true } };
1565
+ const data = { posts: [{ id: '1', title: 'post1' }] };
1566
+
1567
+ nock(makeUrl('User', 'findUnique', queryArgs))
1568
+ .get(/.*/)
1569
+ .reply(200, () => ({ data }))
1570
+ .persist();
1571
+
1572
+ const { result } = renderHook(() => useClientQueries(schema).user.useFindUnique(queryArgs), {
1573
+ wrapper,
1574
+ });
1575
+ await waitFor(() => {
1576
+ expect(result.current.data).toMatchObject(data);
1577
+ });
1578
+
1579
+ nock(makeUrl('Post', 'update'))
1580
+ .put(/.*/)
1581
+ .reply(200, () => {
1582
+ data.posts[0]!.title = 'post2';
1583
+ return data;
1584
+ });
1585
+
1586
+ const { result: mutationResult } = renderHook(() => useClientQueries(schema).post.useUpdate(), {
1587
+ wrapper,
1588
+ });
1589
+
1590
+ act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'post2' } }));
1591
+
1592
+ await waitFor(() => {
1593
+ const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs));
1594
+ expect(cacheData.posts[0].title).toBe('post2');
1595
+ });
1596
+ });
1597
+
1598
+ it('nested mutation and top-level-read invalidation', async () => {
1599
+ const { queryClient, wrapper } = createWrapper();
1600
+
1601
+ const data = [{ id: '1', title: 'post1', ownerId: '1' }];
1602
+
1603
+ nock(makeUrl('Post', 'findMany'))
1604
+ .get(/.*/)
1605
+ .reply(200, () => ({
1606
+ data,
1607
+ }))
1608
+ .persist();
1609
+
1610
+ const { result } = renderHook(() => useClientQueries(schema).post.useFindMany(), {
1611
+ wrapper,
1612
+ });
1613
+ await waitFor(() => {
1614
+ expect(result.current.data).toMatchObject(data);
1615
+ });
1616
+
1617
+ nock(makeUrl('User', 'update'))
1618
+ .put(/.*/)
1619
+ .reply(200, () => {
1620
+ data.push({ id: '2', title: 'post2', ownerId: '1' });
1621
+ return data;
1622
+ });
1623
+
1624
+ const { result: mutationResult } = renderHook(() => useClientQueries(schema).user.useUpdate(), {
1625
+ wrapper,
1626
+ });
1627
+
1628
+ act(() =>
1629
+ mutationResult.current.mutate({ where: { id: '1' }, data: { posts: { create: { title: 'post2' } } } }),
1630
+ );
1631
+
1632
+ await waitFor(() => {
1633
+ const cacheData: any = queryClient.getQueryData(getQueryKey('Post', 'findMany', undefined));
1634
+ expect(cacheData).toHaveLength(2);
1635
+ });
1636
+ });
1637
+
1638
+ it('optimistic create with custom provider', async () => {
1639
+ const { queryClient, wrapper } = createWrapper();
1640
+
1641
+ const data: any[] = [];
1642
+
1643
+ nock(makeUrl('User', 'findMany'))
1644
+ .get(/.*/)
1645
+ .reply(200, () => ({ data }))
1646
+ .persist();
1647
+
1648
+ const { result } = renderHook(
1649
+ () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }),
1650
+ {
1651
+ wrapper,
1652
+ },
1653
+ );
1654
+ await waitFor(() => {
1655
+ expect(result.current.data).toHaveLength(0);
1656
+ });
1657
+
1658
+ nock(makeUrl('User', 'create'))
1659
+ .post(/.*/)
1660
+ .reply(200, () => ({ data: null }))
1661
+ .persist();
1662
+
1663
+ const { result: mutationResult1 } = renderHook(
1664
+ () =>
1665
+ useClientQueries(schema).user.useCreate({
1666
+ optimisticUpdate: true,
1667
+ invalidateQueries: false,
1668
+ optimisticDataProvider: ({ queryModel, queryOperation }) => {
1669
+ if (queryModel === 'User' && queryOperation === 'findMany') {
1670
+ return { kind: 'Skip' };
1671
+ } else {
1672
+ return { kind: 'ProceedDefault' };
1673
+ }
1674
+ },
1675
+ }),
1676
+ {
1677
+ wrapper,
1678
+ },
1679
+ );
1680
+
1681
+ act(() => mutationResult1.current.mutate({ data: { email: 'foo' } }));
1682
+
1683
+ // cache should not update
1684
+ await waitFor(() => {
1685
+ const cacheData: any = queryClient.getQueryData(
1686
+ getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }),
1687
+ );
1688
+ expect(cacheData).toHaveLength(0);
1689
+ });
1690
+
1691
+ const { result: mutationResult2 } = renderHook(
1692
+ () =>
1693
+ useClientQueries(schema).user.useCreate({
1694
+ optimisticUpdate: true,
1695
+ invalidateQueries: false,
1696
+ optimisticDataProvider: ({ queryModel, queryOperation, currentData, mutationArgs }) => {
1697
+ if (queryModel === 'User' && queryOperation === 'findMany') {
1698
+ return {
1699
+ kind: 'Update',
1700
+ data: [
1701
+ ...currentData,
1702
+ { id: 100, email: mutationArgs.data.email + 'hooray', $optimistic: true },
1703
+ ],
1704
+ };
1705
+ } else {
1706
+ return { kind: 'ProceedDefault' };
1707
+ }
1708
+ },
1709
+ }),
1710
+ {
1711
+ wrapper,
1712
+ },
1713
+ );
1714
+
1715
+ act(() => mutationResult2.current.mutate({ data: { email: 'foo' } }));
1716
+
1717
+ // cache should update
1718
+ await waitFor(() => {
1719
+ const cacheData: any = queryClient.getQueryData(
1720
+ getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }),
1721
+ );
1722
+ expect(cacheData).toHaveLength(1);
1723
+ expect(cacheData[0].$optimistic).toBe(true);
1724
+ expect(cacheData[0].id).toBeTruthy();
1725
+ expect(cacheData[0].email).toBe('foohooray');
1726
+ });
1727
+ });
1728
+
1729
+ it('optimistic update mixed with non-zenstack queries', async () => {
1730
+ const { queryClient, wrapper } = createWrapper();
1731
+
1732
+ // non-zenstack query
1733
+ const { result: myQueryResult } = renderHook(
1734
+ () => useQuery({ queryKey: ['myQuery'], queryFn: () => ({ data: 'myData' }) }),
1735
+ {
1736
+ wrapper,
1737
+ },
1738
+ );
1739
+ await waitFor(() => {
1740
+ expect(myQueryResult.current.data).toEqual({ data: 'myData' });
1741
+ });
1742
+
1743
+ const data: any[] = [];
1744
+
1745
+ nock(makeUrl('User', 'findMany'))
1746
+ .get(/.*/)
1747
+ .reply(200, () => ({ data }))
1748
+ .persist();
1749
+
1750
+ const { result } = renderHook(
1751
+ () => useClientQueries(schema).user.useFindMany(undefined, { optimisticUpdate: true }),
1752
+ {
1753
+ wrapper,
1754
+ },
1755
+ );
1756
+ await waitFor(() => {
1757
+ expect(result.current.data).toHaveLength(0);
1758
+ });
1759
+
1760
+ nock(makeUrl('User', 'create'))
1761
+ .post(/.*/)
1762
+ .reply(200, () => ({ data: null }));
1763
+
1764
+ const { result: mutationResult } = renderHook(
1765
+ () =>
1766
+ useClientQueries(schema).user.useCreate({
1767
+ optimisticUpdate: true,
1768
+ invalidateQueries: false,
1769
+ }),
1770
+ {
1771
+ wrapper,
1772
+ },
1773
+ );
1774
+
1775
+ act(() => mutationResult.current.mutate({ data: { email: 'foo' } }));
1776
+
1777
+ await waitFor(() => {
1778
+ const cacheData: any = queryClient.getQueryData(
1779
+ getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }),
1780
+ );
1781
+ expect(cacheData).toHaveLength(1);
1782
+ expect(cacheData[0].$optimistic).toBe(true);
1783
+ expect(cacheData[0].id).toBeTruthy();
1784
+ expect(cacheData[0].email).toBe('foo');
1785
+ });
1786
+ });
1787
+ });