Files
edr-platform/apps/edr-freight-web/backoffice/src/utils/endpoint.ts
2026-06-22 11:52:59 +00:00

145 lines
4.2 KiB
TypeScript

import {
UseQueryOptions,
UseMutationOptions
} from "@tanstack/react-query";
// ---------------------------------------------------------------------------
// React Query shared types
// ---------------------------------------------------------------------------
export type QueryConfig<T> = Omit<
UseQueryOptions<T, Error, T, readonly unknown[]>,
"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<TInput, TResponse> = (
input: TInput,
data: TResponse,
) => ReadonlyArray<readonly unknown[]>;
/** Shape stored in `mutation.meta.invalidates` and consumed by the MutationCache. */
export type InvalidatesMeta = (
variables: unknown,
data: unknown,
) => ReadonlyArray<readonly unknown[]>;
// ---------------------------------------------------------------------------
// Endpoint interfaces
// ---------------------------------------------------------------------------
export interface EndpointWithInput<TInput, TResponse> {
call(input: TInput): Promise<TResponse>;
queryKey(input: TInput): readonly unknown[];
queryOptions(
config: { input: TInput } & QueryConfig<TResponse>,
): UseQueryOptions<TResponse, Error, TResponse, readonly unknown[]>;
}
export interface EndpointWithoutInput<TResponse> {
call(): Promise<TResponse>;
queryKey(): readonly unknown[];
queryOptions(
config?: QueryConfig<TResponse>,
): UseQueryOptions<TResponse, Error, TResponse, readonly unknown[]>;
}
export type Endpoint<TInput, TResponse> = TInput extends void
? EndpointWithoutInput<TResponse>
: EndpointWithInput<TInput, TResponse>;
// ---------------------------------------------------------------------------
// Endpoint builder
// ---------------------------------------------------------------------------
export function endpoint<TInput, TResponse>(
service: string,
action: string,
execute: (input: TInput) => Promise<TResponse>,
queryKeyBuilder?: (input: TInput) => readonly unknown[],
invalidates?: InvalidatesFn<TInput, TResponse>,
) {
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<TResponse>,
): UseQueryOptions<
TResponse,
Error,
TResponse,
readonly unknown[]
> => {
const { input, ...rest } = config ?? {};
return {
...rest,
queryKey: buildKey(input),
queryFn: () => execute(input as TInput),
};
};
const mutationOptions = (
config?: Omit<UseMutationOptions<TResponse, Error, TInput>, "mutationFn">,
): UseMutationOptions<TResponse, Error, TInput> => {
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<TResponse> => execute(variables),
};
};
return {
call,
queryKey,
queryOptions,
mutationOptions
};
}
// ---------------------------------------------------------------------------
// Helper utilities
// ---------------------------------------------------------------------------
export function unwrap<T>(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;
}