import { useMutation, useQueryClient, type UseMutationOptions, } from "@tanstack/react-query"; import apiFetch from "../../utils/api"; export interface ApiError extends Error { status?: number; } export type HttpMethod = "POST" | "PUT" | "PATCH" | "DELETE"; interface ApiResponseBody { success: boolean; data?: T; error?: string; message?: string; } async function performMutation(opts: { url: string | ((input: TIn) => string); method: HttpMethod | ((input: TIn) => HttpMethod); body?: TIn; }): Promise { const url = typeof opts.url === "function" ? opts.url(opts.body as TIn) : opts.url; const method = typeof opts.method === "function" ? opts.method(opts.body as TIn) : opts.method; const response = await apiFetch(url, { method, headers: opts.body !== undefined ? { "Content-Type": "application/json" } : undefined, body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined, }); if (response.status === 401) { const err: ApiError = new Error("Unauthorized"); err.status = 401; throw err; } let result: ApiResponseBody; try { result = (await response.json()) as ApiResponseBody; } catch { throw new Error("Invalid JSON response"); } if (!response.ok || !result.success) { const err: ApiError = new Error( result.error || `Request failed (${response.status})`, ); err.status = response.status; throw err; } return result.data as TOut; } export interface ApiMutationOptions { url: string | ((input: TIn) => string); method: HttpMethod | ((input: TIn) => HttpMethod); /** Query-key prefixes to invalidate on success. Broad per CLAUDE.md. */ invalidate?: readonly string[]; } /** * Hook that wraps `useMutation` with `apiFetch` and broad-invalidation. * * Usage: * const createCategory = useApiMutation({ * url: "/api/admin/warehouse/categories", * method: "POST", * invalidate: ["warehouse"], * }); * createCategory.mutate(form, { * onSuccess: () => { ... }, * }); * * The returned `data` from a successful mutation is `TOut`. Errors are thrown * as `ApiError` (with `.status` attached) so callers can branch on HTTP code. */ export function useApiMutation( opts: ApiMutationOptions & Omit, "mutationFn">, ) { const { url, method, invalidate, onSuccess, ...rest } = opts; const queryClient = useQueryClient(); return useMutation({ mutationFn: (input: TIn) => performMutation({ url, method, body: input }), ...rest, onSuccess: (data, variables, onMutateResult, context) => { if (invalidate) { for (const key of invalidate) { queryClient.invalidateQueries({ queryKey: [key] }); } } // Forward to user-provided onSuccess with the 4-arg signature expected by TanStack. ( onSuccess as | ((d: TOut, v: TIn, r: unknown, c: unknown) => void) | undefined )?.(data, variables, onMutateResult, context); }, }); }