refactor: enforce frontend view model boundaries
Frontend verify / commits (pull_request) Successful in 28s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 8m19s

This commit is contained in:
2026-09-28 18:48:23 +03:00
parent 5b69003516
commit ca0ad4f824
287 changed files with 8558 additions and 4538 deletions
+26 -3
View File
@@ -1,9 +1,11 @@
import { type QueryClient,
import {
type QueryClient,
type QueryFunctionContext,
type QueryKey,
useQueryClient,
type UseQueryOptions,
type UseQueryResult } from "@tanstack/react-query";
type UseQueryResult,
} from "@tanstack/react-query";
import { useEffect } from "react";
import type { components } from "./generated/schema";
@@ -18,6 +20,7 @@ type Resource = {
data: unknown;
meta: components["schemas"]["InvestmentResourceMetaDto"];
};
function invalidateResourceAccess(
client: QueryClient,
group: string,
@@ -36,10 +39,12 @@ function invalidateResourceAccess(
const epochs = accessEpochs.get(client) ?? new Map<string, number>();
accessEpochs.set(client, epochs);
epochs.set(group, (epochs.get(group) ?? 0) + 1);
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({
@@ -57,9 +62,11 @@ function unwrapResourceResult<T extends Resource, E>(
const isMissing = result.isSuccess && result.data.data === null;
const data: T["data"] | undefined = result.data?.data;
const meta: T["meta"] | undefined = result.data?.meta;
const status: UseQueryResult<T, E>["status"] = isMissing
? "pending"
: result.status;
// Project resource fields; refetch still returns the transport envelope.
return {
...result,
@@ -81,17 +88,22 @@ function useResourceQuery<T extends Resource, E, K extends QueryKey>(
options: UseQueryOptions<T, E, T, K>,
) {
const client = useQueryClient();
const group
= typeof options.meta?.accessGroup === "string"
? options.meta.accessGroup
: undefined;
let epochs = accessEpochs.get(client);
if (!epochs) {
epochs = new Map();
accessEpochs.set(client, epochs);
}
const groupEpochs = epochs;
const queryFunction = options.queryFn;
const guarded
= typeof queryFunction === "function"
? async (context: QueryFunctionContext<K>) => {
@@ -99,13 +111,16 @@ function useResourceQuery<T extends Resource, E, K extends QueryKey>(
const result = await queryFunction(context);
if (group && epoch !== (groupEpochs.get(group) ?? 0))
throw new DOMException("Access changed", "AbortError");
return result;
}
: queryFunction;
const query = useAuthenticatedQuery({ ...options, queryFn: guarded });
useEffect(() => {
if (group) invalidateResourceAccess(client, group, query.error);
}, [client, group, query.error]);
const observe = async (expectedStateVersion?: string) => {
const queryFunction = guarded;
if (
@@ -116,13 +131,16 @@ function useResourceQuery<T extends Resource, E, K extends QueryKey>(
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 [method, path, input] = context.queryKey;
const init = input as
| undefined
undefined
| { params?: { query?: Record<string, unknown> } };
const queryKey = [
method,
path,
@@ -134,7 +152,9 @@ function useResourceQuery<T extends Resource, E, K extends QueryKey>(
},
},
] as unknown as K;
const response = await queryFunction({ ...context, queryKey });
if (
expectedStateVersion
&& response.meta.stateVersion !== expectedStateVersion
@@ -143,8 +163,10 @@ function useResourceQuery<T extends Resource, E, K extends QueryKey>(
const current = await queryFunction({ ...context, queryKey });
if (current.meta.stateVersion !== expectedStateVersion)
throw new DOMException("Resource changed", "AbortError");
return current;
}
return response;
},
retry: false,
@@ -153,6 +175,7 @@ function useResourceQuery<T extends Resource, E, K extends QueryKey>(
staleTime: 0,
});
};
return {
...unwrapResourceResult(query),
observe,