Skip to content

Commit a75f8d9

Browse files
authored
fix: optimistic updates for nested queries should work then optional relationships have null values (#1884)
Co-authored-by: Simon Zimmerman <[email protected]>
1 parent b7c6c87 commit a75f8d9

File tree

3 files changed

+391
-5
lines changed

3 files changed

+391
-5
lines changed

packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx

+347-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { RequestHandlerContext, useInfiniteModelQuery, useModelMutation, useMode
1111
import { getQueryKey } from '../src/runtime/common';
1212
import { modelMeta } from './test-model-meta';
1313

14+
const BASE_URL = 'http://localhost';
15+
1416
describe('Tanstack Query React Hooks V5 Test', () => {
1517
function createWrapper() {
1618
const queryClient = new QueryClient();
@@ -25,7 +27,7 @@ describe('Tanstack Query React Hooks V5 Test', () => {
2527
}
2628

2729
function makeUrl(model: string, operation: string, args?: unknown) {
28-
let r = `http://localhost/api/model/${model}/${operation}`;
30+
let r = `${BASE_URL}/api/model/${model}/${operation}`;
2931
if (args) {
3032
r += `?q=${encodeURIComponent(JSON.stringify(args))}`;
3133
}
@@ -345,6 +347,350 @@ describe('Tanstack Query React Hooks V5 Test', () => {
345347
});
346348
});
347349

350+
it('optimistic create updating deeply nested query', async () => {
351+
const { queryClient, wrapper } = createWrapper();
352+
353+
// populate the cache with a user
354+
355+
const userData: any[] = [{ id: '1', name: 'user1', posts: [] }];
356+
357+
nock(BASE_URL)
358+
.get('/api/model/User/findMany')
359+
.query(true)
360+
.reply(200, () => {
361+
console.log('Querying data:', JSON.stringify(userData));
362+
return { data: userData };
363+
})
364+
.persist();
365+
366+
const { result: userResult } = renderHook(
367+
() =>
368+
useModelQuery(
369+
'User',
370+
makeUrl('User', 'findMany'),
371+
{
372+
include: {
373+
posts: {
374+
include: {
375+
category: true,
376+
},
377+
},
378+
},
379+
},
380+
{ optimisticUpdate: true }
381+
),
382+
{
383+
wrapper,
384+
}
385+
);
386+
await waitFor(() => {
387+
expect(userResult.current.data).toHaveLength(1);
388+
});
389+
390+
// pupulate the cache with a category
391+
const categoryData: any[] = [{ id: '1', name: 'category1', posts: [] }];
392+
393+
nock(BASE_URL)
394+
.get('/api/model/Category/findMany')
395+
.query(true)
396+
.reply(200, () => {
397+
console.log('Querying data:', JSON.stringify(categoryData));
398+
return { data: categoryData };
399+
})
400+
.persist();
401+
402+
const { result: categoryResult } = renderHook(
403+
() =>
404+
useModelQuery(
405+
'Category',
406+
makeUrl('Category', 'findMany'),
407+
{ include: { posts: true } },
408+
{ optimisticUpdate: true }
409+
),
410+
{
411+
wrapper,
412+
}
413+
);
414+
await waitFor(() => {
415+
expect(categoryResult.current.data).toHaveLength(1);
416+
});
417+
418+
// create a post and connect it to the category
419+
nock(BASE_URL)
420+
.post('/api/model/Post/create')
421+
.reply(200, () => {
422+
console.log('Not mutating data');
423+
return { data: null };
424+
});
425+
426+
const { result: mutationResult } = renderHook(
427+
() =>
428+
useModelMutation('Post', 'POST', makeUrl('Post', 'create'), modelMeta, {
429+
optimisticUpdate: true,
430+
invalidateQueries: false,
431+
}),
432+
{
433+
wrapper,
434+
}
435+
);
436+
437+
act(() =>
438+
mutationResult.current.mutate({
439+
data: { title: 'post1', owner: { connect: { id: '1' } }, category: { connect: { id: '1' } } },
440+
})
441+
);
442+
443+
// assert that the post was created and connected to the category
444+
await waitFor(() => {
445+
const cacheData: any = queryClient.getQueryData(
446+
getQueryKey(
447+
'Category',
448+
'findMany',
449+
{
450+
include: {
451+
posts: true,
452+
},
453+
},
454+
{ infinite: false, optimisticUpdate: true }
455+
)
456+
);
457+
const posts = cacheData[0].posts;
458+
expect(posts).toHaveLength(1);
459+
console.log('category.posts', posts[0]);
460+
expect(posts[0]).toMatchObject({
461+
$optimistic: true,
462+
id: expect.any(String),
463+
title: 'post1',
464+
ownerId: '1',
465+
});
466+
});
467+
468+
// assert that the post was created and connected to the user, and included the category
469+
await waitFor(() => {
470+
const cacheData: any = queryClient.getQueryData(
471+
getQueryKey(
472+
'User',
473+
'findMany',
474+
{
475+
include: {
476+
posts: {
477+
include: {
478+
category: true,
479+
},
480+
},
481+
},
482+
},
483+
{ infinite: false, optimisticUpdate: true }
484+
)
485+
);
486+
const posts = cacheData[0].posts;
487+
expect(posts).toHaveLength(1);
488+
console.log('user.posts', posts[0]);
489+
expect(posts[0]).toMatchObject({
490+
$optimistic: true,
491+
id: expect.any(String),
492+
title: 'post1',
493+
ownerId: '1',
494+
categoryId: '1',
495+
// TODO: should this include the category object and not just the foreign key?
496+
// category: { $optimistic: true, id: '1', name: 'category1' },
497+
});
498+
});
499+
});
500+
501+
it('optimistic update with optional one-to-many relationship', async () => {
502+
const { queryClient, wrapper } = createWrapper();
503+
504+
// populate the cache with a post, with an optional category relatonship
505+
const postData: any = {
506+
id: '1',
507+
title: 'post1',
508+
ownerId: '1',
509+
categoryId: null,
510+
category: null,
511+
};
512+
513+
const data: any[] = [postData];
514+
515+
nock(makeUrl('Post', 'findMany'))
516+
.get(/.*/)
517+
.query(true)
518+
.reply(200, () => {
519+
console.log('Querying data:', JSON.stringify(data));
520+
return { data };
521+
})
522+
.persist();
523+
524+
const { result: postResult } = renderHook(
525+
() =>
526+
useModelQuery(
527+
'Post',
528+
makeUrl('Post', 'findMany'),
529+
{
530+
include: {
531+
category: true,
532+
},
533+
},
534+
{ optimisticUpdate: true }
535+
),
536+
{
537+
wrapper,
538+
}
539+
);
540+
await waitFor(() => {
541+
expect(postResult.current.data).toHaveLength(1);
542+
});
543+
544+
// mock a put request to update the post title
545+
nock(makeUrl('Post', 'update'))
546+
.put(/.*/)
547+
.reply(200, () => {
548+
console.log('Mutating data');
549+
postData.title = 'postA';
550+
return { data: postData };
551+
});
552+
553+
const { result: mutationResult } = renderHook(
554+
() =>
555+
useModelMutation('Post', 'PUT', makeUrl('Post', 'update'), modelMeta, {
556+
optimisticUpdate: true,
557+
invalidateQueries: false,
558+
}),
559+
{
560+
wrapper,
561+
}
562+
);
563+
564+
act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'postA' } }));
565+
566+
// assert that the post was updated despite the optional (null) category relationship
567+
await waitFor(() => {
568+
const cacheData: any = queryClient.getQueryData(
569+
getQueryKey(
570+
'Post',
571+
'findMany',
572+
{
573+
include: {
574+
category: true,
575+
},
576+
},
577+
{ infinite: false, optimisticUpdate: true }
578+
)
579+
);
580+
const posts = cacheData;
581+
expect(posts).toHaveLength(1);
582+
expect(posts[0]).toMatchObject({
583+
$optimistic: true,
584+
id: expect.any(String),
585+
title: 'postA',
586+
ownerId: '1',
587+
categoryId: null,
588+
category: null,
589+
});
590+
});
591+
});
592+
593+
it('optimistic update with nested optional one-to-many relationship', async () => {
594+
const { queryClient, wrapper } = createWrapper();
595+
596+
// populate the cache with a user and a post, with an optional category
597+
const postData: any = {
598+
id: '1',
599+
title: 'post1',
600+
ownerId: '1',
601+
categoryId: null,
602+
category: null,
603+
};
604+
605+
const userData: any[] = [{ id: '1', name: 'user1', posts: [postData] }];
606+
607+
nock(BASE_URL)
608+
.get('/api/model/User/findMany')
609+
.query(true)
610+
.reply(200, () => {
611+
console.log('Querying data:', JSON.stringify(userData));
612+
return { data: userData };
613+
})
614+
.persist();
615+
616+
const { result: userResult } = renderHook(
617+
() =>
618+
useModelQuery(
619+
'User',
620+
makeUrl('User', 'findMany'),
621+
{
622+
include: {
623+
posts: {
624+
include: {
625+
category: true,
626+
},
627+
},
628+
},
629+
},
630+
{ optimisticUpdate: true }
631+
),
632+
{
633+
wrapper,
634+
}
635+
);
636+
await waitFor(() => {
637+
expect(userResult.current.data).toHaveLength(1);
638+
});
639+
640+
// mock a put request to update the post title
641+
nock(BASE_URL)
642+
.put('/api/model/Post/update')
643+
.reply(200, () => {
644+
console.log('Mutating data');
645+
postData.title = 'postA';
646+
return { data: postData };
647+
});
648+
649+
const { result: mutationResult } = renderHook(
650+
() =>
651+
useModelMutation('Post', 'PUT', makeUrl('Post', 'update'), modelMeta, {
652+
optimisticUpdate: true,
653+
invalidateQueries: false,
654+
}),
655+
{
656+
wrapper,
657+
}
658+
);
659+
660+
act(() => mutationResult.current.mutate({ where: { id: '1' }, data: { title: 'postA' } }));
661+
662+
// assert that the post was updated
663+
await waitFor(() => {
664+
const cacheData: any = queryClient.getQueryData(
665+
getQueryKey(
666+
'User',
667+
'findMany',
668+
{
669+
include: {
670+
posts: {
671+
include: {
672+
category: true,
673+
},
674+
},
675+
},
676+
},
677+
{ infinite: false, optimisticUpdate: true }
678+
)
679+
);
680+
const posts = cacheData[0].posts;
681+
expect(posts).toHaveLength(1);
682+
console.log('user.posts', posts[0]);
683+
expect(posts[0]).toMatchObject({
684+
$optimistic: true,
685+
id: expect.any(String),
686+
title: 'postA',
687+
ownerId: '1',
688+
categoryId: null,
689+
category: null,
690+
});
691+
});
692+
});
693+
348694
it('optimistic nested create updating query', async () => {
349695
const { queryClient, wrapper } = createWrapper();
350696

0 commit comments

Comments
 (0)