standardize mobile api response parsing with shared helper

This commit is contained in:
echo 2026-03-29 15:48:58 +02:00
parent b1f84722af
commit ff9f3d582a
5 changed files with 43 additions and 90 deletions

View File

@ -38,9 +38,12 @@ describe("recommendations api", () => {
expect(withAuth).toHaveBeenCalledWith("token_1"); 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({ postMock.mockResolvedValue({
data: {
success: true,
data: { id: "rec_2", status: "pending" }, data: { id: "rec_2", status: "pending" },
},
}); });
const result = await generateRecommendation({ userId: "user_1" }, null); const result = await generateRecommendation({ userId: "user_1" }, null);

View File

@ -0,0 +1,14 @@
import { ApiError, handleResponse } from "./responses";
import { type ApiResponse } from "./types";
export function parseApiData<T>(payload: unknown): T {
if (Array.isArray(payload)) {
return payload as T;
}
if (payload && typeof payload === "object" && "success" in payload) {
return handleResponse(payload as ApiResponse<T>);
}
throw new ApiError("Invalid response format");
}

View File

@ -13,4 +13,5 @@ export * from "./recommendations";
export * from "./nutrition"; export * from "./nutrition";
export * from "./hydration"; export * from "./hydration";
export * from "./client"; export * from "./client";
export * from "./helpers";
export * from "./gyms"; export * from "./gyms";

View File

@ -1,5 +1,6 @@
import { isAxiosError } from "axios"; import { isAxiosError } from "axios";
import { apiClient, withAuth } from "./client"; import { apiClient, withAuth } from "./client";
import { parseApiData } from "./helpers";
export interface Notification { export interface Notification {
id: string; id: string;
@ -11,15 +12,6 @@ export interface Notification {
createdAt: Date; createdAt: Date;
} }
interface ApiResponse<T> {
success: boolean;
data: T;
meta?: {
timestamp: string;
count?: number;
};
}
/** /**
* Fetch all notifications for the authenticated user * Fetch all notifications for the authenticated user
*/ */
@ -27,21 +19,12 @@ export async function fetchNotifications(
token: string | null, token: string | null,
): Promise<Notification[]> { ): Promise<Notification[]> {
try { try {
const response = await apiClient.get<ApiResponse<Notification[]>>( const response = await apiClient.get("/api/notifications", withAuth(token));
"/api/notifications", const notifications = parseApiData<Notification[]>(response.data);
withAuth(token), return notifications.map((notification) => ({
);
const result = response.data;
if (result.success && result.data) {
return result.data.map((notification) => ({
...notification, ...notification,
createdAt: new Date(notification.createdAt), createdAt: new Date(notification.createdAt),
})); }));
}
return [];
} catch (error) { } catch (error) {
if (isAxiosError(error) && error.response) { if (isAxiosError(error) && error.response) {
throw new Error( throw new Error(
@ -57,12 +40,12 @@ export async function fetchNotifications(
*/ */
export async function fetchUnreadCount(token: string | null): Promise<number> { export async function fetchUnreadCount(token: string | null): Promise<number> {
try { try {
const response = await apiClient.get<ApiResponse<{ count: number }>>( const response = await apiClient.get(
"/api/notifications/unread-count", "/api/notifications/unread-count",
withAuth(token), withAuth(token),
); );
const result = response.data; const data = parseApiData<{ count: number }>(response.data);
return result.success && result.data ? result.data.count : 0; return data.count;
} catch (error) { } catch (error) {
if (isAxiosError(error) && error.response) { if (isAxiosError(error) && error.response) {
throw new Error(`Failed to fetch unread count: ${error.response.status}`); throw new Error(`Failed to fetch unread count: ${error.response.status}`);
@ -79,29 +62,22 @@ export async function markAsRead(
token: string | null, token: string | null,
): Promise<Notification> { ): Promise<Notification> {
try { try {
const response = await apiClient.put<ApiResponse<Notification>>( const response = await apiClient.put(
`/api/notifications/${notificationId}`, `/api/notifications/${notificationId}`,
{}, {},
withAuth(token), withAuth(token),
); );
const notification = parseApiData<Notification>(response.data);
const result = response.data;
if (result.success && result.data) {
return { return {
...result.data, ...notification,
createdAt: new Date(result.data.createdAt), createdAt: new Date(notification.createdAt),
}; };
}
throw new Error("Invalid response format");
} catch (error) { } catch (error) {
if (isAxiosError(error) && error.response) { if (isAxiosError(error) && error.response) {
throw new Error( throw new Error(
`Failed to mark notification as read: ${error.response.status}`, `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"); throw new Error("Failed to mark notification as read");
} }
} }

View File

@ -1,6 +1,7 @@
import { isAxiosError } from "axios"; import { isAxiosError } from "axios";
import { apiClient, withAuth } from "./client"; import { apiClient, withAuth } from "./client";
import { API_ENDPOINTS } from "../config/api"; import { API_ENDPOINTS } from "../config/api";
import { parseApiData } from "./helpers";
export interface Recommendation { export interface Recommendation {
id: string; id: string;
@ -23,15 +24,6 @@ export interface GenerateRecommendationRequest {
modelProvider?: "openai" | "deepseek" | "ollama"; modelProvider?: "openai" | "deepseek" | "ollama";
} }
interface ApiResponse<T> {
success?: boolean;
data?: T;
}
function isApiResponse<T>(value: unknown): value is ApiResponse<T> {
return typeof value === "object" && value !== null && "success" in value;
}
/** /**
* Get recommendations for a user * Get recommendations for a user
* *
@ -43,34 +35,20 @@ export async function getRecommendations(
userId: string, userId: string,
token: string | null, token: string | null,
): Promise<Recommendation[]> { ): Promise<Recommendation[]> {
let result: ApiResponse<Recommendation[]> | Recommendation[];
try { try {
const response = await apiClient.get(API_ENDPOINTS.RECOMMENDATIONS, { const response = await apiClient.get(API_ENDPOINTS.RECOMMENDATIONS, {
params: { userId }, params: { userId },
...withAuth(token), ...withAuth(token),
}); });
result = response.data; return parseApiData<Recommendation[]>(response.data);
} catch (error) { } catch (error) {
if (isAxiosError(error) && error.response) { if (isAxiosError(error) && error.response) {
throw new Error( throw new Error(
`Failed to fetch recommendations: ${error.response.status}`, `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<Recommendation[]>(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, data: GenerateRecommendationRequest,
token: string | null, token: string | null,
): Promise<Recommendation> { ): Promise<Recommendation> {
let result: ApiResponse<Recommendation> | Recommendation;
try { try {
const response = await apiClient.post( const response = await apiClient.post(
`${API_ENDPOINTS.RECOMMENDATIONS}/generate`, `${API_ENDPOINTS.RECOMMENDATIONS}/generate`,
data, data,
withAuth(token), withAuth(token),
); );
result = response.data; return parseApiData<Recommendation>(response.data);
} catch (error) { } catch (error) {
if (isAxiosError(error) && error.response) { if (isAxiosError(error) && error.response) {
throw new Error( throw new Error(
`Failed to generate recommendation: ${error.response.status}`, `Failed to generate recommendation: ${error.response.status}`,
); );
} }
throw new Error("Failed to generate recommendation"); throw error;
} }
// Handle standardized API response format
if (isApiResponse<Recommendation>(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 recommendationId - Recommendation ID
* @param token - Auth token * @param token - Auth token
* @param approvedBy - User ID of the approver (optional)
* @returns The approved recommendation * @returns The approved recommendation
*/ */
export async function approveRecommendation( export async function approveRecommendation(
recommendationId: string, recommendationId: string,
token: string | null, token: string | null,
): Promise<Recommendation> { ): Promise<Recommendation> {
let result: ApiResponse<Recommendation> | Recommendation;
try { try {
const response = await apiClient.post( const response = await apiClient.post(
`${API_ENDPOINTS.RECOMMENDATIONS}/approve`, `${API_ENDPOINTS.RECOMMENDATIONS}/approve`,
@ -132,21 +99,13 @@ export async function approveRecommendation(
}, },
withAuth(token), withAuth(token),
); );
result = response.data; return parseApiData<Recommendation>(response.data);
} catch (error) { } catch (error) {
if (isAxiosError(error) && error.response) { if (isAxiosError(error) && error.response) {
throw new Error( throw new Error(
`Failed to approve recommendation: ${error.response.status}`, `Failed to approve recommendation: ${error.response.status}`,
); );
} }
throw new Error("Failed to approve recommendation"); throw error;
} }
// Handle standardized API response format
if (isApiResponse<Recommendation>(result) && result.success && result.data) {
return result.data;
}
// Fallback for legacy format
return result as Recommendation;
} }