refactor: simplify frontend state and mutation boundaries
Frontend verify / commits (pull_request) Successful in 31s
Frontend verify / security (pull_request) Successful in 11s
Frontend verify / verify (pull_request) Successful in 9m1s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 9m59s

This commit was merged in pull request #44.
This commit is contained in:
2026-09-28 20:01:37 +03:00
parent a4046dbe87
commit a7537da344
25 changed files with 414 additions and 547 deletions
+25 -17
View File
@@ -6,7 +6,13 @@ import { apiClient, configureAuthClient } from "@/shared/api";
import { createQueryWrapper } from "@/test/query";
import { server } from "@/test/server";
import { useBankMutations } from "./mutations";
import {
useCreateBankOperation,
useDeleteBankOperation,
useUpdateBankCard,
useUpdateBankOperation,
useUpdateExpenseCategory,
} from "./mutations";
import {
type BankCard,
type CreateBankOperation,
@@ -111,7 +117,7 @@ function useReadModels() {
const original = useBankOperation("original");
return {
mutations: useBankMutations(),
createOperation: useCreateBankOperation(),
reads: [
cards,
a,
@@ -217,11 +223,11 @@ describe("banking API", () => {
}),
);
const { result } = renderHook(useBankMutations, {
const { result } = renderHook(useCreateBankOperation, {
wrapper: createQueryWrapper().wrapper,
});
await act(() => result.current.createOperation.mutateAsync(transfer));
await act(() => result.current.mutateAsync(transfer));
expect(bodies).toEqual([transfer, transfer]);
expect(headers).toEqual(["Bearer expired", "Bearer renewed"]);
expect(refresh).toHaveBeenCalledTimes(1);
@@ -239,17 +245,17 @@ describe("banking API", () => {
}),
);
const { result } = renderHook(useBankMutations, {
const { result } = renderHook(useCreateBankOperation, {
wrapper: createQueryWrapper().wrapper,
});
await act(async () => {
await expect(
result.current.createOperation.mutateAsync(transfer),
result.current.mutateAsync(transfer),
).rejects.toThrow();
});
expect(bodies).toEqual([transfer]);
await act(() => result.current.createOperation.mutateAsync(transfer));
await act(() => result.current.mutateAsync(transfer));
expect(bodies).toEqual([transfer, transfer]);
});
@@ -266,20 +272,20 @@ describe("banking API", () => {
);
const { result } = renderHook(
() => ({ mutations: useBankMutations(), query: useBankCard("a") }),
() => ({ createOperation: useCreateBankOperation(), query: useBankCard("a") }),
{ wrapper: createQueryWrapper().wrapper },
);
await waitFor(() => expect(result.current.query.isSuccess).toBe(true));
let saving: Promise<unknown>;
act(() => {
saving = result.current.mutations.createOperation
saving = result.current.createOperation
.mutateAsync(transfer)
// eslint-disable-next-line unicorn/prefer-await -- Attach the handler immediately and retain the pending promise for queue/race ordering.
.catch(() => {});
});
await waitFor(() =>
expect(result.current.mutations.createOperation.isPending).toBe(true),
expect(result.current.createOperation.isPending).toBe(true),
);
expect(result.current.query.data?.balance.value).toBe("100");
await act(async () => {
@@ -313,7 +319,7 @@ describe("banking API", () => {
expect(result.current.reads.every(read => read.isSuccess)).toBe(true),
);
await act(() =>
result.current.mutations.createOperation.mutateAsync(transfer),
result.current.createOperation.mutateAsync(transfer),
);
expect(counts.size).toBe(9);
for (const count of counts.values()) expect(count).toBe(2);
@@ -336,14 +342,14 @@ describe("banking API", () => {
);
const { result } = renderHook(
() => ({ card: useBankCard("a"), mutations: useBankMutations() }),
() => ({ card: useBankCard("a"), updateCard: useUpdateBankCard() }),
{ wrapper: createQueryWrapper().wrapper },
);
await waitFor(() => expect(result.current.card.data?.version).toBe(1));
await act(async () => {
await expect(
result.current.mutations.updateCard.mutateAsync({
result.current.updateCard.mutateAsync({
body: { name: "changed", version: 1 },
id: "a",
}),
@@ -389,7 +395,9 @@ describe("banking API", () => {
const { result } = renderHook(
() => ({
categories: useBankCategories(),
mutations: useBankMutations(),
deleteOperation: useDeleteBankOperation(),
updateCategory: useUpdateExpenseCategory(),
updateOperation: useUpdateBankOperation(),
}),
{ wrapper: createQueryWrapper().wrapper },
);
@@ -398,7 +406,7 @@ describe("banking API", () => {
expect(result.current.categories.data?.[0].name).toBe("Food"),
);
await act(() =>
result.current.mutations.updateCategory.mutateAsync({
result.current.updateCategory.mutateAsync({
body: { name: "Groceries", version: 1 },
id: "food",
}),
@@ -407,13 +415,13 @@ describe("banking API", () => {
expect(result.current.categories.data?.[0].name).toBe("Groceries"),
);
await act(() =>
result.current.mutations.updateOperation.mutateAsync({
result.current.updateOperation.mutateAsync({
body: { description: "corrected", version: 1 },
id: "original",
}),
);
await act(() =>
result.current.mutations.deleteOperation.mutateAsync({
result.current.deleteOperation.mutateAsync({
id: "original",
version: 2,
}),
+63 -37
View File
@@ -26,7 +26,7 @@ async function mutationData<T>(
return data;
}
function useBankMutations() {
function useBankMutationOptions() {
const queryClient = useQueryClient();
const invalidate = () =>
@@ -43,7 +43,7 @@ function useBankMutations() {
},
});
const options = {
return {
onError: (error: unknown) => {
const errorCode = apiErrorCode(error);
if (
@@ -60,14 +60,56 @@ function useBankMutations() {
onSuccess: invalidate,
retry: false,
} as const;
}
const cardCreation = useMutation({
function useCreateBankCard() {
const options = useBankMutationOptions();
return useMutation({
...options,
mutationFn: (body: CreateBankCard) =>
mutationData(apiClient.POST("/api/v1/bank-cards", { body })),
});
}
const updateCard = useMutation({
function useCreateBankOperation() {
const options = useBankMutationOptions();
return useMutation({
...options,
mutationFn: (body: CreateBankOperation) =>
mutationData(apiClient.POST("/api/v1/bank-cards/operations", { body })),
});
}
function useCreateExpenseCategory() {
const options = useBankMutationOptions();
return useMutation({
...options,
mutationFn: (body: CreateExpenseCategory) =>
mutationData(apiClient.POST("/api/v1/bank-cards/categories", { body })),
});
}
function useDeleteBankOperation() {
const options = useBankMutationOptions();
return useMutation({
...options,
mutationFn: ({ id, version }: { id: string; version: number }) =>
mutationData(
apiClient.DELETE("/api/v1/bank-cards/operations/{id}", {
params: { path: { id }, query: { version } },
}),
),
});
}
function useUpdateBankCard() {
const options = useBankMutationOptions();
return useMutation({
...options,
mutationFn: ({ body, id }: { body: UpdateBankCard; id: string }) =>
mutationData(
@@ -77,14 +119,12 @@ function useBankMutations() {
}),
),
});
}
const operationCreation = useMutation({
...options,
mutationFn: (body: CreateBankOperation) =>
mutationData(apiClient.POST("/api/v1/bank-cards/operations", { body })),
});
function useUpdateBankOperation() {
const options = useBankMutationOptions();
const updateOperation = useMutation({
return useMutation({
...options,
mutationFn: ({ body, id }: { body: UpdateBankOperation; id: string }) =>
mutationData(
@@ -94,24 +134,12 @@ function useBankMutations() {
}),
),
});
}
const operationDeletion = useMutation({
...options,
mutationFn: ({ id, version }: { id: string; version: number }) =>
mutationData(
apiClient.DELETE("/api/v1/bank-cards/operations/{id}", {
params: { path: { id }, query: { version } },
}),
),
});
function useUpdateExpenseCategory() {
const options = useBankMutationOptions();
const categoryCreation = useMutation({
...options,
mutationFn: (body: CreateExpenseCategory) =>
mutationData(apiClient.POST("/api/v1/bank-cards/categories", { body })),
});
const updateCategory = useMutation({
return useMutation({
...options,
mutationFn: ({ body, id }: { body: UpdateExpenseCategory; id: string }) =>
mutationData(
@@ -121,16 +149,14 @@ function useBankMutations() {
}),
),
});
return {
createCard: cardCreation,
createCategory: categoryCreation,
createOperation: operationCreation,
deleteOperation: operationDeletion,
updateCard,
updateCategory,
updateOperation,
};
}
export { useBankMutations };
export {
useCreateBankCard,
useCreateBankOperation,
useCreateExpenseCategory,
useDeleteBankOperation,
useUpdateBankCard,
useUpdateBankOperation,
useUpdateExpenseCategory,
};