standardize mobile api response parsing with shared helper
This commit is contained in:
parent
b1f84722af
commit
ff9f3d582a
@ -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: { id: "rec_2", status: "pending" },
|
data: {
|
||||||
|
success: true,
|
||||||
|
data: { id: "rec_2", status: "pending" },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await generateRecommendation({ userId: "user_1" }, null);
|
const result = await generateRecommendation({ userId: "user_1" }, null);
|
||||||
|
|||||||
14
apps/mobile/src/api/helpers.ts
Normal file
14
apps/mobile/src/api/helpers.ts
Normal 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");
|
||||||
|
}
|
||||||
@ -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";
|
||||||
|
|||||||
@ -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) => ({
|
||||||
);
|
...notification,
|
||||||
|
createdAt: new Date(notification.createdAt),
|
||||||
const result = response.data;
|
}));
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
return result.data.map((notification) => ({
|
|
||||||
...notification,
|
|
||||||
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;
|
return {
|
||||||
if (result.success && result.data) {
|
...notification,
|
||||||
return {
|
createdAt: new Date(notification.createdAt),
|
||||||
...result.data,
|
};
|
||||||
createdAt: new Date(result.data.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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user