UNPKG

react-query-manager

Version:

This is a library to simplify the work with @tanstack/react-query. It offers unified style for keys in the cache, ability to cancel a request. automatic cache refresh after mutation.

1 lines 128 kB
{"version":3,"sources":["../src/utils/custom-error.ts","../src/utils/fetcher.ts","../src/index.ts","../src/hooks/use-get-list.ts","../src/internal/utils/remove-first-and-last-slash.ts","../src/utils/get-url-from-resource.ts","../src/components/RQWrapper.tsx","../src/internal/components/Toaster.tsx","../src/utils/toast.ts","../src/internal/utils/undo-event-emitter.ts","../src/internal/env.ts","../src/internal/query-client.ts","../src/hooks/use-get-infinite-list.ts","../src/hooks/use-get-one.ts","../src/hooks/use-delete.ts","../src/utils/queries/add-item-to-query-cache.ts","../src/utils/queries/add-items-to-list-query-cache.ts","../src/utils/queries/remove-queries.ts","../src/utils/queries/delete-items-from-query-cache.ts","../src/utils/queries/helpers-query-keys.ts","../src/internal/utils/is-equal.ts","../src/utils/queries/invalidate-matching-queries.ts","../src/utils/queries/remove-matching-queries.ts","../src/utils/queries/invalidate-queries.ts","../src/internal/utils/merge-objects.ts","../src/utils/queries/update-items-from-query-cache.ts","../src/internal/utils/create-snapshot.ts","../src/hooks/use-update.ts","../src/hooks/use-create.ts","../src/hooks/use-data-query.ts","../src/hooks/use-data-mutate.ts"],"sourcesContent":["/**\n * Custom error class for handling HTTP request errors.\n *\n * @class\n * @extends {Error}\n * @param message - The error message.\n * @param status - The HTTP status code associated with the error.\n * @param data - Additional data related to the error.\n *\n * @example\n * try {\n * // Some code that may throw an error\n * } catch (error) {\n * throw new CustomError('Failed to fetch resource', 500, error);\n * }\n */\nexport class CustomError extends Error {\n constructor(\n public readonly message: string,\n public readonly status?: number,\n public readonly data?: any,\n ) {\n super(message);\n\n Object.setPrototypeOf(this, CustomError.prototype);\n\n this.name = this.constructor.name;\n\n if (typeof (Error as any).captureStackTrace === 'function') {\n (Error as any).captureStackTrace(this, this.constructor);\n } else {\n this.stack = new Error(message).stack;\n }\n\n this.stack = new Error().stack;\n\n console.error(this.message, this);\n }\n}\n","import { ApiClient } from '../type';\nimport { CustomError } from './custom-error';\n\n/**\n * Filters out null, undefined, and empty string values from the provided parameters object,\n * while keeping boolean and numeric values intact. The function returns a new object containing\n * only the non-empty parameters.\n *\n * @param {Record<string, any>} params - The object containing the parameters to be filtered.\n * @returns {Record<string, unknown>} A new object with only the non-empty parameters.\n */\nexport const filterEmptyParams = (params: any) => {\n if (params !== null && typeof params === 'object') {\n const optionParams: Record<string, unknown> = {};\n const entries = Object.entries(params);\n\n entries.forEach(([key, value]) => {\n if (value || typeof value === 'boolean' || typeof value === 'number') {\n optionParams[key] = value;\n }\n });\n\n return optionParams;\n }\n\n return {};\n};\n\n/**\n * It replaces all instances of the characters `:`, `$`, `,`, `+`, `[`, and `]` with their\n * URI encoded counterparts\n *\n * @param value The value to be encoded.\n *\n * @returns The encoded value.\n */\nexport const encode = (value: string) => {\n return encodeURIComponent(value)\n .replace(/%3A/gi, ':')\n .replace(/%24/g, '$')\n .replace(/%2C/gi, ',')\n .replace(/%20/g, '+')\n .replace(/%5B/gi, '[')\n .replace(/%5D/gi, ']');\n};\n\n/**\n * A utility function for making API requests.\n *\n * @example\n * import { fetcher } from 'react-query-manager';\n *\n * fetcher({\n * url: 'https://jsonplaceholder.typicode.com/todos/1',\n * method: 'GET',\n * onSuccess: (data, args, context) => {\n * console.log(data);\n * console.log(args);\n * console.log(context);\n * },\n * onError: (error, args, context) => {\n * console.error(error);\n * console.error(args);\n * console.error(context);\n * },\n * context: { value: '1' }\n * });\n *\n * @param args The request configuration.\n *\n * @returns The response as a promise.\n */\nexport const fetcher: ApiClient = ({\n onSuccess, onError, context, ...args\n}) => {\n const isFormData = args.data instanceof FormData;\n\n const apiUrl = (() => {\n let URL = args.url;\n\n if (args.params) {\n const queryParams = filterEmptyParams(args.params);\n\n if (args.queryParamsSerializer) {\n URL += `?${args.queryParamsSerializer(args.params)}`;\n } else if (Object.keys(queryParams).length > 0) {\n const str = [];\n\n for (const p in queryParams) {\n // eslint-disable-next-line no-prototype-builtins\n if (queryParams.hasOwnProperty(p)) {\n if (Array.isArray(queryParams[p])) {\n queryParams[p].forEach((value) => {\n str.push(`${encode(p)}${args.queryArrayParamStyle === 'indexedArray' ? '[]' : ''}=${encode(value)}`);\n });\n } else {\n str.push(\n `${encode(p)}=${encode((queryParams as any)[p])}`,\n );\n }\n }\n }\n\n URL += `?${str.join('&')}`;\n }\n }\n\n const [startUrl, endUrl] = URL.split('?');\n\n return `${startUrl}${endUrl ? `?${endUrl}` : ''}`;\n })();\n\n const body = (() => {\n if (isFormData) {\n return args.data;\n }\n\n if (args.data) {\n return JSON.stringify(args.data);\n }\n\n return '';\n })();\n\n const fetchOptions = {\n method: args.method,\n headers: {\n ...(!isFormData && { 'Content-Type': 'application/json' }),\n ...(args.authorization && { Authorization: args.authorization }),\n ...args.headers,\n },\n ...(body && { body }),\n ...args.options,\n };\n\n return fetch(apiUrl, fetchOptions).then(async (response) => {\n const responseData = await (async () => {\n try {\n const contentLength = response.headers.get('Content-Length');\n const contentType = response.headers.get('Content-Type')?.toLowerCase();\n\n if (\n response.status === 204 ||\n response.status === 304 ||\n contentLength === '0' ||\n !contentType\n ) {\n return null;\n }\n\n if (contentType.includes('application/json')) {\n return await response.json();\n }\n\n if (\n contentType.includes('text/plain')\n || contentType.includes('text/csv')\n || contentType.includes('application/xml')\n || contentType.includes('text/xml')\n || contentType.includes('application/javascript')\n || contentType.includes('text/html')\n ) {\n return await response.text();\n }\n\n if (contentType.includes('multipart/form-data')) {\n return await response.formData();\n }\n\n return await response.blob();\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n } catch (error) {\n return null;\n }\n })();\n\n const headers: Record<string, string> = {};\n\n response.headers.forEach((value, key) => {\n headers[key] = value;\n });\n\n return {\n status: response.status,\n statusText: response.statusText,\n headers,\n data: responseData,\n };\n }).then((result) => {\n if (result.status < 200 || result.status >= 300) {\n const error = new CustomError(\n `Request failed with status code: ${result.status}`,\n result.status,\n result.data,\n );\n\n if (onError) {\n onError(error, args, context);\n }\n\n return Promise.reject(error);\n }\n\n if (onSuccess) {\n onSuccess(result, args, context);\n }\n\n return Promise.resolve(result);\n }).catch((error) => {\n return Promise.reject(new CustomError(\n error.message,\n ));\n });\n};\n","import { fetcher } from './utils/fetcher';\n\nexport * from '@tanstack/react-query';\n\nexport * from './hooks/use-get-list';\nexport * from './hooks/use-get-infinite-list';\nexport * from './hooks/use-get-one';\nexport * from './hooks/use-delete';\nexport * from './hooks/use-update';\nexport * from './hooks/use-create';\nexport * from './hooks/use-data-query';\nexport * from './hooks/use-data-mutate';\n\nexport * from './components/RQWrapper';\n\nexport * from './utils/custom-error';\nexport * from './utils/toast';\nexport * from './utils/get-url-from-resource';\nexport * from './utils/queries';\n\nexport * from './type';\n\nexport { fetcher };\n","import { useQuery } from '@tanstack/react-query';\nimport {\n Resource,\n UseQueryProps,\n QueryResponse,\n QueryListKey,\n ApiProps,\n} from '../type';\nimport { getUrlFromResource } from '../utils/get-url-from-resource';\nimport { useRQWrapperContext } from '../components/RQWrapper';\nimport { CustomError } from '../utils/custom-error';\n\n/**\n * A hook that helps you fetch a list of resources.\n *\n * The hook uses `useQuery` from `@tanstack/react-query` to fetch data and cache it.\n * It accepts various query options and performs an API request to fetch a list of resources\n * based on the provided `resource` and `params`. The hook supports additional query parameters\n * and custom API client parameters.\n *\n * If a custom `queryFn` is provided, it will be used to perform the query; otherwise,\n * the default API client method will be used. The `queryKey` is constructed based on\n * the resource path and additional parameters to ensure proper caching and refetching.\n *\n * @example\n * import { useGetList } from 'react-query-manager';\n *\n * type TData = { id: 1, name: 'Test' };\n * const PATH = 'users/{id}/messages';\n *\n * const queryList = useGetList<typeof PATH, TData>({\n * resource: { path: PATH, params: { id: 1 } },\n * queryOptions: {\n * onSuccess: (data) => {\n * console.log('Data fetched successfully:', data);\n * },\n * },\n * params: { sortBy: 'price', order: 'asc' },\n * });\n *\n * @template TPath - The API path as a string.\n * @template TData - The expected shape of the data returned by the API.\n *\n * @param params The parameters for the hook.\n * @param params.queryOptions - Additional options to configure the `useQuery`\n * @param params.resource - The resource path and any static parameters for the API request.\n * @param params.params - Dynamic query parameters for the API request.\n * @param params.apiClientParams - Additional options to pass to the API client.\n *\n * @returns The result of the `useQuery` hook.\n */\nexport const useGetList = <TPath extends string, TData = any>({\n queryOptions,\n resource,\n params = {},\n apiClientParams,\n}: {\n queryOptions?: UseQueryProps<\n QueryResponse<TData[]>,\n QueryListKey<TPath>,\n {\n resource: Resource<TPath>;\n params: QueryListKey<TPath>['3'];\n queryKey: QueryListKey<TPath>;\n }\n >;\n resource: Resource<TPath>;\n params?: QueryListKey<TPath>['3'];\n apiClientParams?: Partial<ApiProps>;\n}) => {\n const { apiUrl, apiClient, apiEnsureTrailingSlash } = useRQWrapperContext();\n\n const query = useQuery<\n QueryResponse<TData[]>,\n CustomError,\n QueryResponse<TData[]>,\n QueryListKey<TPath>\n >({\n ...queryOptions,\n queryKey: [\n 'get-list',\n resource.path,\n resource.params,\n params,\n ...(queryOptions?.queryKey ? queryOptions.queryKey : []),\n ] as QueryListKey<TPath>,\n queryFn: async ({ queryKey }) => {\n const variables = { resource, params, queryKey };\n\n const url = `${apiUrl}/${getUrlFromResource(variables.resource, apiEnsureTrailingSlash)}`;\n\n if (queryOptions?.queryFn) {\n const results = await queryOptions?.queryFn({\n apiClient, apiUrl, variables, url,\n });\n return results;\n }\n\n const result = await apiClient({\n url, method: 'GET', params, ...apiClientParams,\n });\n\n return result;\n },\n });\n\n return query;\n};\n","/**\n * Removes leading and trailing slashes from the given string.\n *\n * @param path The string to remove slashes from.\n * @return The string with leading and trailing slashes removed.\n */\nexport function removeFirstAndLastSlash(path: string): string {\n return typeof path === 'string'\n ? path.replace(/^\\/+/, '').replace(/\\/+$/, '')\n : '';\n}\n","import { removeFirstAndLastSlash } from '../internal/utils/remove-first-and-last-slash';\nimport { Resource } from '../type';\n\n/**\n * Takes a `Resource` object and returns its path as a string,\n * with any path parameters replaced with their corresponding values.\n * Optionally, it can ensure that the returned URL has a trailing slash.\n *\n * @template TPath - A string literal representing the path template with placeholders.\n *\n * @param {Resource<TPath>} resource - The `Resource` object containing the path and parameters.\n * @param {boolean} ensureTrailingSlash - If `true`, the returned URL will have a trailing slash.\n *\n * @returns {string} The URL with all placeholders replaced by the corresponding values from `params`.\n *\n * @example\n * const resource = {\n * path: 'users/{id}/messages',\n * params: { id: 1 },\n * };\n *\n * getUrlFromResource(resource, false); // 'users/1/messages'\n * getUrlFromResource(resource, true); // 'users/1/messages/'\n */\nexport const getUrlFromResource = <TPath extends string>(resource: Resource<TPath>, ensureTrailingSlash?: boolean) => {\n const url = removeFirstAndLastSlash(resource.path.replace(/{(\\w+)}/g, (_, key: keyof Resource<TPath>['params']) => {\n return resource.params[key]!.toString();\n }));\n\n return ensureTrailingSlash ? `${url}/` : url;\n};\n","import { QueryClient, QueryClientConfig, QueryClientProvider } from '@tanstack/react-query';\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools';\nimport React, {\n createContext, ReactNode, useCallback, useContext, useMemo,\n} from 'react';\nimport { fetcher } from '../utils/fetcher';\nimport {\n ApiProps, ApiClient, ToastProps, ToastCustomContent,\n ToastCustomUndoContent,\n RQWrapperContextProps,\n} from '../type';\nimport { Toaster } from '../internal/components/Toaster';\nimport { toast } from '../utils/toast';\nimport { undoEventEmitter } from '../internal/utils/undo-event-emitter';\nimport { removeFirstAndLastSlash } from '../internal/utils/remove-first-and-last-slash';\nimport { IS_TEST_ENV } from '../internal/env';\nimport { queryClientManager } from '../internal/query-client';\n\nconst Context = createContext<RQWrapperContextProps>({\n apiUrl: '',\n apiEnsureTrailingSlash: false,\n apiClient: fetcher,\n toastUndo: () => {},\n});\n\n/**\n * Get the context for the RQWrapper component.\n *\n * This hook returns the context for the RQWrapper component, which includes the\n * API URL, API client, and toast undo function.\n *\n * @returns The RQWrapper context.\n */\nexport const useRQWrapperContext = () => {\n return useContext(Context);\n};\n\ntype ReactQueryDevtoolsProps = React.ComponentProps<typeof ReactQueryDevtools>;\n\n/**\n * This component wraps your application and provides the necessary context\n * for the hooks to work.\n *\n * @example\n * import { RQWrapper, ToastCustomContent, ToastBar } from 'react-query-manager';\n *\n * const ToastWrapper: ToastCustomContent = (props) => {\n * return <ToastBar toast={props} position={props.position} />;\n * };\n *\n * <RQWrapper\n * isDevTools\n * devToolsOptions={{\n * buttonPosition: 'bottom-left',\n * }}\n * apiUrl=\"https://jsonplaceholder.typicode.com\"\n * apiAuthorization={() => 'Bearer 12345'}\n * apiOnSuccess={(...args) => {\n * console.log('apiOnSuccess: ', args);\n * }}\n * apiOnError={(...args) => {\n * console.log('apiOnError: ', args);\n * }}\n * toast={{\n * globalProps: {\n * position: 'bottom-center',\n * },\n * wrapper: ToastWrapper,\n * }}\n * >\n * <List />\n * </RQWrapper>\n *\n * @param props\n * @param props.children - The children components to render.\n * @param props.config - The configuration for the underlying QueryClient instance.\n * @param props.apiUrl - The base URL for all API requests.\n * @param props.apiClient - The function to use for making API requests.\n * Defaults to `fetcher` from `react-query-manager`.\n * @param props.apiEnsureTrailingSlash - If `true`, the returned URL will have a trailing slash.\n * @param props.apiAuthorization - A function to get the authorization\n * token for API requests. If not provided, or if the function returns an empty\n * string, no authorization token will be used.\n * @param props.apiHeaders - A function to get the headers\n * for API requests. If not provided, or if the function returns an empty\n * object, no headers will be used.\n * @param props.apiOnSuccess - A callback to run when an API\n * request is successful. If not provided, the default behavior will be used.\n * @param props.apiOnError - A callback to run when an API\n * request fails. If not provided, the default behavior will be used.\n * @param props.isDevTools - Whether to render the React Query devtools.\n * Defaults to `false`.\n * @param props.devToolsOptions - Options to pass to the\n * React Query devtools.\n * @param props.toast - Options for the\n * `toast` utility from `react-hot-toast`.\n * See the [documentation](https://react-hot-toast.com/docs) for more details.\n *\n * The `wrapper` property can be used to customize the toast component.\n *\n * The `globalProps` property can be used to customize the default props for the toast component.\n *\n * The `ToastCustomUndoContent` property can be used to customize the content of the toast when the user\n * clicks the \"UNDO\" button.\n */\nexport function RQWrapper({\n children,\n config = {},\n apiUrl,\n apiClient = fetcher,\n apiEnsureTrailingSlash = false,\n apiAuthorization,\n apiHeaders,\n apiOnSuccess,\n apiOnError,\n isDevTools,\n devToolsOptions,\n toast: toastProps,\n}: {\n children: ReactNode;\n apiUrl: string;\n config?: QueryClientConfig;\n apiClient?: ApiClient;\n apiAuthorization?: () => string;\n apiHeaders?: () => ApiProps['headers'];\n apiOnSuccess?: ApiProps['onSuccess'];\n apiOnError?: ApiProps['onError'];\n apiEnsureTrailingSlash?: boolean;\n isDevTools?: boolean;\n devToolsOptions?: ReactQueryDevtoolsProps;\n toast?: {\n globalProps?: ToastProps;\n CustomContent?: ToastCustomContent;\n CustomUndoContent?: ToastCustomUndoContent;\n };\n}) {\n const queryClient = useMemo(() => {\n const client = new QueryClient({\n ...config,\n });\n\n queryClientManager.queryClient = client;\n\n return client;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const fetch = useCallback<ApiClient>((args) => {\n const globalAuthorization = apiAuthorization ? apiAuthorization() : '';\n const globalHeaders = apiHeaders ? apiHeaders() : {};\n\n const onSuccess: ApiProps['onSuccess'] = (...successArgs) => {\n if (apiOnSuccess) {\n apiOnSuccess(...successArgs);\n }\n\n if (args.onSuccess) {\n args.onSuccess(...successArgs);\n }\n };\n\n const onError: ApiProps['onError'] = (...errorArgs) => {\n if (apiOnError) {\n apiOnError(...errorArgs);\n }\n\n if (args.onError) {\n args.onError(...errorArgs);\n }\n };\n\n return apiClient({\n ...args,\n headers: args.headers ? {\n ...globalHeaders,\n ...args.headers,\n } : globalHeaders,\n authorization: args.authorization || globalAuthorization,\n onSuccess,\n onError,\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const toastUndo = useCallback<RQWrapperContextProps['toastUndo']>((data) => {\n let isSuccess = false;\n\n toast.dismiss();\n\n const onUndo = () => {\n isSuccess = true;\n undoEventEmitter.emit('end', true);\n toast.dismiss();\n };\n\n if (!IS_TEST_ENV) {\n toast.success(\n (t) => {\n const CustomContent = toastProps?.CustomUndoContent;\n\n if (!t.visible && !isSuccess) {\n isSuccess = true;\n undoEventEmitter.emit('end', false);\n }\n\n return CustomContent\n ? (\n <CustomContent\n message={data.message}\n onUndo={onUndo}\n type={data.type}\n toast={t}\n />\n )\n : (\n <>\n {data.message}\n\n <span\n style={{ marginLeft: '10px', cursor: 'pointer' }}\n onClick={onUndo}\n role=\"button\"\n tabIndex={0}\n aria-label=\"Undo\"\n title=\"Undo\"\n >\n UNDO\n </span>\n </>\n );\n },\n {\n duration: toastProps?.globalProps?.toastOptions?.duration || 5000,\n },\n );\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const contextValue = useMemo<RQWrapperContextProps>(() => ({\n apiUrl: removeFirstAndLastSlash(apiUrl),\n apiClient: fetch,\n apiEnsureTrailingSlash,\n toastUndo,\n }), [apiUrl, fetch, toastUndo, apiEnsureTrailingSlash]);\n\n return (\n <QueryClientProvider client={queryClient}>\n {!IS_TEST_ENV && (\n <Toaster {...toastProps?.globalProps}>\n {toastProps?.CustomContent}\n </Toaster>\n )}\n\n <Context.Provider value={contextValue}>\n {children}\n </Context.Provider>\n\n {isDevTools && (\n <ReactQueryDevtools\n buttonPosition=\"bottom-right\"\n initialIsOpen={false}\n {...devToolsOptions}\n />\n )}\n </QueryClientProvider>\n );\n}\n","import React, { useCallback } from 'react';\nimport { type ToasterProps, type ToastPosition } from 'react-hot-toast';\nimport { resolveValue, useToaster } from 'react-hot-toast/headless';\n\nconst prefersReducedMotion = (() => {\n let shouldReduceMotion: boolean | undefined;\n\n return () => {\n // @ts-ignore\n if (shouldReduceMotion === undefined && typeof window !== 'undefined' && window.matchMedia) {\n const mediaQuery = matchMedia('(prefers-reduced-motion: reduce)');\n shouldReduceMotion = !mediaQuery || mediaQuery.matches;\n }\n return shouldReduceMotion;\n };\n})();\n\nfunction ToastWrapper({\n id,\n className,\n style,\n onHeightUpdate,\n children,\n}: {\n id: string;\n className?: string;\n style?: React.CSSProperties;\n onHeightUpdate: (id: string, height: number) => void;\n children?: React.ReactNode;\n}) {\n const ref = useCallback(\n (el: HTMLElement | null) => {\n if (el) {\n const updateHeight = () => {\n const { height } = el.getBoundingClientRect();\n onHeightUpdate(id, height);\n };\n updateHeight();\n new MutationObserver(updateHeight).observe(el, {\n subtree: true,\n childList: true,\n characterData: true,\n });\n }\n },\n [id, onHeightUpdate],\n );\n\n return (\n <div data-toast-id={id} data-testid=\"toast-wrapper\" ref={ref} className={className} style={style}>\n {children}\n </div>\n );\n}\n\nconst getPositionStyle = (\n position: ToastPosition,\n offset: number,\n): React.CSSProperties => {\n const top = position.includes('top');\n const verticalStyle: React.CSSProperties = top ? { top: 0 } : { bottom: 0 };\n const horizontalStyle: React.CSSProperties = position.includes('center')\n ? { justifyContent: 'center' }\n : position.includes('right')\n ? { justifyContent: 'flex-end' }\n : {};\n\n return {\n left: 0,\n right: 0,\n display: 'flex',\n position: 'absolute',\n transition: prefersReducedMotion()\n ? undefined\n : 'all 230ms cubic-bezier(.21,1.02,.73,1)',\n transform: `translateY(${offset * (top ? 1 : -1)}px)`,\n ...verticalStyle,\n ...horizontalStyle,\n };\n};\n\nconst DEFAULT_OFFSET = 16;\n\nexport function Toaster({\n reverseOrder,\n position = 'top-center',\n toastOptions,\n gutter,\n children,\n containerStyle,\n containerClassName,\n}: ToasterProps) {\n const { toasts, handlers } = useToaster(toastOptions);\n\n return (\n <div\n style={{\n position: 'fixed',\n zIndex: 9999,\n top: DEFAULT_OFFSET,\n left: DEFAULT_OFFSET,\n right: DEFAULT_OFFSET,\n bottom: DEFAULT_OFFSET,\n pointerEvents: 'none',\n ...containerStyle,\n }}\n className={containerClassName}\n onMouseEnter={handlers.startPause}\n onMouseLeave={handlers.endPause}\n >\n {toasts.map((t) => {\n const toastPosition = t.position || position;\n const toast = { ...t, position: toastPosition };\n\n const offset = handlers.calculateOffset(toast, {\n reverseOrder,\n gutter,\n defaultPosition: position,\n });\n const positionStyle = getPositionStyle(toastPosition, offset);\n\n const Component = children;\n\n return (\n <ToastWrapper\n id={toast.id}\n key={toast.id}\n onHeightUpdate={handlers.updateHeight}\n style={{\n ...positionStyle,\n pointerEvents: 'auto',\n }}\n >\n {toast.type === 'custom' ? (\n resolveValue(t.message, toast)\n ) : Component ? (\n <Component {...toast} />\n ) : (\n <div style={{ display: t.visible ? 'flex' : 'none' }}>\n {resolveValue(toast.message, toast)}\n </div>\n )}\n </ToastWrapper>\n );\n })}\n </div>\n );\n}\n","import toastApi, { Renderable, ToastPosition } from 'react-hot-toast/headless';\nimport { type Toast, ToastBar as ToastBarToast, resolveValue } from 'react-hot-toast';\nimport React from 'react';\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst { remove, ...restOfToastApi } = toastApi;\n\n/** @notExported */\ninterface ToastBarProps {\n toast: Toast;\n position?: ToastPosition;\n style?: React.CSSProperties;\n children?: (components: {\n icon: Renderable;\n message: Renderable;\n }) => Renderable;\n}\n\n/**\n * Export of `toast` from `react-hot-toast/headless`, which is an API for creating notifications,\n *\n * ⚠️ but without the **`remove`** method ⚠️.\n *\n * See the [documentation](https://react-hot-toast.com/docs/toast) for more details.\n */\nexport const toast = Object.assign(\n (...args: Parameters<typeof toastApi>) => toastApi(...args),\n restOfToastApi,\n);\n\n/**\n * `ToastBar` is a wrapper for displaying notifications from the `react-hot-toast` library.\n * You can use it or create your own implementation. See the [documentation](https://react-hot-toast.com/docs/toast-bar) for more details.\n */\nexport const ToastBar: React.FC<ToastBarProps> = ToastBarToast;\n\n/**\n * A utility function that resolves the value of a toast message. The value can either be a static value or a function\n * that generates the value based on the provided argument.\n *\n * @example\n * // Resolving a static value\n * const message = resolveToastValue('Hello, World!', toast);\n *\n * @example\n * // Resolving a value from a function\n * const message = resolveToastValue((ctx) => `Hello, ${ctx.userName}!`, toast);\n */\nexport const resolveToastValue = resolveValue;\n","import EventEmitter from 'eventemitter3';\n\nexport const eventEmitter = new EventEmitter();\n\nexport const undoEventEmitter = {\n /**\n * Listens for the next 'end' event and then removes the listener.\n *\n * @param type The type of event to listen for. Currently only 'end' is supported.\n * @param callback The callback function to be called when the event is emitted. The callback will receive a boolean indicating whether the event was triggered by an undo action.\n * @return A function that can be called to remove the listener.\n */\n once: (type: 'end', callback: (isUndo: boolean) => void) => {\n eventEmitter.once(type, callback);\n },\n /**\n * Emits an 'end' event, which is used to let any registered callbacks know that an undo/redo action has completed.\n *\n * @param type The type of event to emit. Currently only 'end' is supported.\n * @param isUndo A boolean indicating whether the event was triggered by an undo action.\n */\n emit: (type: 'end', isUndo: boolean) => {\n eventEmitter.emit(type, isUndo);\n },\n};\n","// TODO\nexport const IS_TEST_ENV = process.env.NODE_ENV === 'test';\n","import { QueryClient } from '@tanstack/react-query';\n\nexport const queryClientManager: {\n queryClient: QueryClient;\n} = {\n queryClient: {} as QueryClient,\n};\n\nexport const getQueryClient = () => {\n return queryClientManager.queryClient;\n};\n","import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query';\nimport { useRQWrapperContext } from '../components/RQWrapper';\nimport {\n ApiProps, QueryResponse, QueryInfiniteListKey,\n QueryInfinitePagination,\n Resource, UseInfiniteQueryProps,\n} from '../type';\nimport { CustomError } from '../utils/custom-error';\nimport { getUrlFromResource } from '../utils/get-url-from-resource';\n\n/**\n * A hook that helps you fetch a infinite list of resources.\n *\n * The hook uses `useInfiniteQuery` from `@tanstack/react-query` to fetch data and cache it.\n * It accepts various query options and performs an API request to fetch a list of resources\n * based on the provided `resource` and `params`. The hook supports additional query parameters\n * and custom API client parameters.\n *\n * If a custom `queryFn` is provided, it will be used to perform the query; otherwise,\n * the default API client method will be used. The `queryKey` is constructed based on\n * the resource path and additional parameters to ensure proper caching and refetching.\n *\n * By default, this hook sets the following options:\n * - `initialPageParam`: 1\n * - `getNextPageParam`: Calculates the next page number based on the length of the data in the last page.\n * - `getPreviousPageParam`: Calculates the previous page number, but prevents it from going below 1.\n *\n * These default options can be overridden if necessary.\n *\n * @example\n * import { useGetInfiniteList } from 'react-query-manager';\n *\n * type TData = { id: 1, name: 'Test' };\n * const PATH = 'users/{id}/messages';\n *\n * const infiniteQuery = useGetInfiniteList<typeof PATH, TData>({\n * resource: { path: PATH, params: { id: 10 } },\n * pagination: { page: ['page_number'], per_page: ['count', 20] },\n * });\n *\n * @template TPath - The API path as a string.\n * @template TData - The expected shape of the data returned by the API.\n *\n * @param options - The options object for configuring the hook.\n * @param options.queryOptions - Additional options to configure the `useInfiniteQuery` hook.\n * @param options.resource - The resource path and any static parameters for the API request.\n * @param options.params - Dynamic query parameters for the API request.\n * @param options.apiClientParams - Additional options to pass to the API client.\n * @param options.pagination - The pagination configuration.\n *\n * - **`page`** - An array where the first element is the name of the query parameter that represents the page number. The page number will automatically increment with each subsequent request.\n *\n * - **`per_page`** - An array where the first element is the name of the query parameter that represents the number of items per page, and the second element is the value to be used for that parameter.\n *\n * For example:\n *\n * - **`{ page: ['page_number'], per_page: ['count', 20] }`** will result in query parameters like **`?page_number={{pageParam}}&count=20`**.\n *\n * @returns The result of the `useInfiniteQuery` hook.\n */\nexport const useGetInfiniteList = <TPath extends string, TData = any>({\n queryOptions,\n resource,\n params = {},\n apiClientParams,\n pagination,\n}: {\n queryOptions?: UseInfiniteQueryProps<\n QueryResponse<TData[]>,\n QueryInfiniteListKey<TPath>,\n {\n resource: Resource<TPath>;\n params: QueryInfiniteListKey<TPath>['4'];\n queryKey: QueryInfiniteListKey<TPath>;\n }\n >;\n resource: Resource<TPath>;\n params?: QueryInfiniteListKey<TPath>['4'];\n apiClientParams?: Partial<ApiProps>;\n pagination: QueryInfinitePagination;\n}) => {\n const { apiUrl, apiClient, apiEnsureTrailingSlash } = useRQWrapperContext();\n\n const query = useInfiniteQuery<\n QueryResponse<TData[]>,\n CustomError,\n InfiniteData<QueryResponse<TData[]>>,\n QueryInfiniteListKey<TPath>\n >({\n initialPageParam: 1,\n getNextPageParam: (...args) => {\n const lastPage = args[0];\n const lastPageParam = Number(args[2]);\n\n if (!lastPage?.data?.length) {\n return undefined;\n }\n\n return lastPageParam + 1;\n },\n getPreviousPageParam: (...args) => {\n const firstPageParam = Number(args[2]);\n\n if (firstPageParam <= 1) {\n return undefined;\n }\n\n return firstPageParam - 1;\n },\n ...queryOptions,\n queryKey: [\n 'get-infinite-list',\n resource.path,\n resource.params,\n pagination,\n params,\n ...(queryOptions?.queryKey ? queryOptions.queryKey : []),\n ] as QueryInfiniteListKey<TPath>,\n queryFn: async ({ queryKey, pageParam }) => {\n const variables = {\n resource,\n params: {\n ...params,\n [pagination.page[0]]: pageParam,\n [pagination.per_page[0]]: pagination.per_page[1],\n },\n queryKey,\n };\n\n const url = `${apiUrl}/${getUrlFromResource(variables.resource, apiEnsureTrailingSlash)}`;\n\n if (queryOptions?.queryFn) {\n const results = await queryOptions?.queryFn({\n apiClient, apiUrl, variables, url,\n });\n return results;\n }\n\n const result = await apiClient({\n url,\n method: 'GET',\n params: variables.params,\n ...apiClientParams,\n });\n\n return result;\n },\n });\n\n return query;\n};\n","import { useQuery } from '@tanstack/react-query';\nimport {\n Resource,\n UseQueryProps,\n QueryResponse,\n QueryOneKey,\n ApiProps,\n} from '../type';\nimport { getUrlFromResource } from '../utils/get-url-from-resource';\nimport { useRQWrapperContext } from '../components/RQWrapper';\nimport { CustomError } from '../utils/custom-error';\n\n/**\n * A hook that helps you fetch a single resource.\n *\n * The hook uses `useQuery` from `@tanstack/react-query` to fetch data and cache it.\n * It accepts various query options and performs the API request to fetch the resource\n * identified by the given `id`. The hook supports additional query parameters and custom\n * API client parameters.\n *\n * If a custom `queryFn` is provided, it will be used to perform the query; otherwise,\n * the default API client method will be used. The `queryKey` is constructed based on\n * the resource path, ID, and other optional parameters to ensure proper caching and\n * refetching.\n *\n * @example\n * import { useGetOne } from 'react-query-manager';\n *\n * type TData = { id: 1, name: 'Test' };\n * const PATH = 'users/{id}/messages';\n *\n * const queryOne = useGetOne<typeof PATH, TData>({\n * resource: { path: PATH, params: { id: 1 } },\n * id: 123,\n * queryOptions: {\n * onSuccess: (data) => {\n * console.log('Data fetched successfully:', data);\n * },\n * },\n * params: { include: 'details' },\n * });\n *\n * @template TPath - The API path as a string.\n * @template TData - The expected shape of the data returned by the API.\n *\n * @param params The parameters for the hook.\n * @param params.queryOptions - Additional options to configure the `useQuery`\n * @param params.resource - The resource path and any static parameters for the API request.\n * @param params.params - Dynamic query parameters for the API request.\n * @param params.apiClientParams - Additional options to pass to the API client.\n */\nexport const useGetOne = <TPath extends string, TData = any>({\n resource,\n id,\n queryOptions,\n params = {},\n apiClientParams,\n}: {\n resource: Resource<TPath>;\n id: string | number;\n queryOptions?: UseQueryProps<\n QueryResponse<TData>,\n QueryOneKey<TPath>,\n {\n resource: Resource<TPath>;\n id: string | number;\n params: QueryOneKey<TPath>['4'];\n queryKey: QueryOneKey<TPath>;\n }\n >;\n params?: QueryOneKey<TPath>['4'];\n apiClientParams?: Partial<ApiProps>;\n}) => {\n const { apiUrl, apiClient, apiEnsureTrailingSlash } = useRQWrapperContext();\n\n const query = useQuery<\n QueryResponse<TData>,\n CustomError,\n QueryResponse<TData>,\n QueryOneKey<TPath>\n >({\n ...queryOptions,\n queryKey: [\n 'get-one',\n resource.path,\n resource.params,\n String(id),\n params,\n ...(queryOptions?.queryKey ? queryOptions.queryKey : []),\n ] as QueryOneKey<TPath>,\n queryFn: async ({ queryKey }) => {\n const variables = {\n id, resource, params, queryKey,\n };\n\n const url = `${apiUrl}/${getUrlFromResource(variables.resource, true)}`;\n\n if (queryOptions?.queryFn) {\n const results = await queryOptions?.queryFn({\n apiClient, apiUrl, variables, url,\n });\n return results;\n }\n\n const result = await apiClient({\n url: `${url}${variables.id}${apiEnsureTrailingSlash ? '/' : ''}`,\n method: 'GET',\n params,\n ...apiClientParams,\n });\n\n return result;\n },\n });\n\n return query;\n};\n","import { useMutation, useQueryClient } from '@tanstack/react-query';\nimport { useRef } from 'react';\nimport {\n MutateMode,\n Resource,\n UseMutateProps,\n QueryResponse,\n MutationMode,\n MutateKey,\n ApiProps,\n} from '../type';\nimport { useRQWrapperContext } from '../components/RQWrapper';\nimport { getUrlFromResource } from '../utils/get-url-from-resource';\nimport { CustomError } from '../utils/custom-error';\nimport { Snapshot } from '../internal/type';\nimport { deleteItemsFromQueryCache, helpersQueryKeys, invalidateQueries } from '../utils/queries';\nimport { undoEventEmitter } from '../internal/utils/undo-event-emitter';\nimport { createSnapshot } from '../internal/utils/create-snapshot';\nimport { IS_TEST_ENV } from '../internal/env';\n\n/** @notExported */\ntype MutateBaseVariables<TPath extends string, TType, TExtraData> = (\n TType extends 'many' ? {\n ids: (string | number)[];\n resource: Resource<TPath>;\n apiClientParams?: Partial<ApiProps>;\n extraData?: TExtraData;\n extraResources?: Resource<any>[];\n } : {\n id: string | number;\n resource: Resource<TPath>;\n apiClientParams?: Partial<ApiProps>;\n extraData?: TExtraData;\n extraResources?: Resource<any>[];\n }\n);\n\n/** @notExported */\ntype DeleteBaseVariables<TPath extends string, TType, TExtraData> = (\n Omit<MutateBaseVariables<TPath, TType, TExtraData>, 'resource'> & {\n resourceParams: Resource<TPath>['params'];\n undoMessage?: string;\n }\n);\n\n/** @notExported */\ntype DeleteBase<\n TPath extends string,\n TData = any,\n TType extends MutationMode = 'many',\n TExtraData = any,\n> = {\n resourcePath: Resource<TPath>['path'];\n mutationOptions?: UseMutateProps<\n TType extends 'many' ? QueryResponse<TData>[] : QueryResponse<TData>,\n MutateBaseVariables<TPath, TType, TExtraData>\n >;\n mode?: MutateMode;\n extraResources?: Resource<any>[];\n shouldUpdateCurrentResource?: boolean;\n shouldInvalidateCache?: boolean;\n type: TType;\n};\n\nconst useDeleteBase = <\n TPath extends string,\n TData = any,\n TType extends MutationMode = 'many',\n TExtraData = any,\n>({\n resourcePath,\n mutationOptions,\n mode = {\n optimistic: true,\n undoable: true,\n },\n extraResources: extraResourcesProps = [],\n shouldUpdateCurrentResource = true,\n shouldInvalidateCache = true,\n type = 'many' as TType,\n }: DeleteBase<TPath, TData, TType, TExtraData>) => {\n const {\n apiUrl, apiClient, apiEnsureTrailingSlash, toastUndo,\n } = useRQWrapperContext();\n const queryClient = useQueryClient();\n\n const snapshot = useRef<Snapshot>([]);\n const backToSnapshot = () => {\n snapshot.current.forEach(([key, value]) => {\n queryClient.setQueryData(key, value);\n });\n };\n\n const { mutate, ...mutation } = useMutation<\n TType extends 'many' ? QueryResponse<TData>[] : QueryResponse<TData>,\n CustomError,\n MutateBaseVariables<TPath, TType, TExtraData>\n >({\n ...mutationOptions,\n mutationKey: [\n type === 'many' ? 'delete-many' : 'delete-one',\n resourcePath,\n ...(mutationOptions?.mutationKey ? mutationOptions.mutationKey : []),\n ] as MutateKey<TPath>,\n mutationFn: async (variables) => {\n const url = `${apiUrl}/${getUrlFromResource(variables.resource, true)}`;\n\n if (mutationOptions?.mutationFn) {\n const results = await mutationOptions?.mutationFn({\n apiClient, apiUrl, variables, url,\n });\n return results;\n }\n\n const ids = type === 'many'\n ? (variables as MutateBaseVariables<TPath, 'many', TExtraData>).ids\n : [(variables as MutateBaseVariables<TPath, 'one', TExtraData>).id];\n\n const actions = await Promise.allSettled(ids.map((id) => apiClient<TData>({\n url: `${url}${id}${apiEnsureTrailingSlash ? '/' : ''}`,\n method: 'DELETE',\n ...variables.apiClientParams,\n })));\n\n const result: QueryResponse<TData>[] = [];\n\n actions.forEach((response) => {\n if (response.status === 'fulfilled') {\n result.push(response.value);\n } else {\n throw response.reason;\n }\n });\n\n return (type === 'many' ? result : result[0]) as any;\n },\n onSuccess: (...rest) => {\n if (shouldInvalidateCache) {\n const variables = rest[1];\n\n const extraResources = variables.extraResources ? [\n ...extraResourcesProps,\n ...variables.extraResources,\n ] : extraResourcesProps;\n\n const queryKeys = [\n helpersQueryKeys.getList(variables.resource),\n helpersQueryKeys.getInfiniteList(variables.resource),\n ];\n\n extraResources.forEach((extResource) => {\n queryKeys.push(helpersQueryKeys.getList(extResource));\n queryKeys.push(helpersQueryKeys.getInfiniteList(extResource));\n });\n\n invalidateQueries({ queryKeys });\n }\n\n if (mutationOptions?.onSuccess) {\n mutationOptions.onSuccess(...rest);\n }\n },\n onError: (...rest) => {\n if (mutationOptions?.onError) {\n mutationOptions.onError(...rest);\n }\n\n backToSnapshot();\n },\n });\n\n const deleteBase = async ({ resourceParams, undoMessage, ...variables }: DeleteBaseVariables<TPath, TType, TExtraData>) => {\n const resource: Resource<TPath> = {\n path: resourcePath,\n params: resourceParams,\n };\n\n const ids = type === 'many'\n ? (variables as any as DeleteBaseVariables<TPath, 'many', TExtraData>).ids\n : [(variables as any as DeleteBaseVariables<TPath, 'one', TExtraData>).id];\n\n if (mode.optimistic) {\n const extraResources = variables.extraResources ? [\n ...extraResourcesProps,\n ...variables.extraResources,\n ] : extraResourcesProps;\n\n const queryKeysOne = shouldUpdateCurrentResource ? helpersQueryKeys.getOneArray(resource, ids) : [];\n const queryKeysList = shouldUpdateCurrentResource ? [helpersQueryKeys.getList(resource)] : [];\n const queryKeysInfiniteList = shouldUpdateCurrentResource ? [helpersQueryKeys.getInfiniteList(resource)] : [];\n\n extraResources.forEach((extResource) => {\n queryKeysOne.push(...helpersQueryKeys.getOneArray(extResource, ids));\n queryKeysList.push(helpersQueryKeys.getList(extResource));\n queryKeysInfiniteList.push(helpersQueryKeys.getInfiniteList(extResource));\n });\n\n snapshot.current = await createSnapshot([\n ...queryKeysOne,\n ...queryKeysList,\n ...queryKeysInfiniteList,\n ]);\n\n deleteItemsFromQueryCache({\n ids,\n queryKeysOne,\n queryKeysList,\n queryKeysInfiniteList,\n });\n }\n\n if (mode.undoable && !IS_TEST_ENV) {\n const isMany = ids.length > 1;\n\n undoEventEmitter.once('end', (isUndo) => {\n if (isUndo) {\n backToSnapshot();\n } else {\n mutate({ ...variables, resource } as any);\n }\n });\n\n toastUndo({\n message: undoMessage || `Element${isMany ? 's' : ''} deleted`,\n type: isMany ? 'delete-many' : 'delete-one',\n });\n } else {\n mutate({ ...variables, resource } as any);\n }\n };\n\n return {\n mutation,\n delete: deleteBase,\n };\n};\n\n/**\n * A hook that helps you delete a single resource.\n *\n * The hook uses `useMutation` from `@tanstack/react-query` under the hood, so it accepts all the same options.\n * It performs an optimistic update by removing the resource from the cache before\n * the deletion request is sent. If the deletion fails, the resource is restored in the cache.\n *\n * If the `undoable` mode is enabled, the hook allows the deletion to be undone within a certain\n * period of time. If the undo action is not performed, the resource will be permanently deleted.\n *\n * @example\n * import { useDeleteOne } from 'react-query-manager';\n *\n * type TData = { id: 1, name: 'Test' };\n * const PATH = 'users/{id}/messages';\n *\n * const { delete: deleteOne } = useDeleteOne<typeof PATH, TData>({\n * resourcePath: PATH,\n * });\n *\n * deleteOne({\n * id: 123,\n * resourceParams: { id: 1 },\n * undoMessage: 'Message deleted',\n * });\n *\n *\n * @template TPath - The API path as a string.\n * @template TData - The expected shape of the data returned by the API.\n *\n * @param props - The options for the hook.\n * @returns An object with properties, `delete` and `mutation`.\n *\n * `delete` is a function that takes the ID and params of the resource to delete,\n * and calls the mutation function with the necessary data.\n *\n * `mutation` is result `useMutation` without propery `mutate`\n */\nexport const useDeleteOne = <\n TPath extends string,\n TData = any,\n TExtraData = any,\n>(props: Omit<DeleteBase<TPath, TData, 'one', TExtraData>, 'type'>) => {\n return useDeleteBase({ ...props, type: 'one' });\n};\n\n/**\n * A hook that helps you delete multiple resources at once.\n *\n * The hook uses `useMutation` from `@tanstack/react-query under the hood, so it accepts all the same options.\n * It performs an optimistic update by removing the resources from the cache before\n * the deletion requests are sent. If any deletion fails, the resources are restored in the cache.\n *\n * If the `undoable` mode is enabled, the hook allows the deletions to be undone within a certain\n * period of time. If the undo action is not performed, the resources will be permanently deleted.\n *\n * @example\n * import { useDeleteMany } from 'react-query-manager';\n *\n * type TData = { id: 1, name: 'Test' };\n * const PATH = 'users/{id}/messages';\n *\n * const { delete: deleteMany } = useDeleteMany<typeof PATH, TData>({\n * resourcePath: PATH,\n * });\n *\n * deleteMany({\n * ids: [123, 456],\n * resourceParams: { id: 1 },\n * undoMessage: 'Messages deleted',\n * });\n *\n * @template TPath - The API path as a string.\n * @template TData - The expected shape of the data returned by the API.\n *\n * @param props - The options for the hook.\n * @returns An object with properties, `delete` and `mutation`.\n *\n * `delete` is a function that takes the ID and params of the resource to delete,\n * and calls the mutation function with the necessary data.\n *\n * `mutation` is result `useMutation` without propery `mutate`\n */\nexport const useDeleteMany = <\n TPath extends string,\n TData = any,\n TExtraData = any,\n>(props: Omit<DeleteBase<TPath, TData, 'many', TExtraData>, 'type'>) => {\n return useDeleteBase({ ...props, type: 'many' });\n};\n","import { getQueryClient } from '../../internal/query-client';\nimport { OnlyObject, QueryOneKey } from '../../type';\n\n/**\n * Adds an item to the query cache based on provided data and cache keys.\n *\n * @template TData - The type of data stored in the cache.\n * @param params - The parameters for the function.\n * @param params.data - The new data to add to the corresponding items.\n * @param params.queryKeysOne - Cache keys for single queries that should be updated.\n *\n * @example\n * addItemFromQueryCache({\n * data: { name: 'New Item' },\n * queryKeysOne: [['get-one', 'posts', {}, '1']],\n * });\n */\nexport const addItemToQueryCache = ({\n data,\n queryKeysOne,\n}: {\n data: OnlyObject;\n queryKeysOne?: [QueryOneKey<''>[0], ...any[]][];\n}) => {\n const queryClient = getQueryClient();\n\n if (queryKeysOne) {\n queryKeysOne.forEach((queryKeyOne) => {\n queryClient.setQueryData(queryKeyOne, data);\n });\n }\n};\n","import { InfiniteData } from '