wellcrafted
Version:
Delightful TypeScript patterns for elegant, type-safe applications
255 lines (253 loc) • 9.53 kB
JavaScript
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