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