Skip to content

Commit 5738e6a

Browse files
committed
Fix mutationFn overrides in mutation hooks
1 parent 3448aad commit 5738e6a

7 files changed

Lines changed: 279 additions & 92 deletions

File tree

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

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,14 @@ export const useCreate = <
9393
const {
9494
mutationMode = 'pessimistic',
9595
getMutateWithMiddlewares,
96+
mutationFn: customMutationFn,
9697
...mutationOptions
9798
} = options;
99+
const wrappedCustomMutationFn = customMutationFn as
100+
| ((
101+
params: Partial<UseCreateMutateParams<RecordType>>
102+
) => Promise<ResultRecordType>)
103+
| undefined;
98104

99105
const dataProviderCreate = useEvent((resource: string, params) =>
100106
dataProvider.create<RecordType, ResultRecordType>(
@@ -113,17 +119,23 @@ export const useCreate = <
113119
...mutationOptions,
114120
mutationKey: [resource, 'create', params],
115121
mutationMode,
116-
mutationFn: ({ resource, ...params }) => {
117-
if (resource == null) {
118-
throw new Error('useCreate mutation requires a resource');
119-
}
120-
if (params.data == null) {
121-
throw new Error(
122-
'useCreate mutation requires a non-empty data object'
123-
);
124-
}
125-
return dataProviderCreate(resource, params);
126-
},
122+
mutationFn: wrappedCustomMutationFn
123+
? async params => ({
124+
data: await wrappedCustomMutationFn(params),
125+
})
126+
: ({ resource, ...params }) => {
127+
if (resource == null) {
128+
throw new Error(
129+
'useCreate mutation requires a resource'
130+
);
131+
}
132+
if (params.data == null) {
133+
throw new Error(
134+
'useCreate mutation requires a non-empty data object'
135+
);
136+
}
137+
return dataProviderCreate(resource, params);
138+
},
127139
updateCache: (
128140
{ resource, ...params },
129141
{ mutationMode },
@@ -178,12 +190,20 @@ export const useCreate = <
178190

179191
return queryKeys;
180192
},
181-
getMutateWithMiddlewares: mutationFn => {
193+
getMutateWithMiddlewares: mutateWithMutationMode => {
182194
if (getMutateWithMiddlewares) {
183195
// Immediately get the function with middlewares applied so that even if the middlewares gets unregistered (because of a redirect for instance),
184196
// we still have them applied when users have called the mutate function.
185197
const mutateWithMiddlewares = getMutateWithMiddlewares(
186-
dataProviderCreate.bind(dataProvider)
198+
wrappedCustomMutationFn
199+
? (resource, params) =>
200+
wrappedCustomMutationFn({
201+
resource,
202+
...params,
203+
} as Partial<
204+
UseCreateMutateParams<RecordType>
205+
>)
206+
: dataProviderCreate.bind(dataProvider)
187207
);
188208
return args => {
189209
// This is necessary to avoid breaking changes in useCreate:
@@ -193,7 +213,7 @@ export const useCreate = <
193213
};
194214
}
195215

196-
return args => mutationFn(args);
216+
return args => mutateWithMutationMode(args);
197217
},
198218
onUndo: ({ resource, data, meta }) => {
199219
queryClient.removeQueries({

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
// This story uses the pessimistic mode by default
81126
render(<MutationMode />);

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

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,16 @@ export const useDelete = <
9090
): UseDeleteResult<RecordType, MutationError> => {
9191
const dataProvider = useDataProvider();
9292
const queryClient = useQueryClient();
93-
const { mutationMode = 'pessimistic', ...mutationOptions } = options;
93+
const {
94+
mutationMode = 'pessimistic',
95+
mutationFn: customMutationFn,
96+
...mutationOptions
97+
} = options;
98+
const wrappedCustomMutationFn = customMutationFn as
99+
| ((
100+
params: Partial<UseDeleteMutateParams<RecordType>>
101+
) => Promise<RecordType>)
102+
| undefined;
94103

95104
const [mutate, mutationResult] = useMutationWithMutationMode<
96105
MutationError,
@@ -102,20 +111,26 @@ export const useDelete = <
102111
...mutationOptions,
103112
mutationKey: [resource, 'delete', params],
104113
mutationMode,
105-
mutationFn: ({ resource, ...params }) => {
106-
if (resource == null) {
107-
throw new Error('useDelete mutation requires a resource');
108-
}
109-
if (params.id == null) {
110-
throw new Error(
111-
'useDelete mutation requires a non-empty id'
112-
);
113-
}
114-
return dataProvider.delete<RecordType>(
115-
resource,
116-
params as DeleteParams<RecordType>
117-
);
118-
},
114+
mutationFn: wrappedCustomMutationFn
115+
? async params => ({
116+
data: await wrappedCustomMutationFn(params),
117+
})
118+
: ({ resource, ...params }) => {
119+
if (resource == null) {
120+
throw new Error(
121+
'useDelete mutation requires a resource'
122+
);
123+
}
124+
if (params.id == null) {
125+
throw new Error(
126+
'useDelete mutation requires a non-empty id'
127+
);
128+
}
129+
return dataProvider.delete<RecordType>(
130+
resource,
131+
params as DeleteParams<RecordType>
132+
);
133+
},
119134
updateCache: ({ resource, ...params }, { mutationMode }) => {
120135
// hack: only way to tell react-query not to fetch this query for the next 5 seconds
121136
// because setQueryData doesn't accept a stale time option

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

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,16 @@ export const useDeleteMany = <
9090
): UseDeleteManyResult<RecordType, MutationError> => {
9191
const dataProvider = useDataProvider();
9292
const queryClient = useQueryClient();
93-
const { mutationMode = 'pessimistic', ...mutationOptions } = options;
93+
const {
94+
mutationMode = 'pessimistic',
95+
mutationFn: customMutationFn,
96+
...mutationOptions
97+
} = options;
98+
const wrappedCustomMutationFn = customMutationFn as
99+
| ((
100+
params: Partial<UseDeleteManyMutateParams<RecordType>>
101+
) => Promise<Array<RecordType['id']> | undefined>)
102+
| undefined;
94103

95104
const [mutate, mutationResult] = useMutationWithMutationMode<
96105
MutationError,
@@ -102,22 +111,26 @@ export const useDeleteMany = <
102111
...mutationOptions,
103112
mutationKey: [resource, 'deleteMany', params],
104113
mutationMode,
105-
mutationFn: ({ resource, ...params }) => {
106-
if (resource == null) {
107-
throw new Error(
108-
'useDeleteMany mutation requires a resource'
109-
);
110-
}
111-
if (params.ids == null) {
112-
throw new Error(
113-
'useDeleteMany mutation requires an array of ids'
114-
);
115-
}
116-
return dataProvider.deleteMany<RecordType>(
117-
resource,
118-
params as DeleteManyParams<RecordType>
119-
);
120-
},
114+
mutationFn: wrappedCustomMutationFn
115+
? async params => ({
116+
data: await wrappedCustomMutationFn(params),
117+
})
118+
: ({ resource, ...params }) => {
119+
if (resource == null) {
120+
throw new Error(
121+
'useDeleteMany mutation requires a resource'
122+
);
123+
}
124+
if (params.ids == null) {
125+
throw new Error(
126+
'useDeleteMany mutation requires an array of ids'
127+
);
128+
}
129+
return dataProvider.deleteMany<RecordType>(
130+
resource,
131+
params as DeleteManyParams<RecordType>
132+
);
133+
},
121134
updateCache: ({ resource, ...params }, { mutationMode }) => {
122135
// hack: only way to tell react-query not to fetch this query for the next 5 seconds
123136
// because setQueryData doesn't accept a stale time option

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,62 @@ describe('useUpdate', () => {
104104
});
105105
});
106106

107+
it('uses a custom mutationFn with mutation middlewares', async () => {
108+
const dataProvider = {
109+
update: jest.fn(() =>
110+
Promise.resolve({ data: { id: 1 } } as any)
111+
),
112+
} as any;
113+
const customMutationFn = jest.fn(async params => ({
114+
id: params.id,
115+
title: params.data?.title,
116+
middlewareApplied: params.data?.middlewareApplied,
117+
}));
118+
let localUpdate;
119+
const Dummy = () => {
120+
const [update] = useUpdate(undefined, undefined, {
121+
mutationFn: customMutationFn,
122+
getMutateWithMiddlewares:
123+
mutate => async (resource, params) =>
124+
mutate(resource, {
125+
...params,
126+
data: {
127+
...params.data,
128+
middlewareApplied: true,
129+
},
130+
}),
131+
});
132+
localUpdate = update;
133+
return <span />;
134+
};
135+
136+
render(
137+
<CoreAdminContext dataProvider={dataProvider}>
138+
<Dummy />
139+
</CoreAdminContext>
140+
);
141+
142+
localUpdate('foo', {
143+
id: 1,
144+
data: { title: 'Hello' },
145+
previousData: { id: 1, title: 'World' },
146+
});
147+
148+
await waitFor(() => {
149+
expect(customMutationFn).toHaveBeenCalledWith({
150+
resource: 'foo',
151+
id: 1,
152+
data: {
153+
title: 'Hello',
154+
middlewareApplied: true,
155+
},
156+
previousData: { id: 1, title: 'World' },
157+
});
158+
});
159+
160+
expect(dataProvider.update).not.toHaveBeenCalled();
161+
});
162+
107163
it('uses the latest declaration time mutationMode', async () => {
108164
// This story uses the pessimistic mode by default
109165
render(<MutationMode timeout={10} />);

0 commit comments

Comments
 (0)