import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { api, fetcher as defaultFetcher } from "./api.js"; function getBaseKey(key) { return Array.isArray(key) ? key : [key]; } function getListKey(key) { return [...getBaseKey(key), "list"]; } function getItemKey(key, id) { return [...getBaseKey(key), id]; } function buildUrl(url, params) { if (!params) return url; const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== "") { searchParams.append(key, value); } }); const query = searchParams.toString(); return query ? `${url}?${query}` : url; } function resolveUrl(url, data) { if (typeof url === "function") { return url(data?.id, data); } return url; } function omitId(data = {}) { const { id, ...rest } = data; return rest; } function joinUrl(baseUrl, id) { return `${baseUrl.replace(/\/$/, "")}/${id}`; } function toHookName(actionName) { return `use${actionName.charAt(0).toUpperCase()}${actionName.slice(1)}`; } export function createApi({ key, url, actions = {}, axiosInstance = api, fetcher = defaultFetcher, }) { const baseKey = getBaseKey(key); const listKey = getListKey(key); function useGet(params, options = {}) { return useQuery({ queryKey: params ? [...listKey, params] : listKey, queryFn: () => fetcher(buildUrl(url, params)), ...options, }); } function useItem(id, options = {}) { return useQuery({ queryKey: getItemKey(key, id), queryFn: () => fetcher(joinUrl(url, id)), enabled: Boolean(id), ...options, }); } function useAction(actionName, options = {}) { const queryClient = useQueryClient(); const action = actions[actionName]; if (!action) { throw new Error(`Action "${actionName}" is not defined.`); } return useMutation({ mutationKey: [...baseKey, actionName], mutationFn: async (data = {}) => { const method = action.method || "post"; const requestUrl = resolveUrl(action.url || url, data); const payload = action.body ? action.body(data) : action.sendRawPayload ? data : omitId(data); const response = method === "delete" ? await axiosInstance.delete( requestUrl, payload && Object.keys(payload).length > 0 ? { data: payload } : undefined, ) : await axiosInstance[method](requestUrl, payload); return response.data; }, onSuccess: (...args) => { const [responseData, variables] = args; queryClient.invalidateQueries({ queryKey: listKey, }); const id = variables?.id ?? responseData?.id; if (id) { queryClient.invalidateQueries({ queryKey: getItemKey(key, id), }); } const extraInvalidateKeys = typeof action.invalidateKeys === "function" ? action.invalidateKeys(responseData, variables) : action.invalidateKeys; extraInvalidateKeys?.forEach((queryKey) => { queryClient.invalidateQueries({ queryKey }); }); options.onSuccess?.(...args); }, ...options, }); } const actionHooks = Object.keys(actions).reduce((acc, actionName) => { acc[toHookName(actionName)] = (options = {}) => useAction(actionName, options); return acc; }, {}); return { key: baseKey, listKey, useGet, useItem, useAction, ...actionHooks, }; }