From ff9f3d582a4273a7a5a609352b959664adf7c27a Mon Sep 17 00:00:00 2001 From: echo Date: Sun, 29 Mar 2026 15:48:58 +0200 Subject: [PATCH] standardize mobile api response parsing with shared helper --- .../src/api/__tests__/recommendations.test.ts | 7 ++- apps/mobile/src/api/helpers.ts | 14 +++++ apps/mobile/src/api/index.ts | 1 + apps/mobile/src/api/notifications.ts | 56 ++++++------------- apps/mobile/src/api/recommendations.ts | 55 +++--------------- 5 files changed, 43 insertions(+), 90 deletions(-) create mode 100644 apps/mobile/src/api/helpers.ts diff --git a/apps/mobile/src/api/__tests__/recommendations.test.ts b/apps/mobile/src/api/__tests__/recommendations.test.ts index f1cd079..c859f6e 100644 --- a/apps/mobile/src/api/__tests__/recommendations.test.ts +++ b/apps/mobile/src/api/__tests__/recommendations.test.ts @@ -38,9 +38,12 @@ describe("recommendations api", () => { expect(withAuth).toHaveBeenCalledWith("token_1"); }); - it("falls back to legacy response for generate", async () => { + it("returns recommendation from standardized response for generate", async () => { postMock.mockResolvedValue({ - data: { id: "rec_2", status: "pending" }, + data: { + success: true, + data: { id: "rec_2", status: "pending" }, + }, }); const result = await generateRecommendation({ userId: "user_1" }, null); diff --git a/apps/mobile/src/api/helpers.ts b/apps/mobile/src/api/helpers.ts new file mode 100644 index 0000000..c8475b1 --- /dev/null +++ b/apps/mobile/src/api/helpers.ts @@ -0,0 +1,14 @@ +import { ApiError, handleResponse } from "./responses"; +import { type ApiResponse } from "./types"; + +export function parseApiData(payload: unknown): T { + if (Array.isArray(payload)) { + return payload as T; + } + + if (payload && typeof payload === "object" && "success" in payload) { + return handleResponse(payload as ApiResponse); + } + + throw new ApiError("Invalid response format"); +} diff --git a/apps/mobile/src/api/index.ts b/apps/mobile/src/api/index.ts index 528eba6..1776eda 100644 --- a/apps/mobile/src/api/index.ts +++ b/apps/mobile/src/api/index.ts @@ -13,4 +13,5 @@ export * from "./recommendations"; export * from "./nutrition"; export * from "./hydration"; export * from "./client"; +export * from "./helpers"; export * from "./gyms"; diff --git a/apps/mobile/src/api/notifications.ts b/apps/mobile/src/api/notifications.ts index 4ddaf60..6a5a8b8 100644 --- a/apps/mobile/src/api/notifications.ts +++ b/apps/mobile/src/api/notifications.ts @@ -1,5 +1,6 @@ import { isAxiosError } from "axios"; import { apiClient, withAuth } from "./client"; +import { parseApiData } from "./helpers"; export interface Notification { id: string; @@ -11,15 +12,6 @@ export interface Notification { createdAt: Date; } -interface ApiResponse { - success: boolean; - data: T; - meta?: { - timestamp: string; - count?: number; - }; -} - /** * Fetch all notifications for the authenticated user */ @@ -27,21 +19,12 @@ export async function fetchNotifications( token: string | null, ): Promise { try { - const response = await apiClient.get>( - "/api/notifications", - withAuth(token), - ); - - const result = response.data; - - if (result.success && result.data) { - return result.data.map((notification) => ({ - ...notification, - createdAt: new Date(notification.createdAt), - })); - } - - return []; + const response = await apiClient.get("/api/notifications", withAuth(token)); + const notifications = parseApiData(response.data); + return notifications.map((notification) => ({ + ...notification, + createdAt: new Date(notification.createdAt), + })); } catch (error) { if (isAxiosError(error) && error.response) { throw new Error( @@ -57,12 +40,12 @@ export async function fetchNotifications( */ export async function fetchUnreadCount(token: string | null): Promise { try { - const response = await apiClient.get>( + const response = await apiClient.get( "/api/notifications/unread-count", withAuth(token), ); - const result = response.data; - return result.success && result.data ? result.data.count : 0; + const data = parseApiData<{ count: number }>(response.data); + return data.count; } catch (error) { if (isAxiosError(error) && error.response) { throw new Error(`Failed to fetch unread count: ${error.response.status}`); @@ -79,29 +62,22 @@ export async function markAsRead( token: string | null, ): Promise { try { - const response = await apiClient.put>( + const response = await apiClient.put( `/api/notifications/${notificationId}`, {}, withAuth(token), ); - - const result = response.data; - if (result.success && result.data) { - return { - ...result.data, - createdAt: new Date(result.data.createdAt), - }; - } - throw new Error("Invalid response format"); + const notification = parseApiData(response.data); + return { + ...notification, + createdAt: new Date(notification.createdAt), + }; } catch (error) { if (isAxiosError(error) && error.response) { throw new Error( `Failed to mark notification as read: ${error.response.status}`, ); } - if (error instanceof Error && error.message === "Invalid response format") { - throw error; - } throw new Error("Failed to mark notification as read"); } } diff --git a/apps/mobile/src/api/recommendations.ts b/apps/mobile/src/api/recommendations.ts index 3e42eda..a7796d4 100644 --- a/apps/mobile/src/api/recommendations.ts +++ b/apps/mobile/src/api/recommendations.ts @@ -1,6 +1,7 @@ import { isAxiosError } from "axios"; import { apiClient, withAuth } from "./client"; import { API_ENDPOINTS } from "../config/api"; +import { parseApiData } from "./helpers"; export interface Recommendation { id: string; @@ -23,15 +24,6 @@ export interface GenerateRecommendationRequest { modelProvider?: "openai" | "deepseek" | "ollama"; } -interface ApiResponse { - success?: boolean; - data?: T; -} - -function isApiResponse(value: unknown): value is ApiResponse { - return typeof value === "object" && value !== null && "success" in value; -} - /** * Get recommendations for a user * @@ -43,34 +35,20 @@ export async function getRecommendations( userId: string, token: string | null, ): Promise { - let result: ApiResponse | Recommendation[]; try { const response = await apiClient.get(API_ENDPOINTS.RECOMMENDATIONS, { params: { userId }, ...withAuth(token), }); - result = response.data; + return parseApiData(response.data); } catch (error) { if (isAxiosError(error) && error.response) { throw new Error( `Failed to fetch recommendations: ${error.response.status}`, ); } - throw new Error("Failed to fetch recommendations"); + throw error; } - - // Handle standardized API response format - // API returns: { success: true, data: [...], meta: {...} } - if ( - isApiResponse(result) && - result.success && - result.data - ) { - return Array.isArray(result.data) ? result.data : []; - } - - // Fallback for legacy format (direct array) - return Array.isArray(result) ? result : []; } /** @@ -84,30 +62,21 @@ export async function generateRecommendation( data: GenerateRecommendationRequest, token: string | null, ): Promise { - let result: ApiResponse | Recommendation; try { const response = await apiClient.post( `${API_ENDPOINTS.RECOMMENDATIONS}/generate`, data, withAuth(token), ); - result = response.data; + return parseApiData(response.data); } catch (error) { if (isAxiosError(error) && error.response) { throw new Error( `Failed to generate recommendation: ${error.response.status}`, ); } - throw new Error("Failed to generate recommendation"); + throw error; } - - // Handle standardized API response format - if (isApiResponse(result) && result.success && result.data) { - return result.data; - } - - // Fallback for legacy format - return result as Recommendation; } /** @@ -115,14 +84,12 @@ export async function generateRecommendation( * * @param recommendationId - Recommendation ID * @param token - Auth token - * @param approvedBy - User ID of the approver (optional) * @returns The approved recommendation */ export async function approveRecommendation( recommendationId: string, token: string | null, ): Promise { - let result: ApiResponse | Recommendation; try { const response = await apiClient.post( `${API_ENDPOINTS.RECOMMENDATIONS}/approve`, @@ -132,21 +99,13 @@ export async function approveRecommendation( }, withAuth(token), ); - result = response.data; + return parseApiData(response.data); } catch (error) { if (isAxiosError(error) && error.response) { throw new Error( `Failed to approve recommendation: ${error.response.status}`, ); } - throw new Error("Failed to approve recommendation"); + throw error; } - - // Handle standardized API response format - if (isApiResponse(result) && result.success && result.data) { - return result.data; - } - - // Fallback for legacy format - return result as Recommendation; }