From a3a17d3b1b6c8a6a3d5fbdc5b3175b39280e3e9d Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 4 Nov 2026 10:24:48 +0300 Subject: [PATCH] refactor: share path-root query predicate (WealthLedger/frontend#142) --- src/entities/bank-card/api/mutations.ts | 14 +++------- .../model/connection-boundary.ts | 23 +++++----------- src/shared/api/index.ts | 1 + src/shared/api/query-predicate.test.ts | 26 +++++++++++++++++++ src/shared/api/query-predicate.ts | 18 +++++++++++++ 5 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 src/shared/api/query-predicate.test.ts create mode 100644 src/shared/api/query-predicate.ts diff --git a/src/entities/bank-card/api/mutations.ts b/src/entities/bank-card/api/mutations.ts index 2d46cc6..14d6c63 100644 --- a/src/entities/bank-card/api/mutations.ts +++ b/src/entities/bank-card/api/mutations.ts @@ -5,6 +5,7 @@ import { ApiErrorCode, apiErrorCode, isApiErrorCode, + isGetUnder, } from "@/shared/api"; import type { @@ -26,21 +27,14 @@ async function mutationData( return data; } +const bankCardGetPredicate = isGetUnder(["/api/v1/bank-cards"]); + function useBankMutationOptions() { const queryClient = useQueryClient(); const invalidate = () => queryClient.invalidateQueries({ - predicate: ({ queryKey }) => { - const [method, path] = queryKey; - - return ( - method === "get" - && typeof path === "string" - && (path === "/api/v1/bank-cards" - || path.startsWith("/api/v1/bank-cards/")) - ); - }, + predicate: ({ queryKey }) => bankCardGetPredicate(queryKey), }); return { diff --git a/src/features/tbank-connection/model/connection-boundary.ts b/src/features/tbank-connection/model/connection-boundary.ts index 61c9caa..3c1cf4b 100644 --- a/src/features/tbank-connection/model/connection-boundary.ts +++ b/src/features/tbank-connection/model/connection-boundary.ts @@ -2,19 +2,18 @@ import type { QueryClient } from "@tanstack/react-query"; import { apiQueryClient, + isGetUnder, type paths, } from "@/shared/api"; type TbankConnectionStatus = paths["/api/v1/tbank-connection"]["get"]["responses"][200]["content"]["application/json"]; -function isInvestmentPath(path: string) { - return [ - "/api/v1/brokerage-accounts", - "/api/v1/stocks", - "/api/v1/bonds", - ].some(root => path === root || path.startsWith(`${root}/`)); -} +const investmentGetPredicate = isGetUnder([ + "/api/v1/brokerage-accounts", + "/api/v1/stocks", + "/api/v1/bonds", +]); const tbankConnectionQueryOptions = () => apiQueryClient.queryOptions("get", "/api/v1/tbank-connection"); @@ -27,15 +26,7 @@ function commitTbankConnectionBoundary( ) { queryClient.setQueryData(tbankConnectionQueryKey, status); void queryClient.resetQueries({ - predicate: ({ queryKey }) => { - const [method, path] = queryKey; - - return ( - method === "get" - && typeof path === "string" - && isInvestmentPath(path) - ); - }, + predicate: ({ queryKey }) => investmentGetPredicate(queryKey), }); } diff --git a/src/shared/api/index.ts b/src/shared/api/index.ts index 60bc969..b27dc60 100644 --- a/src/shared/api/index.ts +++ b/src/shared/api/index.ts @@ -13,6 +13,7 @@ export { isApiErrorCode, isBrokerageAccessLoss, } from "./problem-details"; +export { isGetUnder } from "./query-predicate"; export { isReadFailureExhausted, readRetry } from "./read-failure"; export { diff --git a/src/shared/api/query-predicate.test.ts b/src/shared/api/query-predicate.test.ts new file mode 100644 index 0000000..b5357c2 --- /dev/null +++ b/src/shared/api/query-predicate.test.ts @@ -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); + }); +}); diff --git a/src/shared/api/query-predicate.ts b/src/shared/api/query-predicate.ts new file mode 100644 index 0000000..62c450b --- /dev/null +++ b/src/shared/api/query-predicate.ts @@ -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 };