import { useMutation, useQueryClient, QueryKey, UseMutationOptions } from '@tanstack/react-query'; import apiClient from '../api/client'; import { AxiosError, AxiosResponse } from 'axios'; type HttpMethod = 'POST' | 'PUT' | 'PATCH' | 'DELETE'; interface CrudMutationConfig { /** The API endpoint (e.g., '/resources') */ endpoint: string; /** HTTP method */ method: HttpMethod; /** Query keys to invalidate on success */ invalidateKeys?: QueryKey[]; /** Transform response data */ transformResponse?: (response: AxiosResponse) => TData; /** React Query mutation options */ options?: Omit< UseMutationOptions, 'mutationFn' >; } /** * Generic CRUD mutation hook factory * * @example * // Create a resource * const useCreateResource = () => useCrudMutation({ * endpoint: '/resources', * method: 'POST', * invalidateKeys: [['resources']], * }); * * @example * // Update a resource * const useUpdateResource = () => useCrudMutation }>({ * endpoint: '/resources', * method: 'PATCH', * invalidateKeys: [['resources']], * }); */ export function useCrudMutation({ endpoint, method, invalidateKeys = [], transformResponse, options = {}, }: CrudMutationConfig) { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (variables: TVariables) => { let response: AxiosResponse; // Handle different variable shapes for different methods if (method === 'DELETE') { // For DELETE, variables is typically just the ID const id = typeof variables === 'object' && variables !== null && 'id' in variables ? (variables as { id: string | number }).id : variables; response = await apiClient.delete(`${endpoint}/${id}/`); } else if (method === 'PUT' || method === 'PATCH') { // For PUT/PATCH, variables should have id and data if (typeof variables === 'object' && variables !== null && 'id' in variables) { const { id, ...data } = variables as { id: string | number; [key: string]: unknown }; response = await apiClient[method.toLowerCase() as 'put' | 'patch'](`${endpoint}/${id}/`, data); } else { // If no id, just send to the endpoint response = await apiClient[method.toLowerCase() as 'put' | 'patch'](`${endpoint}/`, variables); } } else { // POST - create new response = await apiClient.post(`${endpoint}/`, variables); } return transformResponse ? transformResponse(response) : (response.data as unknown as TData); }, onSuccess: (data, variables, context) => { // Invalidate specified query keys invalidateKeys.forEach((key) => { queryClient.invalidateQueries({ queryKey: key }); }); // Call custom onSuccess if provided options.onSuccess?.(data, variables, context); }, ...options, }); } /** * Create hook factory for a resource */ export function createCrudHooks< TResource, TCreateData = Partial, TUpdateData = Partial >(endpoint: string, queryKey: string) { return { useCreate: (options?: UseMutationOptions) => useCrudMutation({ endpoint, method: 'POST', invalidateKeys: [[queryKey]], options, }), useUpdate: (options?: UseMutationOptions) => useCrudMutation({ endpoint, method: 'PATCH', invalidateKeys: [[queryKey]], options, }), useDelete: (options?: UseMutationOptions) => useCrudMutation({ endpoint, method: 'DELETE', invalidateKeys: [[queryKey]], options: options as UseMutationOptions, }), }; } export default useCrudMutation;