diff --git a/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx index a52da3316..63c22e4eb 100644 --- a/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx +++ b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx @@ -1,6 +1,8 @@ /** * @jest-environment jsdom */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ import { QueryClient, QueryClientProvider } from '@tanstack/react-query-v5'; import { act, renderHook, waitFor } from '@testing-library/react'; import nock from 'nock'; @@ -470,6 +472,118 @@ describe('Tanstack Query React Hooks V5 Test', () => { }); }); + it('optimistic upsert - create', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any[] = []; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'upsert')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('User', 'POST', makeUrl('User', 'upsert'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => + mutationResult.current.mutate({ + where: { id: '1' }, + create: { id: '1', name: 'foo' }, + update: { name: 'bar' }, + }) + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData( + getQueryKey('User', 'findMany', undefined, { infinite: false, optimisticUpdate: true }) + ); + expect(cacheData).toHaveLength(1); + expect(cacheData[0].$optimistic).toBe(true); + expect(cacheData[0].id).toBeTruthy(); + expect(cacheData[0].name).toBe('foo'); + }); + }); + + it('optimistic upsert - update', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: '1' } }; + const data = { id: '1', name: 'foo' }; + + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); + }); + + nock(makeUrl('User', 'upsert')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return data; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('User', 'POST', makeUrl('User', 'upsert'), modelMeta, { + optimisticUpdate: true, + invalidateQueries: false, + }), + { + wrapper, + } + ); + + act(() => mutationResult.current.mutate({ ...queryArgs, update: { name: 'bar' }, create: { name: 'zee' } })); + + await waitFor(() => { + const cacheData = queryClient.getQueryData( + getQueryKey('User', 'findUnique', queryArgs, { infinite: false, optimisticUpdate: true }) + ); + expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true }); + }); + }); + it('delete and invalidation', async () => { const { queryClient, wrapper } = createWrapper(); diff --git a/packages/plugins/tanstack-query/tests/react-hooks.test.tsx b/packages/plugins/tanstack-query/tests/react-hooks.test.tsx index c788988de..48ff6b650 100644 --- a/packages/plugins/tanstack-query/tests/react-hooks.test.tsx +++ b/packages/plugins/tanstack-query/tests/react-hooks.test.tsx @@ -1,6 +1,8 @@ /** * @jest-environment jsdom */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { act, renderHook, waitFor } from '@testing-library/react'; import nock from 'nock'; @@ -389,6 +391,122 @@ describe('Tanstack Query React Hooks V4 Test', () => { }); }); + it('optimistic upsert - create', async () => { + const { queryClient, wrapper } = createWrapper(); + + const data: any[] = []; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => useModelQuery('User', makeUrl('User', 'findMany'), undefined, { optimisticUpdate: true }), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'upsert')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation( + 'User', + 'POST', + makeUrl('User', 'upsert'), + modelMeta, + { optimisticUpdate: true, invalidateQueries: false }, + undefined + ), + { + wrapper, + } + ); + + act(() => + mutationResult.current.mutate({ + where: { id: '1' }, + create: { id: '1', name: 'foo' }, + update: { name: 'bar' }, + }) + ); + + await waitFor(() => { + const cacheData: any = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); + expect(cacheData).toHaveLength(1); + expect(cacheData[0].$optimistic).toBe(true); + expect(cacheData[0].id).toBeTruthy(); + expect(cacheData[0].name).toBe('foo'); + }); + }); + + it('optimistic upsert - update', async () => { + const { queryClient, wrapper } = createWrapper(); + + const queryArgs = { where: { id: '1' } }; + const data = { id: '1', name: 'foo' }; + + nock(makeUrl('User', 'findUnique', queryArgs)) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook( + () => useModelQuery('User', makeUrl('User', 'findUnique'), queryArgs, { optimisticUpdate: true }), + { + wrapper, + } + ); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); + }); + + nock(makeUrl('User', 'upsert')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return data; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation( + 'User', + 'POST', + makeUrl('User', 'upsert'), + modelMeta, + { optimisticUpdate: true, invalidateQueries: false }, + undefined + ), + { + wrapper, + } + ); + + act(() => mutationResult.current.mutate({ ...queryArgs, update: { name: 'bar' }, create: { name: 'zee' } })); + + await waitFor(() => { + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); + expect(cacheData).toMatchObject({ name: 'bar', $optimistic: true }); + }); + }); + it('delete and invalidation', async () => { const { queryClient, wrapper } = createWrapper(); diff --git a/packages/runtime/src/cross/mutator.ts b/packages/runtime/src/cross/mutator.ts index 874689c2d..e38570399 100644 --- a/packages/runtime/src/cross/mutator.ts +++ b/packages/runtime/src/cross/mutator.ts @@ -75,6 +75,31 @@ export async function applyMutation( } }, + upsert: (model, args) => { + if (model === queryModel && args?.where && args?.create && args?.update) { + // first see if a matching update can be applied + const updateResult = updateMutate( + queryModel, + resultData, + model, + { where: args.where, data: args.update }, + modelMeta, + logging + ); + if (updateResult) { + resultData = updateResult; + updated = true; + } else { + // if not, try to apply a create + const createResult = createMutate(queryModel, queryOp, resultData, args.create, modelMeta, logging); + if (createResult) { + resultData = createResult; + updated = true; + } + } + } + }, + delete: (model, args) => { if (model === queryModel) { const r = deleteMutate(queryModel, resultData, model, args, modelMeta, logging);