Skip to content

Commit 374e962

Browse files
authored
fix(hooks): support optimistic update for "upsert" (#1767)
1 parent fc940eb commit 374e962

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

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

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* @jest-environment jsdom
33
*/
4+
/* eslint-disable @typescript-eslint/no-explicit-any */
5+
/* eslint-disable @typescript-eslint/ban-ts-comment */
46
import { QueryClient, QueryClientProvider } from '@tanstack/react-query-v5';
57
import { act, renderHook, waitFor } from '@testing-library/react';
68
import nock from 'nock';
@@ -470,6 +472,118 @@ describe('Tanstack Query React Hooks V5 Test', () => {
470472
});
471473
});
472474

475+
it('optimistic upsert - create', async () => {
476+
const { queryClient, wrapper } = createWrapper();
477+
478+
const data: any[] = [];
479+
480+
nock(makeUrl('User', 'findMany'))
481+
.get(/.*/)
482+
.reply(200, () => {
483+
console.log('Querying data:', JSON.stringify(data));
484+
return { data };
485+
})
486+
.persist();
487+
488+
const { result } = renderHook(
489+
() => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }),
490+
{
491+
wrapper,
492+
}
493+
);
494+
await waitFor(() => {
495+
expect(result.current.data).toHaveLength(0);
496+
});
497+
498+
nock(makeUrl('User', 'upsert'))
499+
.post(/.*/)
500+
.reply(200, () => {
501+
console.log('Not mutating data');
502+
return { data: null };
503+
});
504+
505+
const { result: mutationResult } = renderHook(
506+
() =>
507+
useModelMutation('User', 'POST', makeUrl('User', 'upsert'), modelMeta, {
508+
optimisticUpdate: true,
509+
invalidateQueries: false,
510+
}),
511+
{
512+
wrapper,
513+
}
514+
);
515+
516+
act(() =>
517+
mutationResult.current.mutate({
518+
where: { id: '1' },
519+
create: { id: '1', name: 'foo' },
520+
update: { name: 'bar' },
521+
})
522+
);
523+
524+
await waitFor(() => {
525+
const cacheData: any = queryClient.getQueryData(
526+
getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true })
527+
);
528+
expect(cacheData).toHaveLength(1);
529+
expect(cacheData[0].$optimistic).toBe(true);
530+
expect(cacheData[0].id).toBeTruthy();
531+
expect(cacheData[0].name).toBe('foo');
532+
});
533+
});
534+
535+
it('optimistic upsert - update', async () => {
536+
const { queryClient, wrapper } = createWrapper();
537+
538+
const queryArgs = { where: { id: '1' } };
539+
const data = { id: '1', name: 'foo' };
540+
541+
nock(makeUrl('User', 'findUnique', queryArgs))
542+
.get(/.*/)
543+
.reply(200, () => {
544+
console.log('Querying data:', JSON.stringify(data));
545+
return { data };
546+
})
547+
.persist();
548+
549+
const { result } = renderHook(
550+
() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }),
551+
{
552+
wrapper,
553+
}
554+
);
555+
await waitFor(() => {
556+
expect(result.current.data).toMatchObject({ name: 'foo' });
557+
});
558+
559+
nock(makeUrl('User', 'upsert'))
560+
.post(/.*/)
561+
.reply(200, () => {
562+
console.log('Not mutating data');
563+
return data;
564+
});
565+
566+
const { result: mutationResult } = renderHook(
567+
() =>
568+
useModelMutation('User', 'POST', makeUrl('User', 'upsert'), modelMeta, {
569+
optimisticUpdate: true,
570+
invalidateQueries: false,
571+
}),
572+
{
573+
wrapper,
574+
}
575+
);
576+
577+
act(() => mutationResult.current.mutate({ ...queryArgs, update: { name: 'bar' }, create: { name: 'zee' } }));
578+
579+
await waitFor(() => {
580+
const cacheData = queryClient.getQueryData(
581+
getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true })
582+
);
583+
expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true });
584+
});
585+
});
586+
473587
it('delete and invalidation', async () => {
474588
const { queryClient, wrapper } = createWrapper();
475589

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

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/**
22
* @jest-environment jsdom
33
*/
4+
/* eslint-disable @typescript-eslint/ban-ts-comment */
5+
/* eslint-disable @typescript-eslint/no-explicit-any */
46
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
57
import { act, renderHook, waitFor } from '@testing-library/react';
68
import nock from 'nock';
@@ -389,6 +391,122 @@ describe('Tanstack Query React Hooks V4 Test', () => {
389391
});
390392
});
391393

