From fef854048d0f6fd5bdf10413a4eb2693b0e766c9 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Kaiser Date: Wed, 18 Dec 2024 18:14:03 +0100 Subject: [PATCH] Fix `useUpdate` ignores `meta` when populating the query cache in pessimistic mode --- .../src/dataProvider/useUpdate.spec.tsx | 367 ++++++++++++++++++ .../ra-core/src/dataProvider/useUpdate.ts | 3 +- 2 files changed, 369 insertions(+), 1 deletion(-) diff --git a/packages/ra-core/src/dataProvider/useUpdate.spec.tsx b/packages/ra-core/src/dataProvider/useUpdate.spec.tsx index 3d4847f0a26..73ea6d7c786 100644 --- a/packages/ra-core/src/dataProvider/useUpdate.spec.tsx +++ b/packages/ra-core/src/dataProvider/useUpdate.spec.tsx @@ -510,7 +510,374 @@ describe('useUpdate', () => { }); }); }); + describe('pessimistic mutation mode', () => { + it('updates getOne query cache when dataProvider promise resolves', async () => { + const queryClient = new QueryClient(); + queryClient.setQueryData( + ['foo', 'getOne', { id: '1', meta: undefined }], + { id: 1, bar: 'bar' } + ); + const dataProvider = { + update: jest.fn(() => + Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) + ), + } as any; + let localUpdate; + const Dummy = () => { + const [update] = useUpdate(); + localUpdate = update; + return ; + }; + render( + + + + ); + localUpdate('foo', { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + }); + await waitFor(() => { + expect(dataProvider.update).toHaveBeenCalledWith('foo', { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + }); + }); + await waitFor(() => { + expect( + queryClient.getQueryData([ + 'foo', + 'getOne', + { id: '1', meta: undefined }, + ]) + ).toEqual({ + id: 1, + bar: 'baz', + }); + }); + }); + + it('updates getOne query cache when dataProvider promise resolves with meta', async () => { + const queryClient = new QueryClient(); + queryClient.setQueryData( + ['foo', 'getOne', { id: '1', meta: { key: 'value' } }], + { id: 1, bar: 'bar' } + ); + const dataProvider = { + update: jest.fn(() => + Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) + ), + } as any; + let localUpdate; + const Dummy = () => { + const [update] = useUpdate(); + localUpdate = update; + return ; + }; + render( + + + + ); + localUpdate('foo', { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + meta: { key: 'value' }, + }); + await waitFor(() => { + expect(dataProvider.update).toHaveBeenCalledWith('foo', { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + meta: { key: 'value' }, + }); + }); + await waitFor(() => { + expect( + queryClient.getQueryData([ + 'foo', + 'getOne', + { id: '1', meta: { key: 'value' } }, + ]) + ).toEqual({ + id: 1, + bar: 'baz', + }); + }); + }); + + it('updates getOne query cache when dataProvider promise resolves with meta at hook time', async () => { + const queryClient = new QueryClient(); + queryClient.setQueryData( + ['foo', 'getOne', { id: '1', meta: { key: 'value' } }], + { id: 1, bar: 'bar' } + ); + const dataProvider = { + update: jest.fn(() => + Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) + ), + } as any; + let localUpdate; + const Dummy = () => { + const [update] = useUpdate('foo', { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + meta: { key: 'value' }, + }); + localUpdate = update; + return ; + }; + render( + + + + ); + localUpdate(); + await waitFor(() => { + expect(dataProvider.update).toHaveBeenCalledWith('foo', { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + meta: { key: 'value' }, + }); + }); + await waitFor(() => { + expect( + queryClient.getQueryData([ + 'foo', + 'getOne', + { id: '1', meta: { key: 'value' } }, + ]) + ).toEqual({ + id: 1, + bar: 'baz', + }); + }); + }); + }); + + describe('optimistic mutation mode', () => { + it('updates getOne query cache immediately and invalidates query when dataProvider promise resolves', async () => { + const queryClient = new QueryClient(); + queryClient.setQueryData( + ['foo', 'getOne', { id: '1', meta: undefined }], + { id: 1, bar: 'bar' } + ); + const dataProvider = { + update: jest.fn(() => + Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) + ), + } as any; + const queryClientSpy = jest.spyOn( + queryClient, + 'invalidateQueries' + ); + let localUpdate; + const Dummy = () => { + const [update] = useUpdate(undefined, undefined, { + mutationMode: 'optimistic', + }); + localUpdate = update; + return ; + }; + render( + + + + ); + localUpdate('foo', { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + }); + await waitFor(() => { + expect( + queryClient.getQueryData([ + 'foo', + 'getOne', + { id: '1', meta: undefined }, + ]) + ).toEqual({ + id: 1, + bar: 'baz', + }); + }); + await waitFor(() => { + expect(dataProvider.update).toHaveBeenCalledWith('foo', { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + }); + }); + await waitFor(() => { + expect(queryClientSpy).toHaveBeenCalledWith({ + queryKey: [ + 'foo', + 'getOne', + { id: '1', meta: undefined }, + ], + }); + }); + }); + + it('updates getOne query cache immediately and invalidates query when dataProvider promise resolves with meta', async () => { + const queryClient = new QueryClient(); + queryClient.setQueryData( + ['foo', 'getOne', { id: '1', meta: { key: 'value' } }], + { id: 1, bar: 'bar' } + ); + const dataProvider = { + update: jest.fn(() => + Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) + ), + } as any; + const queryClientSpy = jest.spyOn( + queryClient, + 'invalidateQueries' + ); + let localUpdate; + const Dummy = () => { + const [update] = useUpdate(undefined, undefined, { + mutationMode: 'optimistic', + }); + localUpdate = update; + return ; + }; + render( + + + + ); + localUpdate('foo', { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + meta: { key: 'value' }, + }); + await waitFor(() => { + expect( + queryClient.getQueryData([ + 'foo', + 'getOne', + { id: '1', meta: { key: 'value' } }, + ]) + ).toEqual({ + id: 1, + bar: 'baz', + }); + }); + await waitFor(() => { + expect(dataProvider.update).toHaveBeenCalledWith('foo', { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + meta: { key: 'value' }, + }); + }); + await waitFor(() => { + expect(queryClientSpy).toHaveBeenCalledWith({ + queryKey: [ + 'foo', + 'getOne', + { id: '1', meta: { key: 'value' } }, + ], + }); + }); + }); + + it('updates getOne query cache immediately and invalidates query when dataProvider promise resolves with meta at hook time', async () => { + const queryClient = new QueryClient(); + queryClient.setQueryData( + ['foo', 'getOne', { id: '1', meta: { key: 'value' } }], + { id: 1, bar: 'bar' } + ); + const dataProvider = { + update: jest.fn(() => + Promise.resolve({ data: { id: 1, bar: 'baz' } } as any) + ), + } as any; + const queryClientSpy = jest.spyOn( + queryClient, + 'invalidateQueries' + ); + let localUpdate; + const Dummy = () => { + const [update] = useUpdate( + 'foo', + { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + meta: { key: 'value' }, + }, + { + mutationMode: 'optimistic', + } + ); + localUpdate = update; + return ; + }; + render( + + + + ); + localUpdate(); + await waitFor(() => { + expect( + queryClient.getQueryData([ + 'foo', + 'getOne', + { id: '1', meta: { key: 'value' } }, + ]) + ).toEqual({ + id: 1, + bar: 'baz', + }); + }); + await waitFor(() => { + expect(dataProvider.update).toHaveBeenCalledWith('foo', { + id: 1, + data: { bar: 'baz' }, + previousData: { id: 1, bar: 'bar' }, + meta: { key: 'value' }, + }); + }); + await waitFor(() => { + expect(queryClientSpy).toHaveBeenCalledWith({ + queryKey: [ + 'foo', + 'getOne', + { id: '1', meta: { key: 'value' } }, + ], + }); + }); + }); + }); }); + describe('middlewares', () => { it('when pessimistic, it accepts middlewares and displays result and success side effects when dataProvider promise resolves', async () => { render(); diff --git a/packages/ra-core/src/dataProvider/useUpdate.ts b/packages/ra-core/src/dataProvider/useUpdate.ts index 41cab6c2ec2..9e3e53c21e8 100644 --- a/packages/ra-core/src/dataProvider/useUpdate.ts +++ b/packages/ra-core/src/dataProvider/useUpdate.ts @@ -269,12 +269,13 @@ export const useUpdate = ( const { resource: callTimeResource = resource, id: callTimeId = id, + meta: callTimeMeta = meta, } = variables; updateCache({ resource: callTimeResource, id: callTimeId, data, - meta: mutationOptions.meta ?? paramsRef.current.meta, + meta: callTimeMeta, }); if (