UNPKG

wellcrafted

Version:

Delightful TypeScript patterns for elegant, type-safe applications

255 lines (253 loc) 9.53 kB
import { Err, Ok, resolve } from "../result-BJtW-Wuc.js"; import "../result-DJgTC46e.js"; //#region src/query/utils.ts /** * Creates factory functions for defining queries and mutations bound to a specific QueryClient. * * This factory pattern allows you to create isolated query/mutation definitions that are * bound to a specific QueryClient instance, enabling: * - Multiple query clients in the same application * - Testing with isolated query clients * - Framework-agnostic query definitions * - Proper separation of concerns between query logic and client instances * * The returned functions handle Result types automatically, unwrapping them for TanStack Query * while maintaining type safety throughout your application. * * @param queryClient - The QueryClient instance to bind the factories to * @returns An object containing defineQuery and defineMutation functions bound to the provided client * * @example * ```typescript * // Create your query client * const queryClient = new QueryClient({ * defaultOptions: { * queries: { staleTime: 5 * 60 * 1000 } * } * }); * * // Create the factory functions * const { defineQuery, defineMutation } = createQueryFactories(queryClient); * * // Now use defineQuery and defineMutation as before * const userQuery = defineQuery({ * queryKey: ['user', userId], * resultQueryFn: () => services.getUser(userId) * }); * * // Use in components * const query = createQuery(userQuery.options()); * * // Or imperatively * const { data, error } = await userQuery.fetch(); * ``` */ function createQueryFactories(queryClient) { /** * Creates a query definition that bridges the gap between pure service functions and reactive UI components. * * This factory function is the cornerstone of our data fetching architecture. It wraps service calls * with TanStack Query superpowers while maintaining type safety through Result types. * * ## Why use defineQuery? * * 1. **Dual Interface**: Provides both reactive (`.options()`) and imperative (`.fetch()`) APIs * 2. **Automatic Error Handling**: Service functions return `Result<T, E>` types which are automatically * unwrapped by TanStack Query, giving you proper error states in your components * 3. **Type Safety**: Full TypeScript support with proper inference for data and error types * 4. **Consistency**: Every query in the app follows the same pattern, making it easy to understand * * @template TQueryFnData - The type of data returned by the query function * @template TError - The type of error that can be thrown * @template TData - The type of data returned by the query (after select transform) * @template TQueryKey - The type of the query key * * @param options - Query configuration object * @param options.queryKey - Unique key for this query (used for caching and refetching) * @param options.resultQueryFn - Function that fetches data and returns a Result type * @param options.* - Any other TanStack Query options (staleTime, refetchInterval, etc.) * * @returns Query definition object with three methods: * - `options()`: Returns config for use with createQuery() in Svelte components * - `fetch()`: Always attempts to fetch data (from cache if fresh, network if stale) * - `ensure()`: Guarantees data availability, preferring cached data (recommended for preloaders) * * @example * ```typescript * // Step 1: Define your query in the query layer * const userQuery = defineQuery({ * queryKey: ['users', userId], * resultQueryFn: () => services.getUser(userId), // Returns Result<User, ApiError> * staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes * }); * * // Step 2a: Use reactively in a Svelte component * const query = createQuery(userQuery.options()); * // $query.data is User | undefined * // $query.error is ApiError | null * * // Step 2b: Use imperatively in preloaders (recommended) * export const load = async () => { * const { data, error } = await userQuery.ensure(); * if (error) throw error; * return { user: data }; * }; * * // Step 2c: Use imperatively for explicit refresh * async function refreshUser() { * const { data, error } = await userQuery.fetch(); * if (error) { * console.error('Failed to fetch user:', error); * } * } * ``` */ const defineQuery = (options) => { const newOptions = { ...options, queryFn: async (context) => { let result = options.resultQueryFn(context); if (result instanceof Promise) result = await result; return resolve(result); } }; return { options: () => newOptions, async fetch() { try { return Ok(await queryClient.fetchQuery({ queryKey: newOptions.queryKey, queryFn: newOptions.queryFn })); } catch (error) { return Err(error); } }, async ensure() { try { return Ok(await queryClient.ensureQueryData({ queryKey: newOptions.queryKey, queryFn: newOptions.queryFn })); } catch (error) { return Err(error); } } }; }; /** * Creates a mutation definition for operations that modify data (create, update, delete). * * This factory function is the mutation counterpart to defineQuery. It provides a clean way to * wrap service functions that perform side effects, while maintaining the same dual interface * pattern for maximum flexibility. * * ## Why use defineMutation? * * 1. **Dual Interface**: Just like queries, mutations can be used reactively or imperatively * 2. **Direct Execution**: The `.execute()` method lets you run mutations without creating hooks, * perfect for event handlers and non-component code * 3. **Consistent Error Handling**: Service functions return `Result<T, E>` types, ensuring * errors are handled consistently throughout the app * 4. **Cache Management**: Mutations often update the cache after success (see examples) * * @template TData - The type of data returned by the mutation * @template TError - The type of error that can be thrown * @template TVariables - The type of variables passed to the mutation * @template TContext - The type of context data for optimistic updates * * @param options - Mutation configuration object * @param options.mutationKey - Unique key for this mutation (used for tracking in-flight state) * @param options.resultMutationFn - Function that performs the mutation and returns a Result type * @param options.* - Any other TanStack Mutation options (onSuccess, onError, etc.) * * @returns Mutation definition object with two methods: * - `options()`: Returns config for use with createMutation() in Svelte components * - `execute()`: Directly executes the mutation and returns a Result * * @example * ```typescript * // Step 1: Define your mutation with cache updates * const createRecording = defineMutation({ * mutationKey: ['recordings', 'create'], * resultMutationFn: async (recording: Recording) => { * // Call the service * const result = await services.db.createRecording(recording); * if (result.error) return Err(result.error); * * // Update cache on success * queryClient.setQueryData(['recordings'], (old) => * [...(old || []), recording] * ); * * return Ok(result.data); * } * }); * * // Step 2a: Use reactively in a component * const mutation = createMutation(createRecording.options()); * // Call with: $mutation.mutate(recordingData) * * // Step 2b: Use imperatively in an action * async function saveRecording(data: Recording) { * const { error } = await createRecording.execute(data); * if (error) { * notify.error.execute({ title: 'Failed to save', description: error.message }); * } else { * notify.success.execute({ title: 'Recording saved!' }); * } * } * ``` * * @tip The imperative `.execute()` method is especially useful for: * - Event handlers that need to await the result * - Sequential operations that depend on each other * - Non-component code that needs to trigger mutations */ const defineMutation = (options) => { const newOptions = { ...options, mutationFn: async (variables) => { return resolve(await options.resultMutationFn(variables)); } }; return { options: () => newOptions, async execute(variables) { try { return Ok(await executeMutation(queryClient, newOptions, variables)); } catch (error) { return Err(error); } } }; }; return { defineQuery, defineMutation }; } /** * Internal helper that executes a mutation directly using the query client's mutation cache. * * This is what powers the `.execute()` method on mutations. It bypasses the reactive * mutation hooks and runs the mutation imperatively, which is perfect for event handlers * and other imperative code. * * @internal * @template TData - The type of data returned by the mutation * @template TError - The type of error that can be thrown * @template TVariables - The type of variables passed to the mutation * @template TContext - The type of context data * @param queryClient - The query client instance to use * @param options - The mutation options including mutationFn and mutationKey * @param variables - The variables to pass to the mutation function * @returns Promise that resolves with the mutation result */ function executeMutation(queryClient, options, variables) { const mutation = queryClient.getMutationCache().build(queryClient, options); return mutation.execute(variables); } //#endregion export { createQueryFactories }; //# sourceMappingURL=index.js.map