refactor: share path-root query predicate (WealthLedger/frontend#142)
Frontend verify / commits (pull_request) Successful in 33s
Frontend verify / security (pull_request) Successful in 13s
Frontend verify / verify (pull_request) Successful in 6m16s

This commit is contained in:
2026-11-04 22:31:55 +03:00
parent 2db39f25d9
commit ce18ceebef
5 changed files with 56 additions and 26 deletions
+4 -10
View File
@@ -5,6 +5,7 @@ import {
ApiErrorCode, ApiErrorCode,
apiErrorCode, apiErrorCode,
isApiErrorCode, isApiErrorCode,
isGetUnder,
} from "@/shared/api"; } from "@/shared/api";
import type { import type {
@@ -26,21 +27,14 @@ async function mutationData<T>(
return data; return data;
} }
const bankCardGetPredicate = isGetUnder(["/api/v1/bank-cards"]);
function useBankMutationOptions() { function useBankMutationOptions() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const invalidate = () => const invalidate = () =>
queryClient.invalidateQueries({ queryClient.invalidateQueries({
predicate: ({ queryKey }) => { predicate: ({ queryKey }) => bankCardGetPredicate(queryKey),
const [method, path] = queryKey;
return (
method === "get"
&& typeof path === "string"
&& (path === "/api/v1/bank-cards"
|| path.startsWith("/api/v1/bank-cards/"))
);
},
}); });
return { return {
@@ -2,19 +2,18 @@ import type { QueryClient } from "@tanstack/react-query";
import { import {
apiQueryClient, apiQueryClient,
isGetUnder,
type paths, type paths,
} from "@/shared/api"; } from "@/shared/api";
type TbankConnectionStatus type TbankConnectionStatus
= paths["/api/v1/tbank-connection"]["get"]["responses"][200]["content"]["application/json"]; = paths["/api/v1/tbank-connection"]["get"]["responses"][200]["content"]["application/json"];
function isInvestmentPath(path: string) { const investmentGetPredicate = isGetUnder([
return [
"/api/v1/brokerage-accounts", "/api/v1/brokerage-accounts",
"/api/v1/stocks", "/api/v1/stocks",
"/api/v1/bonds", "/api/v1/bonds",
].some(root => path === root || path.startsWith(`${root}/`)); ]);
}
const tbankConnectionQueryOptions = () => const tbankConnectionQueryOptions = () =>
apiQueryClient.queryOptions("get", "/api/v1/tbank-connection"); apiQueryClient.queryOptions("get", "/api/v1/tbank-connection");
@@ -27,15 +26,7 @@ function commitTbankConnectionBoundary(
) { ) {
queryClient.setQueryData(tbankConnectionQueryKey, status); queryClient.setQueryData(tbankConnectionQueryKey, status);
void queryClient.resetQueries({ void queryClient.resetQueries({
predicate: ({ queryKey }) => { predicate: ({ queryKey }) => investmentGetPredicate(queryKey),
const [method, path] = queryKey;
return (
method === "get"
&& typeof path === "string"
&& isInvestmentPath(path)
);
},
}); });
} }
+1
View File
@@ -13,6 +13,7 @@ export {
isApiErrorCode, isApiErrorCode,
isBrokerageAccessLoss, isBrokerageAccessLoss,
} from "./problem-details"; } from "./problem-details";
export { isGetUnder } from "./query-predicate";
export { isReadFailureExhausted, readRetry } from "./read-failure"; export { isReadFailureExhausted, readRetry } from "./read-failure";
export { export {
+26
View File
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import { isGetUnder } from "./query-predicate";
describe("isGetUnder", () => {
it("matches the root itself and nested paths for get queries only", () => {
const predicate = isGetUnder(["/api/v1/bank-cards"]);
expect(predicate(["get", "/api/v1/bank-cards"])).toBe(true);
expect(predicate(["get", "/api/v1/bank-cards/{id}"])).toBe(true);
expect(predicate(["get", "/api/v1/bank-cards"])).toBe(true);
expect(predicate(["get", "/api/v1/bank-cardsx"])).toBe(false);
expect(predicate(["post", "/api/v1/bank-cards"])).toBe(false);
expect(predicate(["get", "/api/v1/other"])).toBe(false);
});
it("checks every root and tolerates init and non-string parts", () => {
const predicate = isGetUnder(["/api/v1/stocks", "/api/v1/bonds"]);
expect(predicate(["get", "/api/v1/stocks/{uid}/quote", { params: {} }]))
.toBe(true);
expect(predicate(["get", "/api/v1/bonds"])).toBe(true);
expect(predicate([undefined, "/api/v1/stocks"])).toBe(false);
expect(predicate(["get", undefined])).toBe(false);
});
});
+18
View File
@@ -0,0 +1,18 @@
import type { QueryKey } from "@tanstack/react-query";
// openapi-react-query shapes query keys as [method, path, init].
function isGetUnder(roots: readonly string[]) {
const predicate = (queryKey: QueryKey) => {
const [method, path] = queryKey as readonly [unknown, unknown];
return (
method === "get"
&& typeof path === "string"
&& roots.some(root => path === root || path.startsWith(`${root}/`))
);
};
return predicate;
}
export { isGetUnder };