wip query/mutation hooks
This commit is contained in:
132
src/lib/tanstack-query/hooks/use-server-result.ts
Normal file
132
src/lib/tanstack-query/hooks/use-server-result.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { ServerResult } from '@/lib/tanstack-query/types';
|
||||
|
||||
export function useServerQuery<TData>(
|
||||
options: Omit<UseQueryOptions<TData, Error, TData>, 'queryFn'> & {
|
||||
queryFn: () => Promise<ServerResult<TData>>;
|
||||
showErrorToast?: boolean;
|
||||
}
|
||||
) {
|
||||
const { queryFn, showErrorToast = true, ...queryOptions } = options;
|
||||
|
||||
return useQuery({
|
||||
...queryOptions,
|
||||
queryFn: async () => {
|
||||
const result = await queryFn();
|
||||
|
||||
if (!result.success) {
|
||||
if (showErrorToast) {
|
||||
toast.error(result.error.userMessage);
|
||||
}
|
||||
throw new Error(result.error.userMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useServerSuspenseQuery<TData>(
|
||||
options: Omit<UseQueryOptions<TData, Error, TData>, 'queryFn'> & {
|
||||
queryFn: () => Promise<ServerResult<TData>>;
|
||||
showErrorToast?: boolean;
|
||||
}
|
||||
) {
|
||||
const { queryFn, showErrorToast = true, ...queryOptions } = options;
|
||||
|
||||
return useSuspenseQuery({
|
||||
...queryOptions,
|
||||
queryFn: async () => {
|
||||
const result = await queryFn();
|
||||
|
||||
if (!result.success) {
|
||||
if (showErrorToast) {
|
||||
toast.error(result.error.userMessage);
|
||||
}
|
||||
throw new Error(result.error.userMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useServerMutation<TData, TVariables = unknown>(
|
||||
options: Omit<UseMutationOptions<TData, Error, TVariables>, 'mutationFn'> & {
|
||||
mutationFn: (variables: TVariables) => Promise<ServerResult<TData>>;
|
||||
successMessage?: string;
|
||||
showErrorToast?: boolean;
|
||||
showSuccessToast?: boolean;
|
||||
}
|
||||
) {
|
||||
const {
|
||||
mutationFn,
|
||||
successMessage,
|
||||
showErrorToast = true,
|
||||
showSuccessToast = true,
|
||||
onSuccess,
|
||||
onError,
|
||||
...mutationOptions
|
||||
} = options;
|
||||
|
||||
return useMutation({
|
||||
...mutationOptions,
|
||||
mutationFn: async (variables: TVariables) => {
|
||||
const result = await mutationFn(variables);
|
||||
|
||||
if (!result.success) {
|
||||
if (showErrorToast) {
|
||||
toast.error(result.error.userMessage);
|
||||
}
|
||||
throw new Error(result.error.userMessage);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
},
|
||||
onSuccess: (data, variables, context) => {
|
||||
if (showSuccessToast && successMessage) {
|
||||
toast.success(successMessage);
|
||||
}
|
||||
onSuccess?.(data, variables, context);
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
onError?.(error, variables, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useOptimisticMutation<TData, TVariables = unknown>(
|
||||
options: Parameters<typeof useServerMutation<TData, TVariables>>[0] & {
|
||||
queryKey: readonly (string | number)[];
|
||||
optimisticUpdate?: (oldData: any, variables: TVariables) => any;
|
||||
}
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
const { queryKey, optimisticUpdate, ...mutationOptions } = options;
|
||||
|
||||
return useServerMutation({
|
||||
...mutationOptions,
|
||||
onMutate: async (variables) => {
|
||||
await queryClient.cancelQueries({ queryKey });
|
||||
|
||||
const previousData = queryClient.getQueryData(queryKey);
|
||||
|
||||
if (optimisticUpdate && previousData) {
|
||||
queryClient.setQueryData(queryKey, (old: any) => optimisticUpdate(old, variables));
|
||||
}
|
||||
|
||||
return { previousData };
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
if (context && typeof context === 'object' && 'previousData' in context && context.previousData) {
|
||||
queryClient.setQueryData(queryKey, context.previousData);
|
||||
}
|
||||
mutationOptions.onError?.(error, variables, context);
|
||||
},
|
||||
onSettled: (data, error, variables, context) => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
mutationOptions.onSettled?.(data, error, variables, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
22
src/lib/tanstack-query/types.ts
Normal file
22
src/lib/tanstack-query/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export type ServerResult<T> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: ServerError };
|
||||
|
||||
export type ServerError = {
|
||||
code: ErrorType;
|
||||
message: string;
|
||||
userMessage: string;
|
||||
statusCode?: number;
|
||||
context?: Record<string, any>;
|
||||
};
|
||||
|
||||
export enum ErrorType {
|
||||
VALIDATION = 'VALIDATION',
|
||||
NOT_FOUND = 'NOT_FOUND',
|
||||
UNAUTHORIZED = 'UNAUTHORIZED',
|
||||
FORBIDDEN = 'FORBIDDEN',
|
||||
NETWORK = 'NETWORK',
|
||||
DATABASE = 'DATABASE',
|
||||
POCKETBASE = 'POCKETBASE',
|
||||
UNKNOWN = 'UNKNOWN'
|
||||
}
|
||||
114
src/lib/tanstack-query/utils.ts
Normal file
114
src/lib/tanstack-query/utils.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { logger } from "../logger";
|
||||
import { ErrorType, ServerError, ServerResult } from "./types";
|
||||
|
||||
export const createServerError = (
|
||||
type: ErrorType,
|
||||
message: string,
|
||||
userMessage: string,
|
||||
statusCode?: number,
|
||||
context?: Record<string, any>
|
||||
): ServerError => ({
|
||||
code: type,
|
||||
message,
|
||||
userMessage,
|
||||
statusCode,
|
||||
context,
|
||||
});
|
||||
|
||||
export const withErrorHandling = async <T>(
|
||||
operation: () => Promise<T>,
|
||||
context: string,
|
||||
customErrorMap?: (error: unknown) => ServerError | null
|
||||
): Promise<ServerResult<T>> => {
|
||||
try {
|
||||
const data = await operation();
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
logger.error(`${context} failed`, { error, context });
|
||||
|
||||
if (customErrorMap) {
|
||||
const customError = customErrorMap(error);
|
||||
if (customError) {
|
||||
return { success: false, error: customError };
|
||||
}
|
||||
}
|
||||
|
||||
const mappedError = mapKnownError(error, context);
|
||||
return { success: false, error: mappedError };
|
||||
}
|
||||
};
|
||||
|
||||
const mapKnownError = (error: unknown, context: string): ServerError => {
|
||||
if (error && typeof error === "object" && "status" in error) {
|
||||
const pbError = error as {
|
||||
status: number;
|
||||
message: string;
|
||||
data?: unknown;
|
||||
};
|
||||
|
||||
switch (pbError.status) {
|
||||
case 400:
|
||||
return createServerError(
|
||||
ErrorType.VALIDATION,
|
||||
pbError.message,
|
||||
"Invalid request",
|
||||
400,
|
||||
{ originalError: pbError.data }
|
||||
);
|
||||
case 401:
|
||||
return createServerError(
|
||||
ErrorType.UNAUTHORIZED,
|
||||
pbError.message,
|
||||
"You are not authorized to perform this action",
|
||||
401
|
||||
);
|
||||
case 403:
|
||||
return createServerError(
|
||||
ErrorType.FORBIDDEN,
|
||||
pbError.message,
|
||||
"Access denied",
|
||||
403
|
||||
);
|
||||
case 404:
|
||||
return createServerError(
|
||||
ErrorType.NOT_FOUND,
|
||||
pbError.message,
|
||||
"The requested resource was not found",
|
||||
404
|
||||
);
|
||||
default:
|
||||
return createServerError(
|
||||
ErrorType.POCKETBASE,
|
||||
pbError.message,
|
||||
"Something went wrong. Please try again.",
|
||||
pbError.status
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof TypeError && error.message.includes("fetch")) {
|
||||
return createServerError(
|
||||
ErrorType.NETWORK,
|
||||
error.message,
|
||||
"Network error. Please check your connection and try again."
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return createServerError(
|
||||
ErrorType.UNKNOWN,
|
||||
error.message,
|
||||
"An unexpected error occurred. Please try again.",
|
||||
undefined,
|
||||
{ stack: error.stack, context }
|
||||
);
|
||||
}
|
||||
|
||||
return createServerError(
|
||||
ErrorType.UNKNOWN,
|
||||
String(error),
|
||||
"An unexpected error occurred. Please try again.",
|
||||
undefined,
|
||||
{ originalError: error, context }
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user