394+
it('optimistic upsert - create', async () => {
395+
const { queryClient, wrapper } = createWrapper();
396+
397+
const data: any[] = [];
398+
399+
nock(makeUrl('User', 'findMany'))
400+
.get(/.*/)
401+
.reply(200, () => {
402+
console.log('Querying data:', JSON.stringify(data));
403+
return { data };
404+
})
405+
.persist();
406+
407+
const { result } = renderHook(
408+
() => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }),
409+
{
410+
wrapper,
411+
}
412+
);
413+
await waitFor(() => {
414+
expect(result.current.data).toHaveLength(0);
415+
});
416+
417+
nock(makeUrl('User', 'upsert'))
418+
.post(/.*/)
419+
.reply(200, () => {
420+
console.log('Not mutating data');
421+
return { data: null };
422+
});
423+
424+
const { result: mutationResult } = renderHook(
425+
() =>
426+
useModelMutation(
427+
'User',
428+
'POST',
429+
makeUrl('User', 'upsert'),
430+
modelMeta,
431+
{ optimisticUpdate: true, invalidateQueries: false },
432+
undefined
433+
),
434+
{
435+
wrapper,
436+
}
437+
);
438+
439+
act(() =>
440+
mutationResult.current.mutate({
441+
where: { id: '1' },
442+
create: { id: '1', name: 'foo' },
443+
update: { name: 'bar' },
444+
})
445+
);
446+
447+
await waitFor(() => {
448+
const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined));
449+
expect(cacheData).toHaveLength(1);
450+
expect(cacheData[0].$optimistic).toBe(true);
451+
expect(cacheData[0].id).toBeTruthy();
452+
expect(cacheData[0].name).toBe('foo');
453+
});
454+
});
455+
456+
it('optimistic upsert - update', async () => {
457+
const { queryClient, wrapper } = createWrapper();
458+
459+
const queryArgs = { where: { id: '1' } };
460+
const data = { id: '1', name: 'foo' };
461+
462+
nock(makeUrl('User', 'findUnique', queryArgs))
463+
.get(/.*/)
464+
.reply(200, () => {
465+
console.log('Querying data:', JSON.stringify(data));
466+
return { data };
467+
})
468+
.persist();
469+
470+
const { result } = renderHook(
471+
() => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }),
472+
{
473+
wrapper,
474+
}
475+
);
476+
await waitFor(() => {
477+
expect(result.current.data).toMatchObject({ name: 'foo' });
478+
});
479+
480+
nock(makeUrl('User', 'upsert'))
481+
.post(/.*/)
482+
.reply(200, () => {
483+
console.log('Not mutating data');
484+
return data;
485+
});
486+
487+
const { result: mutationResult } = renderHook(
488+
() =>
489+
useModelMutation(
490+
'User',
491+
'POST',
492+
makeUrl('User', 'upsert'),
493+
modelMeta,
494+
{ optimisticUpdate: true, invalidateQueries: false },
495+
undefined
496+
),
497+
{
498+
wrapper,
499+
}
500+
);
501+
502+
act(() => mutationResult.current.mutate({ ...queryArgs, update: { name: 'bar' }, create: { name: 'zee' } }));
503+
504+
await waitFor(() => {
505+
const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs));
506+
expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true });
507+
});
508+
});
509+
392510
it('delete and invalidation', async () => {
393511
const { queryClient, wrapper } = createWrapper();
394512

packages/runtime/src/cross/mutator.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,31 @@ export async function applyMutation(
7575
}
7676
},
7777

78+
upsert: (model, args) => {
79+
if (model === queryModel && args?.where && args?.create && args?.update) {
80+
// first see if a matching update can be applied
81+
const updateResult = updateMutate(
82+
queryModel,
83+
resultData,
84+
model,
85+
{ where: args.where, data: args.update },
86+
modelMeta,
87+
logging
88+
);
89+
if (updateResult) {
90+
resultData = updateResult;
91+
updated = true;
92+
} else {
93+
// if not, try to apply a create
94+
const createResult = createMutate(queryModel, queryOp, resultData, args.create, modelMeta, logging);
95+
if (createResult) {
96+
resultData = createResult;
97+
updated = true;
98+
}
99+
}
100+
}
101+
},
102+
78103
delete: (model, args) => {
79104
if (model === queryModel) {
80105
const r = deleteMutate(queryModel, resultData, model, args, modelMeta, logging);

0 commit comments

Comments
 (0)