UNPKG

wellcrafted

Version:

Delightful TypeScript patterns for elegant, type-safe applications

182 lines (180 loc) 6.03 kB
import { Err, Ok, resolve } from "../result-C_ph_izC.js"; import "../tap-err-BENAkFsH.js"; import "../result-pV2mfn0W.js"; //#region src/query/utils.ts /** * Adapter from a Result-returning query function to TanStack Query options. * * This is the single canonical place where `Result<TQueryFnData, TError>` * is converted into TanStack's contract: `Ok(data)` resolves with `data`, * `Err(error)` throws `error` into the query error channel. * * Use this directly with framework hooks when you do not need the * `QueryClient`-bound imperative helpers from `defineQuery`: * * ```ts * const query = createQuery(() => resultQueryOptions({ * queryKey: ['user', userId], * queryFn: () => services.getUser(userId), * })); * ``` * * `defineQuery` composes through this helper, so the `.options` it returns * is the same shape `resultQueryOptions` produces. * * @param input - Result-aware query configuration * @returns TanStack Query `QueryObserverOptions` with `queryFn` rewired to * resolve `Ok` and throw `Err` */ function resultQueryOptions(input) { return { ...input, queryFn: async (context) => resolve(await input.queryFn(context)) }; } /** * Adapter from a Result-returning mutation function to TanStack Query options. * * This is the single canonical place where `Result<TData, TError>` is * converted into TanStack's contract: `Ok(data)` resolves with `data`, * `Err(error)` throws `error` into the mutation error channel. * * Use this directly with framework hooks when you do not need the * `QueryClient`-bound imperative helpers from `defineMutation`: * * ```ts * const save = createMutation(() => resultMutationOptions({ * mutationKey: ['saveUser'], * mutationFn: (input: SaveUserInput) => services.saveUser(input), * })); * ``` * * `defineMutation` composes through this helper, so the `.options` it * returns is the same shape `resultMutationOptions` produces. * * @param input - Result-aware mutation configuration * @returns TanStack Query `MutationObserverOptions` with `mutationFn` rewired * to resolve `Ok` and throw `Err` */ function resultMutationOptions(input) { return { ...input, mutationFn: async (variables) => resolve(await input.mutationFn(variables)) }; } /** * Creates `defineQuery` and `defineMutation` bound to a specific `QueryClient`. * * Use this when you want a reusable query/mutation definition that carries * its own imperative query helpers (`.fetch`, `.ensure`) and callable mutation * execution powered by a specific client. For local one-shot options that only need * to flow into a framework hook, prefer `resultQueryOptions` / `resultMutationOptions` * directly: those are platform-agnostic and do not require a `QueryClient`. * * Both `defineQuery` and `defineMutation` compose through `resultQueryOptions` and * `resultMutationOptions`, so there is exactly one place that unwraps `Result` * into TanStack's throwing contract. * * @param queryClient - The TanStack `QueryClient` to bind imperative helpers to * * @example * ```ts * const queryClient = new QueryClient(); * const { defineQuery, defineMutation } = createQueryFactories(queryClient); * * const userQuery = defineQuery({ * queryKey: ['user', userId], * queryFn: () => services.getUser(userId), * }); * * // Reactive * const query = createQuery(() => userQuery.options); * * // Imperative * const { data, error } = await userQuery.fetch(); * ``` */ function createQueryFactories(queryClient) { const defineQuery = (input) => { const options = resultQueryOptions(input); async function fetch() { try { return Ok(await queryClient.fetchQuery(options)); } catch (error) { return Err(error); } } async function ensure() { try { return Ok(await queryClient.ensureQueryData(options)); } catch (error) { return Err(error); } } return { options, fetch, ensure }; }; const defineMutation = (input) => { const options = resultMutationOptions(input); async function run(variables) { try { return Ok(await runMutation(queryClient, options, variables)); } catch (error) { return Err(error); } } return Object.assign(run, { options }); }; return { defineQuery, defineMutation }; } /** * Internal helper that executes a mutation directly using the query client's * mutation cache. Powers the callable behavior on mutations returned from * `defineMutation`. * * @internal */ function runMutation(queryClient, options, variables) { const mutation = queryClient.getMutationCache().build(queryClient, options); return mutation.execute(variables); } /** * Identity helper for declaring a TanStack Query key map while preserving * tuple types. * * - **Static entries** like `['users', 'active']` are narrowed to readonly * tuples with full literal precision (e.g. `readonly ['users', 'active']`) * via the `const` type parameter modifier. No per-line `as const` needed. * * - **Factory entries** like `(id: string) => ['users', id]` are narrowed to * tuple SHAPE (e.g. `[string, string]` with correct arity), not widened to * `string[]`. This happens because the strict tuple constraint provides * contextual typing into the function body. Literal positions still widen * without `as const` (TS does not propagate literal narrowing through * contextual typing). Add `as const` to the body when you need the literal: * `(id) => ['users', id] as const` -> `readonly ['users', string]`. * * Empty arrays and non-key values are rejected at the type level. * * @example * ```ts * const userKeys = defineKeys({ * all: ['users'], // readonly ['users'] * active: ['users', 'active'], // readonly ['users', 'active'] * detail: (id: string) => ['users', id], // [string, string] (tuple shape kept) * page: (n: number) => ['users', n] as const, // readonly ['users', number] * }); * ``` */ function defineKeys(keys) { return keys; } //#endregion export { createQueryFactories, defineKeys, resultMutationOptions, resultQueryOptions }; //# sourceMappingURL=index.js.map