156 lines
4.1 KiB
TypeScript
156 lines
4.1 KiB
TypeScript
import {
|
|
type QueryClient,
|
|
type QueryFunctionContext,
|
|
useQueryClient,
|
|
type UseQueryOptions,
|
|
} from "@tanstack/react-query";
|
|
import { useEffect } from "react";
|
|
|
|
import type { components, paths } from "./generated/schema";
|
|
|
|
import { useAuthenticatedQuery } from "./authenticated-query";
|
|
import { getAuthenticatedSessionSnapshot } from "./client";
|
|
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
|
|
|
|
type InvestmentResourceMeta = components["schemas"]["InvestmentResourceMetaDto"];
|
|
|
|
type Resource = {
|
|
[key: string]: unknown;
|
|
data: unknown;
|
|
meta: InvestmentResourceMeta;
|
|
sync?: unknown;
|
|
};
|
|
|
|
type ResourcePath = {
|
|
[P in keyof paths]: paths[P]["get"] extends {
|
|
// eslint-disable-next-line @typescript-eslint/no-magic-numbers -- HTTP success response in the generated contract.
|
|
responses: { 200: { content: { "application/json": Resource } } };
|
|
} ? P : never;
|
|
}[keyof paths];
|
|
|
|
type ResourceQueryKey = readonly [
|
|
"get",
|
|
ResourcePath,
|
|
{ params?: { path?: object; query?: { sync?: "observe" } } }?,
|
|
];
|
|
|
|
function invalidateResourceAccess(
|
|
client: QueryClient,
|
|
group: string,
|
|
error: unknown,
|
|
) {
|
|
const errorCode = apiErrorCode(error);
|
|
if (
|
|
[
|
|
ApiErrorCode.Forbidden,
|
|
ApiErrorCode.InvalidTbankToken,
|
|
ApiErrorCode.NotFound,
|
|
ApiErrorCode.TbankNotConnected,
|
|
].every(code => !isApiErrorCode(errorCode, code))
|
|
)
|
|
return;
|
|
|
|
const affected = {
|
|
predicate: (cached: { meta?: Record<string, unknown> }) =>
|
|
cached.meta?.accessGroup === group,
|
|
};
|
|
|
|
void client.cancelQueries(affected);
|
|
for (const cached of client.getQueryCache().findAll(affected))
|
|
cached.setState({
|
|
data: undefined,
|
|
dataUpdatedAt: 0,
|
|
error: error as Error,
|
|
fetchStatus: "idle",
|
|
status: "error",
|
|
});
|
|
}
|
|
|
|
// Keep endpoint and parameters intact. The generated queryFn accepts the
|
|
// optional sync mode even for a cache key originally created without init.
|
|
function observationKey<K extends readonly unknown[]>(key: K): K {
|
|
const [method, path, init] = key as unknown as [
|
|
string,
|
|
string,
|
|
{ params?: { path?: object; query?: object } }?,
|
|
];
|
|
|
|
return [method, path, {
|
|
...init,
|
|
params: { ...init?.params, query: { ...init?.params?.query, sync: "observe" } },
|
|
}] as unknown as K;
|
|
}
|
|
|
|
// Observation changes transport mode, never the generated cache identity.
|
|
function useResourceQuery<
|
|
T extends Resource,
|
|
E = unknown,
|
|
K extends readonly unknown[] = ResourceQueryKey,
|
|
>(
|
|
options: UseQueryOptions<T, E, T, K>,
|
|
) {
|
|
const client = useQueryClient();
|
|
|
|
const group
|
|
= typeof options.meta?.accessGroup === "string"
|
|
? options.meta.accessGroup
|
|
: undefined;
|
|
|
|
const query = useAuthenticatedQuery(options);
|
|
useEffect(() => {
|
|
if (group) invalidateResourceAccess(client, group, query.error);
|
|
}, [client, group, query.error]);
|
|
|
|
const observe = async () => {
|
|
const queryFunction = options.queryFn;
|
|
if (
|
|
typeof queryFunction !== "function"
|
|
|| !query.isEnabled
|
|
|| !getAuthenticatedSessionSnapshot()
|
|
)
|
|
return;
|
|
// Cancellation is synchronous; start the replacement in the same boundary.
|
|
void client.cancelQueries({ exact: true, queryKey: options.queryKey });
|
|
|
|
return client.query({
|
|
...options,
|
|
queryFn: async (context: QueryFunctionContext<K>) => {
|
|
const queryKey = observationKey(context.queryKey);
|
|
|
|
return queryFunction({ ...context, queryKey });
|
|
},
|
|
retry: false,
|
|
// Keep the cache envelope independent of observer projection.
|
|
select: undefined,
|
|
staleTime: 0,
|
|
});
|
|
};
|
|
|
|
const data: T["data"] | undefined = query.data?.data;
|
|
const meta: T["meta"] | undefined = query.data?.meta;
|
|
|
|
// Project resource fields; refetch still returns the transport envelope.
|
|
return {
|
|
data: data ?? undefined,
|
|
error: query.error,
|
|
fetchStatus: query.fetchStatus,
|
|
isEnabled: query.isEnabled,
|
|
isError: query.isError,
|
|
isFetching: query.isFetching,
|
|
isLoading: query.isLoading,
|
|
isPending: query.isPending,
|
|
isSuccess: query.isSuccess,
|
|
meta,
|
|
observe,
|
|
queryKey: options.queryKey,
|
|
refetch: query.refetch,
|
|
status: query.status,
|
|
sync: query.data && "sync" in query.data ? query.data.sync : undefined,
|
|
};
|
|
}
|
|
|
|
export {
|
|
type InvestmentResourceMeta,
|
|
useResourceQuery,
|
|
};
|