Skip to content

Commit ddb1709

Browse files
authored
Merge pull request #11024 from marmelab/fix-mutation-hooks-types
Fix mutation hooks types
2 parents 95c16df + abbedc7 commit ddb1709

File tree

11 files changed

+447
-100
lines changed

11 files changed

+447
-100
lines changed

packages/ra-core/src/controller/create/useCreateController.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { useCallback } from 'react';
2-
import { UseMutationOptions } from '@tanstack/react-query';
32

43
import { useAuthenticated, useRequireAccess } from '../../auth';
5-
import {
6-
HttpError,
7-
useCreate,
8-
UseCreateMutateParams,
9-
} from '../../dataProvider';
4+
import { HttpError, useCreate, UseCreateOptions } from '../../dataProvider';
105
import { useRedirect, RedirectionSideEffect } from '../../routing';
116
import { useNotify } from '../../notification';
127
import {
@@ -225,10 +220,10 @@ export interface CreateControllerProps<
225220
redirect?: RedirectionSideEffect;
226221
resource?: string;
227222
mutationMode?: MutationMode;
228-
mutationOptions?: UseMutationOptions<
229-
ResultRecordType,
223+
mutationOptions?: UseCreateOptions<
224+
RecordType,
230225
MutationOptionsError,
231-
UseCreateMutateParams<RecordType>
226+
ResultRecordType
232227
> & { meta?: any };
233228
transform?: TransformData;
234229
}

packages/ra-core/src/dataProvider/useCreate.spec.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,63 @@ describe('useCreate', () => {
8282
});
8383
});
8484

85+
it('uses a custom mutationFn with mutation middlewares', async () => {
86+
const dataProvider = testDataProvider({
87+
create: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any)),
88+
});
89+
const customMutationFn = jest.fn(async params => ({
90+
id: 1,
91+
title: params.data?.title,
92+
middlewareApplied: params.data?.middlewareApplied,
93+
}));
94+
let localCreate;
95+
let mutationData;
96+
const Dummy = () => {
97+
const [create, { data }] = useCreate(undefined, undefined, {
98+
mutationFn: customMutationFn,
99+
getMutateWithMiddlewares: mutate => async (resource, params) =>
100+
mutate(resource, {
101+
...params,
102+
data: {
103+
...params.data,
104+
middlewareApplied: true,
105+
},
106+
}),
107+
});
108+
localCreate = create;
109+
mutationData = data;
110+
return <span />;
111+
};
112+
113+
render(
114+
<CoreAdminContext dataProvider={dataProvider}>
115+
<Dummy />
116+
</CoreAdminContext>
117+
);
118+
119+
localCreate('foo', { data: { title: 'Hello' } });
120+
121+
await waitFor(() => {
122+
expect(customMutationFn).toHaveBeenCalledWith({
123+
resource: 'foo',
124+
data: {
125+
title: 'Hello',
126+
middlewareApplied: true,
127+
},
128+
});
129+
});
130+
131+
expect(dataProvider.create).not.toHaveBeenCalled();
132+
133+
await waitFor(() => {
134+
expect(mutationData).toEqual({
135+
id: 1,
136+
title: 'Hello',
137+
middlewareApplied: true,
138+
});
139+
});
140+
});
141+
85142
it('uses the latest declaration time mutationMode', async () => {
86143
jest.spyOn(console, 'error').mockImplementation(() => {});
87144
// This story uses the pessimistic mode by default

packages/ra-core/src/dataProvider/useCreate.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ export const useCreate = <
9393
const {
9494
mutationMode = 'pessimistic',
9595
getMutateWithMiddlewares,
96+
mutationFn: customMutationFn,
9697
onSettled,
9798
...mutationOptions
9899
} = options;
@@ -103,6 +104,26 @@ export const useCreate = <
103104
params as CreateParams<RecordType>
104105
)
105106
);
107+
const customMutationFnWithDataProviderResult = async (
108+
resource: string | undefined,
109+
params: Omit<UseCreateMutateParams<RecordType>, 'resource'>
110+
) => {
111+
if (resource == null) {
112+
throw new Error('useCreate mutation requires a resource');
113+
}
114+
if (params.data == null) {
115+
throw new Error(
116+
'useCreate mutation requires a non-empty data object'
117+
);
118+
}
119+
if (customMutationFn == null) {
120+
return dataProviderCreate(resource, params);
121+
}
122+
123+
return {
124+
data: await customMutationFn({ resource, ...params }),
125+
};
126+
};
106127

