refactor: enforce enum machine codes #18
Frontend verify / verify (pull_request) Canceled after 0s
Frontend security / security (pull_request) Canceled after 0s
Frontend security / security (push) Canceled after 0s
Frontend verify / verify (push) Canceled after 0s
Conventional commits / commits (pull_request) Canceled after 0s

Use owner enums for known codes; preserve wire values and unknown fallbacks.

Verified npm ci and verify: 379 tests, 5 lint checks, app and Storybook builds.
This commit is contained in:
2026-09-28 13:15:27 +03:00
parent bc8e4cc4dc
commit d20be86d73
26 changed files with 248 additions and 79 deletions
+8 -7
View File
@@ -1,6 +1,6 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiClient, apiErrorCode } from "@/shared/api";
import { apiClient, ApiErrorCode, apiErrorCode, isApiErrorCode } from "@/shared/api";
import type {
CreateBankCard,
@@ -34,14 +34,15 @@ function useBankMutations() {
});
const options = {
onError: (error: unknown) => {
const errorCode = apiErrorCode(error);
if (
[
"BANK_DEPENDENT_REFUNDS",
"BANK_INTENT_CONFLICT",
"BANK_REFUND_LIMIT",
"BANK_VERSION_CONFLICT",
"BANK_WRITE_CONFLICT",
].includes(apiErrorCode(error) ?? "")
ApiErrorCode.BankDependentRefunds,
ApiErrorCode.BankIntentConflict,
ApiErrorCode.BankRefundLimit,
ApiErrorCode.BankVersionConflict,
ApiErrorCode.BankWriteConflict,
].some(code => isApiErrorCode(errorCode, code))
)
return invalidate();
},
@@ -0,0 +1,23 @@
import { expect, it } from "vitest";
import { operationLabelKey } from "./display";
it.each([
["OPERATION_TYPE_BUY", "buy"],
["OPERATION_TYPE_BUY_CARD", "buy"],
["OPERATION_TYPE_BUY_MARGIN", "buy"],
["OPERATION_TYPE_SELL", "sell"],
["OPERATION_TYPE_SELL_CARD", "sell"],
["OPERATION_TYPE_SELL_MARGIN", "sell"],
["OPERATION_TYPE_DIV_EXT", "dividend"],
["OPERATION_TYPE_DIVIDEND", "dividend"],
["OPERATION_TYPE_DIVIDEND_TRANSFER", "dividend"],
["OPERATION_TYPE_INPUT_SECURITIES", "input"],
["OPERATION_TYPE_OUTPUT_SECURITIES", "output"],
["OPERATION_TYPE_FUTURE_TAX", "tax"],
["OPERATION_TYPE_FUTURE_FEE", "fee"],
["OPERATION_TYPE_TRANS_FUTURE", "transfer"],
["FUTURE_UNKNOWN_OPERATION", "other"],
])("preserves the wire operation label for %s", (type, label) => {
expect(operationLabelKey(type)).toBe(`ledger.operationType.${label}`);
});
+37 -16
View File
@@ -2,6 +2,23 @@ import { formatMoney } from "@/shared/lib";
import type { BrokerageOperation } from "../api/useBrokerageOperations";
enum OperationType {
Buy = "OPERATION_TYPE_BUY",
BuyCard = "OPERATION_TYPE_BUY_CARD",
BuyMargin = "OPERATION_TYPE_BUY_MARGIN",
Coupon = "OPERATION_TYPE_COUPON",
DivExtension = "OPERATION_TYPE_DIV_EXT",
Dividend = "OPERATION_TYPE_DIVIDEND",
DividendTransfer = "OPERATION_TYPE_DIVIDEND_TRANSFER",
InputSecurities = "OPERATION_TYPE_INPUT_SECURITIES",
OutputSecurities = "OPERATION_TYPE_OUTPUT_SECURITIES",
Sell = "OPERATION_TYPE_SELL",
SellCard = "OPERATION_TYPE_SELL_CARD",
SellMargin = "OPERATION_TYPE_SELL_MARGIN",
}
const operationTransferPrefix = "OPERATION_TYPE_TRANS_";
type PaymentPresentation = {
amount: string;
label: string;
@@ -10,12 +27,16 @@ type PaymentPresentation = {
function entryLabelKey(operation: BrokerageOperation): string {
if (operation.category === "income")
return operation.type === "OPERATION_TYPE_COUPON"
return isOperationType(operation.type, OperationType.Coupon)
? "ledger.entryLabel.income.coupon"
: "ledger.entryLabel.income.dividend";
return `ledger.category.${operation.category}`;
}
function isOperationType(value: unknown, expected: OperationType): boolean {
return value === expected;
}
function isZeroDecimal(value: string): boolean {
return /^-?0$|^-?0\.0+$/.test(value);
}
@@ -23,35 +44,35 @@ function isZeroDecimal(value: string): boolean {
function operationLabelKey(type: string): string {
if (
[
"OPERATION_TYPE_BUY",
"OPERATION_TYPE_BUY_CARD",
"OPERATION_TYPE_BUY_MARGIN",
].includes(type)
OperationType.Buy,
OperationType.BuyCard,
OperationType.BuyMargin,
].some(candidate => isOperationType(type, candidate))
)
return "ledger.operationType.buy";
if (
[
"OPERATION_TYPE_SELL",
"OPERATION_TYPE_SELL_CARD",
"OPERATION_TYPE_SELL_MARGIN",
].includes(type)
OperationType.Sell,
OperationType.SellCard,
OperationType.SellMargin,
].some(candidate => isOperationType(type, candidate))
)
return "ledger.operationType.sell";
if (
[
"OPERATION_TYPE_DIV_EXT",
"OPERATION_TYPE_DIVIDEND",
"OPERATION_TYPE_DIVIDEND_TRANSFER",
].includes(type)
OperationType.DivExtension,
OperationType.Dividend,
OperationType.DividendTransfer,
].some(candidate => isOperationType(type, candidate))
)
return "ledger.operationType.dividend";
if (type.includes("TAX")) return "ledger.operationType.tax";
if (type.includes("FEE")) return "ledger.operationType.fee";
if (type === "OPERATION_TYPE_INPUT_SECURITIES")
if (isOperationType(type, OperationType.InputSecurities))
return "ledger.operationType.input";
if (type === "OPERATION_TYPE_OUTPUT_SECURITIES")
if (isOperationType(type, OperationType.OutputSecurities))
return "ledger.operationType.output";
return type.startsWith("OPERATION_TYPE_TRANS_") ? "ledger.operationType.transfer" : "ledger.operationType.other";
return type.startsWith(operationTransferPrefix) ? "ledger.operationType.transfer" : "ledger.operationType.other";
}
function paymentPresentation(
@@ -6,7 +6,7 @@ import {
type OperationCategory,
useBrokerageOperations,
} from "@/entities/broker-operation";
import { apiErrorCode } from "@/shared/api";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "@/shared/api";
function useInstrumentOperations(
uid: string,
@@ -26,7 +26,7 @@ function useInstrumentOperations(
cursor: cursors.at(-1),
});
const retry = (retryScreen: () => unknown) => {
if (apiErrorCode(operations.error) === "INVALID_OPERATIONS_CURSOR") {
if (isApiErrorCode(apiErrorCode(operations.error), ApiErrorCode.InvalidOperationsCursor)) {
setPagination({ cursors: [], scope });
void client.invalidateQueries({
exact: true,
@@ -2,9 +2,11 @@ import { type QueryKey, useQueryClient } from "@tanstack/react-query";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import {
ApiErrorCode,
apiErrorCode,
apiQueryClient,
invalidateResourceAccess,
isApiErrorCode,
type paths,
useAuthenticatedQueries,
} from "@/shared/api";
@@ -112,7 +114,7 @@ function useScreenSync(scope: InvestmentScreen, resources: ScreenResource[]) {
}
deniedRef.current.add(status.error);
if (apiErrorCode(status.error) !== "NOT_FOUND")
if (!isApiErrorCode(apiErrorCode(status.error), ApiErrorCode.NotFound))
invalidateResourceAccess(client, "brokerage", status.error);
}
const latest = new Map<string, ResourceMeta>();
@@ -13,13 +13,15 @@ import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { apiErrorCode, apiQueryClient } from "@/shared/api";
import { ApiErrorCode, apiErrorCode, apiQueryClient, isApiErrorCode } from "@/shared/api";
import {
commitTbankConnectionBoundary,
type TbankConnectionStatus,
} from "../../model/connection-boundary";
enum ConnectionErrorCode { RequestFailed = "REQUEST_ERROR" }
type TbankConnectionSectionLoadedProps = {
refreshError: boolean;
refreshing: boolean;
@@ -43,7 +45,7 @@ const TbankConnectionSectionLoaded: FC<TbankConnectionSectionLoadedProps> = (
reset,
} = useForm<{ token: string }>();
const update = apiQueryClient.useMutation("put", "/api/v1/tbank-connection", {
onError: error => setRequestError(apiErrorCode(error) ?? "REQUEST_ERROR"),
onError: error => setRequestError(apiErrorCode(error) ?? ConnectionErrorCode.RequestFailed),
onSuccess: (data) => {
commitTbankConnectionBoundary(queryClient, data);
reset();
@@ -62,7 +64,7 @@ const TbankConnectionSectionLoaded: FC<TbankConnectionSectionLoadedProps> = (
},
},
);
const unavailableMessage = requestError === "TBANK_UNAVAILABLE" ? "tbank.unavailable" : "tbank.requestError";
const unavailableMessage = isApiErrorCode(requestError, ApiErrorCode.TbankUnavailable) ? "tbank.unavailable" : "tbank.requestError";
const isPending = update.isPending || remove.isPending;
const isConnected = status.connected;
useEffect(() => {
@@ -119,7 +121,7 @@ const TbankConnectionSectionLoaded: FC<TbankConnectionSectionLoadedProps> = (
{requestError && (
<Alert color="red">
{t(
requestError === "INVALID_TBANK_TOKEN"
isApiErrorCode(requestError, ApiErrorCode.InvalidTbankToken)
? "tbank.invalidToken"
: unavailableMessage,
)}
@@ -11,7 +11,7 @@ import {
useBrokerageOperations,
} from "@/entities/broker-operation";
import { useScreenSync } from "@/features/tbank-connection";
import { apiErrorCode } from "@/shared/api";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "@/shared/api";
type AccountOperationsPageViewModel
= | {
@@ -114,7 +114,7 @@ const useAccountOperationsPageVm = (
const resetCursors = () => setPagination({ cursors: [], scopeKey });
const retry = () => {
if (apiErrorCode(operations.error) === "INVALID_OPERATIONS_CURSOR") {
if (isApiErrorCode(apiErrorCode(operations.error), ApiErrorCode.InvalidOperationsCursor)) {
void queryClient.invalidateQueries({
exact: true,
queryKey: brokerageOperationsQueryOptions(accountId, firstPageQuery)
@@ -136,7 +136,7 @@ const useAccountOperationsPageVm = (
if (
(operations.isError || operations.meta?.sync.state === "failed")
&& (!operations.data
|| apiErrorCode(operations.error) === "INVALID_OPERATIONS_CURSOR")
|| isApiErrorCode(apiErrorCode(operations.error), ApiErrorCode.InvalidOperationsCursor))
)
return { onReset, retry, state: "error" };
if (!operations.data) return { state: "loading" };
@@ -5,12 +5,14 @@ import { StateView } from "@wealthledger/ui";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
type AccountPageErrorProps = { code?: string; retry: () => void };
const AccountPageError: FC<AccountPageErrorProps> = (props) => {
const { code, retry } = props;
const { t } = useTranslation();
if (code === "NOT_FOUND")
if (isApiErrorCode(code, ApiErrorCode.NotFound))
return (
<StateView
action={(
@@ -23,7 +25,7 @@ const AccountPageError: FC<AccountPageErrorProps> = (props) => {
title={t("portfolio.accountUnavailableTitle")}
/>
);
if (code === "TBANK_NOT_CONNECTED" || code === "INVALID_TBANK_TOKEN")
if (isApiErrorCode(code, ApiErrorCode.TbankNotConnected) || isApiErrorCode(code, ApiErrorCode.InvalidTbankToken))
return (
<StateView
action={(
@@ -36,7 +38,7 @@ const AccountPageError: FC<AccountPageErrorProps> = (props) => {
title={t("portfolio.connectionErrorTitle")}
/>
);
if (code === "TBANK_UNAVAILABLE")
if (isApiErrorCode(code, ApiErrorCode.TbankUnavailable))
return (
<StateView
action={<Button onClick={retry}>{t("accounts.retry")}</Button>}
@@ -5,6 +5,8 @@ import { StateView } from "@wealthledger/ui";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
type AccountsPageErrorProps = { code?: string; retry?: () => void };
const AccountsPageError: FC<AccountsPageErrorProps> = (props) => {
@@ -15,7 +17,7 @@ const AccountsPageError: FC<AccountsPageErrorProps> = (props) => {
{t("accounts.openSettings")}
</Button>
);
if (code === "TBANK_NOT_CONNECTED")
if (isApiErrorCode(code, ApiErrorCode.TbankNotConnected))
return (
<StateView
action={settingsAction}
@@ -24,7 +26,7 @@ const AccountsPageError: FC<AccountsPageErrorProps> = (props) => {
title={t("accounts.notConnectedTitle")}
/>
);
if (code === "INVALID_TBANK_TOKEN")
if (isApiErrorCode(code, ApiErrorCode.InvalidTbankToken))
return (
<StateView
action={settingsAction}
@@ -33,7 +35,7 @@ const AccountsPageError: FC<AccountsPageErrorProps> = (props) => {
title={t("accounts.invalidTokenTitle")}
/>
);
if (code === "TBANK_UNAVAILABLE")
if (isApiErrorCode(code, ApiErrorCode.TbankUnavailable))
return (
<StateView
action={(
+5 -3
View File
@@ -5,6 +5,8 @@ import { LoadingSkeleton } from "@wealthledger/ui";
import { useTranslation } from "react-i18next";
import { Link, Navigate } from "react-router";
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
import { BondPageView } from "./BondPage.view";
import { useBondPageVm } from "./BondPage.vm";
@@ -22,15 +24,15 @@ const BondPage: FC = () => {
</Stack>
</LoadingSkeleton>
);
const unavailableMessage = vm.code === "BOND_TICKER_AMBIGUOUS" ? "bond.ambiguousTicker" : "bond.unavailableText";
const unavailableMessage = isApiErrorCode(vm.code, ApiErrorCode.BondTickerAmbiguous) ? "bond.ambiguousTicker" : "bond.unavailableText";
if (!vm.bond)
return (
<Stack role="alert">
<Title order={1}>{t("bond.unavailable")}</Title>
<Text>
{t(
vm.code === "TBANK_NOT_CONNECTED"
|| vm.code === "INVALID_TBANK_TOKEN"
isApiErrorCode(vm.code, ApiErrorCode.TbankNotConnected)
|| isApiErrorCode(vm.code, ApiErrorCode.InvalidTbankToken)
? "bond.connect"
: unavailableMessage,
)}
@@ -18,6 +18,7 @@ import { LoadingSkeleton } from "@wealthledger/ui";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
import {
formatDate,
formatDecimal,
@@ -188,8 +189,8 @@ const BondsPageView: FC<BondsPageViewProps> = (props) => {
<Stack gap="xs">
<Text>
{t(
vm.code === "TBANK_NOT_CONNECTED"
|| vm.code === "INVALID_TBANK_TOKEN"
isApiErrorCode(vm.code, ApiErrorCode.TbankNotConnected)
|| isApiErrorCode(vm.code, ApiErrorCode.InvalidTbankToken)
? "bond.connect"
: "stock.sectionError",
)}
+2 -2
View File
@@ -11,7 +11,7 @@ import {
useBankOperations,
useBankSummary,
} from "@/entities/bank-card";
import { apiErrorCode } from "@/shared/api";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "@/shared/api";
import { formatDate, formatMoney } from "@/shared/lib";
const PAGE_SIZES = new Set(["5", "10", "20", "50", "100", "all"]);
@@ -81,7 +81,7 @@ function useCardPageVm() {
deleting,
editingCard,
history,
notFound: apiErrorCode(card.error) === "BANK_NOT_FOUND",
notFound: isApiErrorCode(apiErrorCode(card.error), ApiErrorCode.BankNotFound),
operation,
page,
pageSize,
@@ -27,6 +27,9 @@ import type {
OperationValues,
} from "./OperationForm.types";
// ZodIssueCode is deprecated; this is the custom refinement code accepted by Zod.
enum FormIssueCode { Custom = "custom" }
const MAX_DESCRIPTION_LENGTH = 500;
const operationSchema = z.object({
@@ -49,13 +52,13 @@ function validateRefund(values: OperationValues, context: z.RefinementCtx,
expense: BankOperation | undefined, limit: BankOperation["availableRefund"] | undefined) {
if (!expense || expense.cardId !== values.cardId)
context.addIssue({
code: "custom",
code: FormIssueCode.Custom,
path: ["originalExpenseId"],
...validationMessage("bank.validation.required")(undefined),
});
if (expense && values.date < expense.date)
context.addIssue({
code: "custom",
code: FormIssueCode.Custom,
path: ["date"],
...validationMessage("bank.validation.start")(undefined),
});
@@ -66,7 +69,7 @@ function validateRefund(values: OperationValues, context: z.RefinementCtx,
&& amount > (moneyCents(limit.value) ?? 0n)
)
context.addIssue({
code: "custom",
code: FormIssueCode.Custom,
path: ["amount"],
...validationMessage("bank.validation.refund")(undefined),
});
@@ -116,7 +119,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
const target = props.cards.find(c => c.id === values.targetCardId);
if (!source || source.archivedAt)
context.addIssue({
code: "custom",
code: FormIssueCode.Custom,
path: ["cardId"],
...validationMessage("bank.validation.required")(undefined),
});
@@ -125,7 +128,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
&& (!target || target.archivedAt || target.id === values.cardId)
)
context.addIssue({
code: "custom",
code: FormIssueCode.Custom,
path: ["targetCardId"],
...validationMessage("bank.validation.destination")(undefined),
});
@@ -136,7 +139,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
&& values.date < target.openingDate)
)
context.addIssue({
code: "custom",
code: FormIssueCode.Custom,
path: ["date"],
...validationMessage("bank.validation.start")(undefined),
});
@@ -152,7 +155,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
)
)
context.addIssue({
code: "custom",
code: FormIssueCode.Custom,
path: ["categoryId"],
...validationMessage("bank.validation.required")(undefined),
});
+5 -3
View File
@@ -5,6 +5,8 @@ import { LoadingSkeleton } from "@wealthledger/ui";
import { useTranslation } from "react-i18next";
import { Link, Navigate } from "react-router";
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
import { StockPageView } from "./StockPage.view";
import { useStockPageVm } from "./StockPage.vm";
@@ -22,15 +24,15 @@ const StockPage: FC = () => {
</Stack>
</LoadingSkeleton>
);
const unavailableMessage = vm.code === "STOCK_TICKER_AMBIGUOUS" ? "stock.ambiguousTicker" : "stock.unavailableText";
const unavailableMessage = isApiErrorCode(vm.code, ApiErrorCode.StockTickerAmbiguous) ? "stock.ambiguousTicker" : "stock.unavailableText";
if (!vm.stock)
return (
<Stack role="alert">
<Title order={1}>{t("stock.unavailable")}</Title>
<Text>
{t(
vm.code === "TBANK_NOT_CONNECTED"
|| vm.code === "INVALID_TBANK_TOKEN"
isApiErrorCode(vm.code, ApiErrorCode.TbankNotConnected)
|| isApiErrorCode(vm.code, ApiErrorCode.InvalidTbankToken)
? "stock.connect"
: unavailableMessage,
)}
@@ -18,6 +18,7 @@ import { LoadingSkeleton } from "@wealthledger/ui";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { ApiErrorCode, isApiErrorCode } from "@/shared/api";
import { formatMoney, formatSnapshotTime } from "@/shared/lib";
import type { StocksPageVm } from "./StocksPage.vm";
@@ -156,8 +157,8 @@ const StocksPageView: FC<StocksPageViewProps> = (props) => {
<Stack gap="xs">
<Text>
{t(
vm.code === "TBANK_NOT_CONNECTED"
|| vm.code === "INVALID_TBANK_TOKEN"
isApiErrorCode(vm.code, ApiErrorCode.TbankNotConnected)
|| isApiErrorCode(vm.code, ApiErrorCode.InvalidTbankToken)
? "stock.connect"
: "stock.sectionError",
)}
+2 -2
View File
@@ -3,7 +3,7 @@ import createQueryClient from "openapi-react-query";
import type { paths } from "./generated/schema";
import { apiErrorCode } from "./problem-details";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
const HTTP_UNAUTHORIZED = 401;
@@ -101,7 +101,7 @@ async function fetchCurrent(
catch {
return response;
}
if (apiErrorCode(problem) !== "UNAUTHORIZED") return response;
if (!isApiErrorCode(apiErrorCode(problem), ApiErrorCode.Unauthorized)) return response;
if (!isCurrent()) throw new DOMException("Session changed", "AbortError");
if (!(await sessionState.handlers.refresh())) {
if (isCurrent()) sessionState.handlers.clear();
+1 -1
View File
@@ -9,7 +9,7 @@ export {
invalidateApiRequests,
} from "./client";
export type { paths } from "./generated/schema";
export { apiErrorCode } from "./problem-details";
export { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
export {
invalidateResourceAccess,
+13
View File
@@ -0,0 +1,13 @@
import { expect, it } from "vitest";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
it("matches known codes and preserves unknown wire codes for fallback handling", () => {
expect(isApiErrorCode(apiErrorCode({ code: "UNAUTHORIZED" }), ApiErrorCode.Unauthorized)).toBe(true);
const unknownCode = apiErrorCode({ code: "FUTURE_PROVIDER_ERROR" });
expect(unknownCode).toBe("FUTURE_PROVIDER_ERROR");
expect(isApiErrorCode(unknownCode, ApiErrorCode.Unauthorized)).toBe(false);
expect(isApiErrorCode(undefined, ApiErrorCode.Unauthorized)).toBe(false);
expect(apiErrorCode(null)).toBeUndefined();
expect(apiErrorCode({})).toBeUndefined();
});
+24 -1
View File
@@ -1,7 +1,30 @@
enum ApiErrorCode {
BankDependentRefunds = "BANK_DEPENDENT_REFUNDS",
BankIntentConflict = "BANK_INTENT_CONFLICT",
BankNotFound = "BANK_NOT_FOUND",
BankRefundLimit = "BANK_REFUND_LIMIT",
BankVersionConflict = "BANK_VERSION_CONFLICT",
BankWriteConflict = "BANK_WRITE_CONFLICT",
BondTickerAmbiguous = "BOND_TICKER_AMBIGUOUS",
Forbidden = "FORBIDDEN",
InvalidOperationsCursor = "INVALID_OPERATIONS_CURSOR",
InvalidTbankToken = "INVALID_TBANK_TOKEN",
NotFound = "NOT_FOUND",
StockTickerAmbiguous = "STOCK_TICKER_AMBIGUOUS",
TbankNotConnected = "TBANK_NOT_CONNECTED",
TbankUnavailable = "TBANK_UNAVAILABLE",
Unauthorized = "UNAUTHORIZED",
}
function apiErrorCode(error: unknown): string | undefined {
return typeof error === "object" && error !== null && "code" in error
? String(error.code)
: undefined;
}
export { apiErrorCode };
// Incoming codes are an open wire string; unknown values retain their fallback.
function isApiErrorCode(value: unknown, expected: ApiErrorCode): boolean {
return value === expected;
}
export { ApiErrorCode, apiErrorCode, isApiErrorCode };
+8 -7
View File
@@ -10,7 +10,7 @@ import type { components } from "./generated/schema";
import { useAuthenticatedQuery } from "./authenticated-query";
import { getAuthenticatedSessionSnapshot } from "./client";
import { apiErrorCode } from "./problem-details";
import { ApiErrorCode, apiErrorCode, isApiErrorCode } from "./problem-details";
const accessEpochs = new WeakMap<QueryClient, Map<string, number>>();
@@ -23,13 +23,14 @@ function invalidateResourceAccess(
group: string,
error: unknown,
) {
const errorCode = apiErrorCode(error);
if (
![
"FORBIDDEN",
"INVALID_TBANK_TOKEN",
"NOT_FOUND",
"TBANK_NOT_CONNECTED",
].includes(apiErrorCode(error) ?? "")
[
ApiErrorCode.Forbidden,
ApiErrorCode.InvalidTbankToken,
ApiErrorCode.NotFound,
ApiErrorCode.TbankNotConnected,
].every(code => !isApiErrorCode(errorCode, code))
)
return;
const epochs = accessEpochs.get(client) ?? new Map<string, number>();