import { useQuery, useMutation, useQueryClient, type UseQueryOptions, type UseMutationOptions, type QueryKey, } from '@tanstack/react-query' /** * API interface for CRUD operations * Implement this interface for your entity's API client */ export interface CrudApi, TUpdateDto = Partial> { getAll: () => Promise; getById: (id: string) => Promise; create: (data: TCreateDto) => Promise; update: (id: string, data: TUpdateDto) => Promise; delete: (id: string) => Promise; } /** * Options for CRUD hook generation */ export interface CreateCrudHooksOptions, TUpdateDto = Partial> { /** * Base query key for this entity * @example ['users'], ['products'], ['orders'] */ queryKey: QueryKey; /** * API client implementing CRUD operations */ api: CrudApi; /** * Enable optimistic updates (default: false) * When enabled, mutations will update the cache immediately */ enableOptimistic?: boolean; /** * Custom query options for getAll */ getAllOptions?: Omit, 'queryKey' | 'queryFn'>; /** * Custom query options for getById */ getByIdOptions?: Omit, 'queryKey' | 'queryFn'>; } /** * Generated CRUD hooks */ export interface CrudHooks, TUpdateDto = Partial> { /** * Hook to fetch all entities * @example * ```typescript * const { data: users, isLoading } = useGetAll(); * ``` */ useGetAll: (options?: Omit, 'queryKey' | 'queryFn'>) => ReturnType>; /** * Hook to fetch a single entity by ID * @example * ```typescript * const { data: user } = useGetById('123'); * ``` */ useGetById: (id: string, options?: Omit, 'queryKey' | 'queryFn'>) => ReturnType>; /** * Hook to create a new entity * @example * ```typescript * const { mutate: createUser } = useCreate(); * createUser({ name: 'John', email: 'john@example.com' }); * ``` */ useCreate: (options?: UseMutationOptions) => ReturnType>; /** * Hook to update an existing entity * @example * ```typescript * const { mutate: updateUser } = useUpdate(); * updateUser({ id: '123', data: { name: 'Jane' } }); * ``` */ useUpdate: (options?: UseMutationOptions) => ReturnType>; /** * Hook to delete an entity * @example * ```typescript * const { mutate: deleteUser } = useDelete(); * deleteUser('123'); * ``` */ useDelete: (options?: UseMutationOptions) => ReturnType>; } /** * Create a set of CRUD hooks for an entity * * Generates standard useQuery and useMutation hooks with automatic * cache invalidation and optional optimistic updates. * * @example * ```typescript * // Define your API * const userApi: CrudApi = { * getAll: () => apiClient.get('/users').then(r => r.data), * getById: (id) => apiClient.get(`/users/${id}`).then(r => r.data), * create: (data) => apiClient.post('/users', data).then(r => r.data), * update: (id, data) => apiClient.patch(`/users/${id}`, data).then(r => r.data), * delete: (id) => apiClient.delete(`/users/${id}`).then(r => r.data), * }; * * // Generate hooks * const { * useGetAll, * useGetById, * useCreate, * useUpdate, * useDelete, * } = createCrudHooks({ * queryKey: ['users'], * api: userApi, * enableOptimistic: true, * }); * * // Use in components * function UserList() { * const { data: users, isLoading } = useGetAll(); * const { mutate: createUser } = useCreate(); * const { mutate: deleteUser } = useDelete(); * * return ( * // ... UI code * ); * } * ``` */ export function createCrudHooks, TUpdateDto = Partial>( options: CreateCrudHooksOptions ): CrudHooks { const { queryKey, api, enableOptimistic = false, getAllOptions, getByIdOptions } = options // Hook: Get all entities function useGetAll(customOptions?: Omit, 'queryKey' | 'queryFn'>) { return useQuery({ queryKey, queryFn: api.getAll, ...getAllOptions, ...customOptions, }) } // Hook: Get entity by ID function useGetById(id: string, customOptions?: Omit, 'queryKey' | 'queryFn'>) { return useQuery({ queryKey: [...queryKey, id], queryFn: () => api.getById(id), enabled: !!id, ...getByIdOptions, ...customOptions, }) } // Hook: Create entity function useCreate(customOptions?: UseMutationOptions) { const queryClient = useQueryClient() return useMutation({ mutationFn: api.create, onSuccess: (...args) => { // Invalidate list query to refetch queryClient.invalidateQueries({ queryKey }) // Call custom onSuccess if provided if (customOptions?.onSuccess) { customOptions.onSuccess(...args) } }, ...customOptions, }) } // Hook: Update entity function useUpdate(customOptions?: UseMutationOptions) { const queryClient = useQueryClient() return useMutation({ mutationFn: ({ id, data }) => api.update(id, data), onMutate: enableOptimistic ? async ({ id, data }) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: [...queryKey, id] }) // Snapshot previous value const previous = queryClient.getQueryData([...queryKey, id]) // Optimistically update if (previous) { queryClient.setQueryData([...queryKey, id], { ...previous, ...data, } as TEntity) } return { previous } } : customOptions?.onMutate, onError: enableOptimistic ? (...args) => { // Rollback on error const [, variables] = args const context = { previous: queryClient.getQueryData([...queryKey, variables.id]) } if (context?.previous) { queryClient.setQueryData([...queryKey, variables.id], context.previous) } if (customOptions?.onError) { customOptions.onError(...args) } } : customOptions?.onError, onSuccess: (...args) => { // Invalidate queries const [, variables] = args queryClient.invalidateQueries({ queryKey }) queryClient.invalidateQueries({ queryKey: [...queryKey, variables.id] }) if (customOptions?.onSuccess) { customOptions.onSuccess(...args) } }, ...customOptions, }) } // Hook: Delete entity function useDelete(customOptions?: UseMutationOptions) { const queryClient = useQueryClient() return useMutation({ mutationFn: api.delete, onMutate: enableOptimistic ? async (id) => { // Cancel queries await queryClient.cancelQueries({ queryKey }) // Snapshot previous value const previous = queryClient.getQueryData(queryKey) // Optimistically remove from list if (previous) { queryClient.setQueryData( queryKey, previous.filter((item) => (item as { id: string }).id !== id) ) } return { previous } } : customOptions?.onMutate, onError: enableOptimistic ? (...args) => { // Rollback on error const context = { previous: queryClient.getQueryData(queryKey) } if (context?.previous) { queryClient.setQueryData(queryKey, context.previous) } if (customOptions?.onError) { customOptions.onError(...args) } } : customOptions?.onError, onSuccess: (...args) => { // Invalidate queries const [, id] = args queryClient.invalidateQueries({ queryKey }) queryClient.invalidateQueries({ queryKey: [...queryKey, id] }) if (customOptions?.onSuccess) { customOptions.onSuccess(...args) } }, ...customOptions, }) } return { useGetAll, useGetById, useCreate, useUpdate, useDelete, } }