import { type UseMutationOptions, type UseMutationResult, useMutation, useQueryClient } from '@tanstack/react-query'; import { equals, includes, omit, type } from 'ramda'; import { useEffect } from 'react'; import type { JsonDecoder } from 'ts.data.json'; import useSnackbar from '../../Snackbar/useSnackbar'; import { useDeepCompare } from '../../utils'; import { type CatchErrorProps, customFetch, type ResponseError } from '../customFetch'; import { errorLog } from '../logger'; import { type OptimisticListing, useOptimisticMutation } from './useOptimisticMutation'; export enum Method { DELETE = 'DELETE', GET = 'GET', PATCH = 'PATCH', POST = 'POST', PUT = 'PUT' } interface Variables { _meta?: TMeta; payload?: T; } export type UseMutationQueryProps = { baseEndpoint?: string; catchError?: (props: CatchErrorProps) => void; decoder?: JsonDecoder.Decoder; defaultFailureMessage?: string; fetchHeaders?: HeadersInit; getEndpoint: (_meta: TMeta) => string; httpCodesBypassErrorSnackbar?: Array; method: Method; onError?: ( error: ResponseError, variables: Variables, context: unknown ) => unknown; onMutate?: (variables: Variables) => Promise | unknown; onSuccess?: ( data: ResponseError | T, variables: Variables, context: unknown ) => unknown; optimisticListing?: OptimisticListing; } & Omit< UseMutationOptions<{ _meta?: TMeta; payload: T }>, 'mutationFn' | 'onError' | 'onMutate' | 'onSuccess' | 'mutateAsync' | 'mutate' >; export type UseMutationQueryState = Omit< UseMutationResult, 'isError' | 'mutate' | 'mutateAsync' > & { isError: boolean; isMutating: boolean; mutate: (variables: Variables) => ResponseError | T; mutateAsync: ( variables: Variables, rest?: Pick< UseMutationQueryProps, 'onError' | 'onMutate' | 'onSettled' | 'onSuccess' > ) => Promise; }; const useMutationQuery = ({ getEndpoint, catchError, decoder, defaultFailureMessage, fetchHeaders, httpCodesBypassErrorSnackbar = [], method, onMutate, onError, onSuccess, onSettled, baseEndpoint, optimisticListing }: UseMutationQueryProps): UseMutationQueryState => { const { showErrorMessage } = useSnackbar(); const queryClient = useQueryClient(); const { getListingQueryKey, getOptimisticMutationItems, getPreviousListing } = useOptimisticMutation({ optimisticListing }); const queryData = useMutation< T | ResponseError, ResponseError, Variables >({ mutationFn: ( variables: Variables ): Promise => { const { _meta, payload } = variables || {}; return customFetch({ baseEndpoint, catchError, decoder, defaultFailureMessage, endpoint: getEndpoint(_meta as TMeta), headers: new Headers({ 'Content-Type': equals(method, Method.PATCH) ? 'application/merge-patch+json' : 'application/json', ...fetchHeaders }), isMutation: true, method, payload }); }, onError: (error, variables, context) => { if (optimisticListing?.enabled) { const listingQueryKey = getListingQueryKey(); queryClient.setQueriesData( { queryKey: listingQueryKey }, (context as { previousListing: unknown })?.previousListing ); } onError?.(error, variables, context); }, onMutate: optimisticListing?.enabled ? ({ payload, _meta }) => { const listingQueryKey = getListingQueryKey(); const newListing = getOptimisticMutationItems({ _meta, method, payload }); const previousListing = getPreviousListing(); queryClient.setQueriesData({ queryKey: listingQueryKey }, newListing); return { previousListing }; } : onMutate, onSettled: onSettled as UseMutationOptions< T | ResponseError, ResponseError, Variables >['onSettled'], onSuccess: (data, variables, context) => { if (optimisticListing?.enabled) { const isQueryKeyArray = equals( type(optimisticListing.queryKey), 'Array' ); const listingQueryKey = isQueryKeyArray ? optimisticListing?.queryKey : [optimisticListing?.queryKey]; queryClient.invalidateQueries({ queryKey: listingQueryKey as Array }); } if ((data as ResponseError)?.isError) { if (optimisticListing?.enabled) { const listingQueryKey = getListingQueryKey(); queryClient.setQueriesData( { queryKey: listingQueryKey }, (context as { previousListing: unknown })?.previousListing ); } onError?.(data as ResponseError, variables, context); return; } onSuccess?.(data, variables, context); } }); const manageError = (): void => { const data = queryData.data as ResponseError | undefined; if (data?.isError) { errorLog(data.message); const hasACorrespondingHttpCode = includes( data?.statusCode || 0, httpCodesBypassErrorSnackbar ); if (!hasACorrespondingHttpCode) { showErrorMessage(data?.message); } } }; useEffect( () => { manageError(); }, useDeepCompare([queryData.data]) ); return { ...omit(['isError'], queryData), isError: (queryData.data as ResponseError | undefined)?.isError || false, isMutating: queryData.isPending } as unknown as UseMutationQueryState; }; export default useMutationQuery;