diff --git a/packages/plugins/swr/src/runtime/index.ts b/packages/plugins/swr/src/runtime/index.ts index 994ea178b..e8ce6a255 100644 --- a/packages/plugins/swr/src/runtime/index.ts +++ b/packages/plugins/swr/src/runtime/index.ts @@ -255,7 +255,7 @@ export function useModelMutation( method: 'POST' | 'PUT' | 'DELETE', url: string, data: unknown, - invalidate: Invalidator, + invalidate?: Invalidator, fetch?: FetchFn, checkReadBack?: C ): Promise { @@ -299,7 +299,10 @@ export async function mutationRequest( fetch, checkReadBack ); - await invalidate(getOperationFromUrl(url), data); + + if (invalidate) { + await invalidate(getOperationFromUrl(url), data); + } return r; } diff --git a/packages/plugins/swr/tests/react-hooks.test.tsx b/packages/plugins/swr/tests/react-hooks.test.tsx index f2a6d9aea..60c419bbc 100644 --- a/packages/plugins/swr/tests/react-hooks.test.tsx +++ b/packages/plugins/swr/tests/react-hooks.test.tsx @@ -9,8 +9,15 @@ import { renderHook, waitFor } from '@testing-library/react'; import { lowerCaseFirst } from 'lower-case-first'; import nock from 'nock'; -import { useSWRConfig } from 'swr'; -import { RequestHandlerContext, getQueryKey, mutationRequest, useModelQuery, useInvalidation } from '../src/runtime'; +import { SWRConfig, useSWRConfig } from 'swr'; +import { + RequestHandlerContext, + getQueryKey, + mutationRequest, + useModelQuery, + useInvalidation, + useModelMutation, +} from '../src/runtime'; import { modelMeta } from './test-model-meta'; import React from 'react'; @@ -24,13 +31,23 @@ function makeUrl(model: string, operation: string, args?: unknown) { return r; } -const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} -); - describe('SWR React Hooks Test', () => { - beforeEach(() => { + // let cache: Map; + const wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ); + + beforeEach(async () => { nock.cleanAll(); + const { result } = renderHook(() => useSWRConfig()); + await waitFor(async () => { + await result.current.mutate(() => true, undefined, { revalidate: false }); + }); + // await result.current.mutate(() => true, undefined, { revalidate: false }); }); it('simple query', async () => { @@ -54,12 +71,13 @@ describe('SWR React Hooks Test', () => { const { result: cacheResult } = renderHook(() => useSWRConfig()); await waitFor(() => { - const cacheData = cacheResult.current.cache.get(getQueryKey('User', 'findUnique', queryArgs)); + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findUnique', queryArgs)); expect(cacheData?.data).toMatchObject(data); }); }); - it('create and invalidation', async () => { + it('create and invalidation legacy', async () => { const data: any[] = []; nock(makeUrl('User', 'findMany')) @@ -83,10 +101,10 @@ describe('SWR React Hooks Test', () => { return { data: data[0] }; }); - const { result: useMutateResult } = renderHook(() => useInvalidation('User', modelMeta)); + const { result: invalidationResult } = renderHook(() => useInvalidation('User', modelMeta)); await waitFor(async () => { - const mutate = useMutateResult.current; + const mutate = invalidationResult.current; const r = await mutationRequest( 'POST', makeUrl('User', 'create', undefined), @@ -98,12 +116,100 @@ describe('SWR React Hooks Test', () => { const { result: cacheResult } = renderHook(() => useSWRConfig()); await waitFor(() => { - const cacheData = cacheResult.current.cache.get(getQueryKey('User', 'findMany', undefined)); + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findMany', undefined)); expect(cacheData?.data).toHaveLength(1); }); }); - it('update and invalidation', async () => { + it('create and invalidation hooks with invalidation', async () => { + 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', 'findMany'), { wrapper }); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => { + console.log('Mutating data'); + data.push({ id: '1', name: 'foo' }); + return { data: data[0] }; + }); + + const { result: useMutateResult } = renderHook(() => useModelMutation('User', 'POST', 'create', modelMeta), { + wrapper, + }); + + await waitFor(async () => { + const { trigger } = useMutateResult.current; + const r = await trigger({ data: { name: 'foo' } }); + console.log('Mutate result:', r); + }); + + const { result: cacheResult } = renderHook(() => useSWRConfig()); + await waitFor(() => { + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findMany', undefined)); + expect(cacheData?.data).toHaveLength(1); + }); + }); + + it('create and invalidation hooks no invalidation', async () => { + 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', 'findMany'), { wrapper }); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => { + console.log('Mutating data'); + data.push({ id: '1', name: 'foo' }); + return { data: data[0] }; + }); + + const { result: useMutateResult } = renderHook( + () => useModelMutation('User', 'POST', 'create', modelMeta, { revalidate: false }), + { + wrapper, + } + ); + + await waitFor(async () => { + const { trigger } = useMutateResult.current; + const r = await trigger({ data: { name: 'foo' } }); + console.log('Mutate result:', r); + }); + + const { result: cacheResult } = renderHook(() => useSWRConfig()); + await waitFor(() => { + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findMany', undefined)); + expect(cacheData?.data).toHaveLength(0); + }); + }); + + it('update and invalidation legacy', async () => { const queryArgs = { where: { id: '1' } }; const data = { id: '1', name: 'foo' }; @@ -143,7 +249,51 @@ describe('SWR React Hooks Test', () => { const { result: cacheResult } = renderHook(() => useSWRConfig()); await waitFor(() => { - const cacheData = cacheResult.current.cache.get(getQueryKey('User', 'findUnique', queryArgs)); + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findUnique', queryArgs)); + expect(cacheData?.data).toMatchObject({ name: 'bar' }); + }); + }); + + it('update and invalidation hooks', async () => { + 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', 'findUnique', queryArgs), { wrapper }); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); + }); + + nock(makeUrl('User', 'update')) + .put(/.*/) + .reply(200, () => { + console.log('Mutating data'); + data.name = 'bar'; + return data; + }); + + const { result: useMutateResult } = renderHook(() => useModelMutation('User', 'PUT', 'update', modelMeta), { + wrapper, + }); + + await waitFor(async () => { + const { trigger } = useMutateResult.current; + const r = await trigger({ ...queryArgs, data: { name: 'bar' } }); + console.log('Mutate result:', r); + }); + + const { result: cacheResult } = renderHook(() => useSWRConfig()); + await waitFor(() => { + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findUnique', queryArgs)); expect(cacheData?.data).toMatchObject({ name: 'bar' }); }); }); @@ -188,7 +338,8 @@ describe('SWR React Hooks Test', () => { const { result: cacheResult } = renderHook(() => useSWRConfig()); await waitFor(() => { - const cacheData = cacheResult.current.cache.get(getQueryKey('User', 'findUnique', queryArgs)); + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findUnique', queryArgs)); expect(cacheData?.data.posts[0].title).toBe('post2'); }); }); @@ -232,16 +383,11 @@ describe('SWR React Hooks Test', () => { const { result: cacheResult } = renderHook(() => useSWRConfig()); await waitFor(() => { - const cacheData = cacheResult.current.cache.get(getQueryKey('Post', 'findMany')); + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('Post', 'findMany')); expect(cacheData?.data).toHaveLength(2); }); }); -}); - -describe('SWR React Hooks Test separate due to potential nock issue', () => { - beforeEach(() => { - nock.cleanAll(); - }); it('independent mutation and query', async () => { const queryArgs = { where: { id: '1' } }; @@ -317,7 +463,196 @@ describe('SWR React Hooks Test separate due to potential nock issue', () => { const { result: cacheResult } = renderHook(() => useSWRConfig()); await waitFor(() => { - const cacheData = cacheResult.current.cache.get(getQueryKey('Post', 'findMany')); + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('Post', 'findMany')); + expect(cacheData?.data).toHaveLength(0); + }); + }); + + it('optimistic create single', async () => { + 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', 'findMany'), { wrapper }); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }); + + const { result: useMutateResult } = renderHook( + () => useModelMutation('User', 'POST', 'create', modelMeta, { optimisticUpdate: true, revalidate: false }), + { + wrapper, + } + ); + + await waitFor(async () => { + const { trigger } = useMutateResult.current; + const r = await trigger({ data: { name: 'foo' } }); + console.log('Mutate result:', r); + }); + + const { result: cacheResult } = renderHook(() => useSWRConfig()); + await waitFor(() => { + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findMany')); + expect(cacheData?.data).toHaveLength(1); + }); + }); + + it('optimistic create many', async () => { + 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', 'findMany'), { + wrapper, + }); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'createMany')) + .post(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data: null }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('User', 'POST', 'createMany', modelMeta, { + optimisticUpdate: true, + revalidate: false, + }), + { + wrapper, + } + ); + + await waitFor(async () => { + const { trigger } = mutationResult.current; + await trigger({ data: [{ name: 'foo' }, { name: 'bar' }] }); + }); + + const { result: cacheResult } = renderHook(() => useSWRConfig()); + await waitFor(() => { + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findMany')); + expect(cacheData?.data).toHaveLength(2); + }); + }); + + it('optimistic update', async () => { + 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', 'findUnique', queryArgs), { + wrapper, + }); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); + }); + + nock(makeUrl('User', 'update')) + .put(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return data; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('User', 'PUT', 'update', modelMeta, { + optimisticUpdate: true, + revalidate: false, + }), + { + wrapper, + } + ); + + await waitFor(async () => { + await mutationResult.current.trigger({ ...queryArgs, data: { name: 'bar' } }); + }); + + const { result: cacheResult } = renderHook(() => useSWRConfig()); + await waitFor(() => { + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findUnique', queryArgs)); + expect(cacheData?.data).toMatchObject({ name: 'bar', $optimistic: true }); + }); + }); + + it('optimistic delete', async () => { + const data: any[] = [{ id: '1', name: 'foo' }]; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => { + console.log('Querying data:', JSON.stringify(data)); + return { data }; + }) + .persist(); + + const { result } = renderHook(() => useModelQuery('User', 'findMany'), { + wrapper, + }); + await waitFor(() => { + expect(result.current.data).toHaveLength(1); + }); + + nock(makeUrl('User', 'delete')) + .delete(/.*/) + .reply(200, () => { + console.log('Not mutating data'); + return { data }; + }); + + const { result: mutationResult } = renderHook( + () => + useModelMutation('User', 'DELETE', 'delete', modelMeta, { + optimisticUpdate: true, + revalidate: false, + }), + { + wrapper, + } + ); + + await waitFor(async () => await mutationResult.current.trigger({ where: { id: '1' } })); + + const { result: cacheResult } = renderHook(() => useSWRConfig()); + await waitFor(() => { + const cache = cacheResult.current.cache; + const cacheData = cache.get(getQueryKey('User', 'findMany')); expect(cacheData?.data).toHaveLength(0); }); }); 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 c285a01f8..3c4f32890 100644 --- a/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx +++ b/packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx @@ -148,6 +148,49 @@ describe('Tanstack Query React Hooks V5 Test', () => { }); }); + it('create and no invalidation', 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')), { + wrapper, + }); + await waitFor(() => { + expect(result.current.data).toHaveLength(0); + }); + + nock(makeUrl('User', 'create')) + .post(/.*/) + .reply(200, () => { + console.log('Mutating data'); + data.push({ id: '1', name: 'foo' }); + return { data: data[0] }; + }); + + const { result: mutationResult } = renderHook( + () => useModelMutation('User', 'POST', makeUrl('User', 'create'), modelMeta, undefined, undefined, false), + { + wrapper, + } + ); + + act(() => mutationResult.current.mutate({ data: { name: 'foo' } })); + + await waitFor(() => { + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); + expect(cacheData).toHaveLength(0); + }); + }); + it('optimistic create single', async () => { const { queryClient, wrapper } = createWrapper(); @@ -307,6 +350,50 @@ describe('Tanstack Query React Hooks V5 Test', () => { }); }); + it('update and no invalidation', 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), { + wrapper, + }); + await waitFor(() => { + expect(result.current.data).toMatchObject({ name: 'foo' }); + }); + + nock(makeUrl('User', 'update')) + .put(/.*/) + .reply(200, () => { + console.log('Mutating data'); + data.name = 'bar'; + return data; + }); + + const { result: mutationResult } = renderHook( + () => useModelMutation('User', 'PUT', makeUrl('User', 'update'), modelMeta, undefined, undefined, false), + { + wrapper, + } + ); + + act(() => mutationResult.current.mutate({ ...queryArgs, data: { name: 'bar' } })); + + await waitFor(() => { + const cacheData = queryClient.getQueryData(getQueryKey('User', 'findUnique', queryArgs)); + expect(cacheData).toMatchObject({ name: 'foo' }); + }); + }); + it('optimistic update', async () => { const { queryClient, wrapper } = createWrapper();