107128
const [mutate, mutationResult] = useMutationWithMutationMode<
108129
MutationError,
@@ -114,17 +135,8 @@ export const useCreate = <
114135
...mutationOptions,
115136
mutationKey: [resource, 'create', params],
116137
mutationMode,
117-
mutationFn: ({ resource, ...params }) => {
118-
if (resource == null) {
119-
throw new Error('useCreate mutation requires a resource');
120-
}
121-
if (params.data == null) {
122-
throw new Error(
123-
'useCreate mutation requires a non-empty data object'
124-
);
125-
}
126-
return dataProviderCreate(resource, params);
127-
},
138+
mutationFn: ({ resource, ...params }) =>
139+
customMutationFnWithDataProviderResult(resource, params),
128140
updateCache: (
129141
{ resource, ...params },
130142
{ mutationMode },
@@ -179,12 +191,21 @@ export const useCreate = <
179191

180192
return queryKeys;
181193
},
182-
getMutateWithMiddlewares: mutationFn => {
194+
getMutateWithMiddlewares: mutateWithMutationMode => {
183195
if (getMutateWithMiddlewares) {
184196
// Immediately get the function with middlewares applied so that even if the middlewares gets unregistered (because of a redirect for instance),
185197
// we still have them applied when users have called the mutate function.
186198
const mutateWithMiddlewares = getMutateWithMiddlewares(
187-
dataProviderCreate.bind(dataProvider)
199+
customMutationFn
200+
? (resource, params) =>
201+
customMutationFnWithDataProviderResult(
202+
resource,
203+
params as Omit<
204+
UseCreateMutateParams<RecordType>,
205+
'resource'
206+
>
207+
)
208+
: dataProviderCreate.bind(dataProvider)
188209
);
189210
return args => {
190211
// This is necessary to avoid breaking changes in useCreate:
@@ -194,7 +215,7 @@ export const useCreate = <
194215
};
195216
}
196217

197-
return args => mutationFn(args);
218+
return args => mutateWithMutationMode(args);
198219
},
199220
onUndo: ({ resource, data, meta }) => {
200221
queryClient.removeQueries({
@@ -268,6 +289,9 @@ export type UseCreateOptions<
268289
>,
269290
'mutationFn'
270291
> & {
292+
mutationFn?: (
293+
params: Partial<UseCreateMutateParams<RecordType>>
294+
) => Promise<ResultRecordType>;
271295
mutationMode?: MutationMode;
272296
returnPromise?: boolean;
273297
getMutateWithMiddlewares?: <

packages/ra-core/src/dataProvider/useDelete.spec.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,51 @@ describe('useDelete', () => {
7676
});
7777
});
7878

79+
it('uses a custom mutationFn when provided in options', async () => {
80+
const dataProvider = testDataProvider({
81+
delete: jest.fn(() => Promise.resolve({ data: { id: 1 } } as any)),
82+
});
83+
const customMutationFn = jest.fn(async params => ({
84+
id: params.id,
85+
source: 'custom',
86+
}));
87+
let localDeleteOne;
88+
let mutationData;
89+
const Dummy = () => {
90+
const [deleteOne, { data }] = useDelete(undefined, undefined, {
91+
mutationFn: customMutationFn,
92+
});
93+
localDeleteOne = deleteOne;
94+
mutationData = data;
95+
return <span />;
96+
};
97+
98+
render(
99+
<CoreAdminContext dataProvider={dataProvider}>
100+
<Dummy />
101+
</CoreAdminContext>
102+
);
103+
104+
localDeleteOne('foo', { id: 1, previousData: { id: 1, bar: 'bar' } });
105+
106+
await waitFor(() => {
107+
expect(customMutationFn).toHaveBeenCalledWith({
108+
resource: 'foo',
109+
id: 1,
110+
previousData: { id: 1, bar: 'bar' },
111+
});
112+
});
113+
114+
expect(dataProvider.delete).not.toHaveBeenCalled();
115+
116+
await waitFor(() => {
117+
expect(mutationData).toEqual({
118+
id: 1,
119+
source: 'custom',
120+
});
121+
});
122+
});
123+
79124
it('uses the latest declaration time mutationMode', async () => {
80125
const posts = [
81126
{ id: 1, title: 'Hello' },

packages/ra-core/src/dataProvider/useDelete.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,31 @@ export const useDelete = <
9292
const queryClient = useQueryClient();
9393
const {
9494
mutationMode = 'pessimistic',
95+
mutationFn: customMutationFn,
9596
onSettled,
9697
...mutationOptions
9798
} = options;
99+
const customMutationFnWithDataProviderResult = async (
100+
resource: string | undefined,
101+
params: Omit<UseDeleteMutateParams<RecordType>, 'resource'>
102+
) => {
103+
if (resource == null) {
104+
throw new Error('useDelete mutation requires a resource');
105+
}
106+
if (params.id == null) {
107+
throw new Error('useDelete mutation requires a non-empty id');
108+
}
109+
if (customMutationFn == null) {
110+
return dataProvider.delete<RecordType>(
111+
resource,
112+
params as DeleteParams<RecordType>
113+
);
114+
}
115+
116+
return {
117+
data: await customMutationFn({ resource, ...params }),
118+
};
119+
};
98120

99121
const [mutate, mutationResult] = useMutationWithMutationMode<
100122
MutationError,
@@ -106,20 +128,8 @@ export const useDelete = <
106128
...mutationOptions,
107129
mutationKey: [resource, 'delete', params],
108130
mutationMode,
109-
mutationFn: ({ resource, ...params }) => {
110-
if (resource == null) {
111-
throw new Error('useDelete mutation requires a resource');
112-
}
113-
if (params.id == null) {
114-
throw new Error(
115-
'useDelete mutation requires a non-empty id'
116-
);
117-
}
118-
return dataProvider.delete<RecordType>(
119-
resource,
120-
params as DeleteParams<RecordType>
121-
);
122-
},
131+
mutationFn: ({ resource, ...params }) =>
132+
customMutationFnWithDataProviderResult(resource, params),
123133
updateCache: ({ resource, ...params }, { mutationMode }) => {
124134
// hack: only way to tell react-query not to fetch this query for the next 5 seconds
125135
// because setQueryData doesn't accept a stale time option
@@ -285,6 +295,9 @@ export type UseDeleteOptions<
285295
>,
286296
'mutationFn'
287297
> & {
298+
mutationFn?: (
299+
params: Partial<UseDeleteMutateParams<RecordType>>
300+
) => Promise<RecordType>;
288301
mutationMode?: MutationMode;
289302
returnPromise?: boolean;
290303
};

packages/ra-core/src/dataProvider/useDeleteMany.spec.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,44 @@ describe('useDeleteMany', () => {
5757
});
5858
});
5959

60+
it('uses a custom mutationFn when provided in options', async () => {
61+
const dataProvider = testDataProvider({
62+
deleteMany: jest.fn(() => Promise.resolve({ data: [1, 2] } as any)),
63+
});
64+
const customMutationFn = jest.fn(async params => params.ids);
65+
let localDeleteMany;
66+
let mutationData;
67+
const Dummy = () => {
68+
const [deleteMany, { data }] = useDeleteMany(undefined, undefined, {
69+
mutationFn: customMutationFn,
70+
});
71+
localDeleteMany = deleteMany;
72+
mutationData = data;
73+
return <span />;
74+
};
75+
76+
render(
77+
<CoreAdminContext dataProvider={dataProvider}>
78+
<Dummy />
79+
</CoreAdminContext>
80+
);
81+
82+
localDeleteMany('foo', { ids: [1, 2] });
83+
84+
await waitFor(() => {
85+
expect(customMutationFn).toHaveBeenCalledWith({
86+
resource: 'foo',
87+
ids: [1, 2],
88+
});
89+
});
90+
91+
expect(dataProvider.deleteMany).not.toHaveBeenCalled();
92+
93+
await waitFor(() => {
94+
expect(mutationData).toEqual([1, 2]);
95+
});
96+
});
97+
6098
it('uses the latest declaration time mutationMode', async () => {
6199
// This story uses the pessimistic mode by default
62100
render(<MutationMode />);

0 commit comments

Comments
 (0)