Files
edr-platform/apps/edr-freight-web/backoffice/src/utils/endpoint.ts
Marshal 40904049cf feat: implement consolidation approval process for shared-wagon bookings
- Add migration for consolidation approvals table and status enum
- Create ConsolidationApprovalService to handle approval logic
- Implement repository for managing consolidation approvals
- Add entity for consolidation approval with necessary fields
- Develop frontend components for displaying and managing consolidation approvals
- Create tests for consolidation approval service to ensure correct behavior
2026-08-18 13:17:55 +00:00

181 lines
5.6 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[]>;
/**
* Cache entries a mutation can write DIRECTLY from its own response, skipping
* a refetch. Many endpoints already return the fresh entity they just changed
* (e.g. every train-builder mutation returns the whole `TrainComposition`), so
* re-fetching that same key is a wasted round-trip and a visible flicker.
*
* Returned pairs are written with `setQueryData` by the app-wide MutationCache
* BEFORE the `invalidates` keys are invalidated, and any key seeded here is
* skipped by that invalidation pass — the value just written IS the fresh one.
*/
export type UpdatesFn<TInput, TResponse> = (
input: TInput,
data: TResponse,
) => ReadonlyArray<readonly [readonly unknown[], unknown]>;
/** Shape stored in `mutation.meta.updates` and consumed by the MutationCache. */
export type UpdatesMeta = (
variables: unknown,
data: unknown,
) => ReadonlyArray<readonly [readonly unknown[], 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>,
updates?: UpdatesFn<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 || updates
? {
...config?.meta,
...(invalidates
? {
invalidates: ((variables, data) =>
invalidates(
variables as TInput,
data as TResponse,
)) satisfies InvalidatesMeta,
}
: {}),
...(updates
? {
updates: ((variables, data) =>
updates(
variables as TInput,
data as TResponse,
)) satisfies UpdatesMeta,
}
: {}),
}
: 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;
}