import { UseQueryOptions, UseMutationOptions } from "@tanstack/react-query"; // --------------------------------------------------------------------------- // React Query shared types // --------------------------------------------------------------------------- export type QueryConfig = Omit< UseQueryOptions, "queryKey" | "queryFn" >; /** * Query keys a mutation should invalidate on success. Receives the mutation * input and response so keys can be derived from them. Returns a list of query * keys — each is matched as a *prefix* by React Query, so returning a service * root (e.g. `["cargoes"]`) invalidates every query nested under it. * * The keys are surfaced through `mutationOptions().meta.invalidates`; the * app-wide `MutationCache` (see `lib/queryClient.ts`) reads them and invalidates * automatically, so components never wire `onSuccess` invalidation by hand. */ export type InvalidatesFn = ( input: TInput, data: TResponse, ) => ReadonlyArray; /** Shape stored in `mutation.meta.invalidates` and consumed by the MutationCache. */ export type InvalidatesMeta = ( variables: unknown, data: unknown, ) => ReadonlyArray; // --------------------------------------------------------------------------- // Endpoint interfaces // --------------------------------------------------------------------------- export interface EndpointWithInput { call(input: TInput): Promise; queryKey(input: TInput): readonly unknown[]; queryOptions( config: { input: TInput } & QueryConfig, ): UseQueryOptions; } export interface EndpointWithoutInput { call(): Promise; queryKey(): readonly unknown[]; queryOptions( config?: QueryConfig, ): UseQueryOptions; } export type Endpoint = TInput extends void ? EndpointWithoutInput : EndpointWithInput; // --------------------------------------------------------------------------- // Endpoint builder // --------------------------------------------------------------------------- export function endpoint( service: string, action: string, execute: (input: TInput) => Promise, queryKeyBuilder?: (input: TInput) => readonly unknown[], invalidates?: InvalidatesFn, ) { const buildKey = (input?: TInput): readonly unknown[] => { if (queryKeyBuilder && input !== undefined) { return queryKeyBuilder(input as TInput); } return input === undefined ? [service, action] : [service, action, input]; }; const call = (input: TInput) => execute(input); const queryKey = (input?: TInput) => buildKey(input); const queryOptions = ( config?: { input?: TInput } & QueryConfig, ): UseQueryOptions< TResponse, Error, TResponse, readonly unknown[] > => { const { input, ...rest } = config ?? {}; return { ...rest, queryKey: buildKey(input), queryFn: () => execute(input as TInput), }; }; const mutationOptions = ( config?: Omit, "mutationFn">, ): UseMutationOptions => { const meta = invalidates ? { ...config?.meta, invalidates: ((variables, data) => invalidates( variables as TInput, data as TResponse, )) satisfies InvalidatesMeta, } : config?.meta; return { ...config, meta, mutationFn: (variables: TInput): Promise => execute(variables), }; }; return { call, queryKey, queryOptions, mutationOptions }; } // --------------------------------------------------------------------------- // Helper utilities // --------------------------------------------------------------------------- export function unwrap(response: { data: T } | T): T { if ( response && typeof response === "object" && "data" in (response as object) ) { return (response as { data: T }).data; } return response as T; }