Spaces:
Paused
Paused
File size: 5,198 Bytes
4585b20 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | import { getAccessToken } from './authSession';
const DEFAULT_API_BASE_URL = 'https://yousseftallal-fishfarm-backend-api.hf.space/api/v1';
export class ApiClientError extends Error {
readonly status: number;
readonly payload?: unknown;
readonly path: string;
constructor(message: string, status: number, path: string, payload?: unknown) {
super(message);
this.name = 'ApiClientError';
this.status = status;
this.path = path;
this.payload = payload;
}
}
export interface ApiRequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
body?: unknown;
signal?: AbortSignal;
authenticated?: boolean;
headers?: HeadersInit;
baseUrl?: string;
}
type JsonRecord = Record<string, unknown>;
const isRecord = (value: unknown): value is JsonRecord =>
typeof value === 'object' && value !== null && !Array.isArray(value);
const getEnvValue = (key: string): string | undefined => {
const importMetaEnv = (import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env;
const fromVite = importMetaEnv?.[key];
if (typeof fromVite === 'string' && fromVite.trim()) {
return fromVite.trim();
}
if (typeof process !== 'undefined' && typeof process.env?.[key] === 'string' && process.env[key]?.trim()) {
return process.env[key]?.trim();
}
return undefined;
};
export const resolveApiBaseUrl = (baseUrl?: string): string => {
const resolved =
baseUrl?.trim() ||
getEnvValue('VITE_FISH_API_BASE_URL') ||
getEnvValue('FISH_API_BASE_URL') ||
DEFAULT_API_BASE_URL;
return resolved.replace(/\/+$/, '');
};
const parseJsonSafe = (value: string): unknown => {
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
try {
return JSON.parse(trimmed);
} catch {
return undefined;
}
};
const parseErrorMessage = (status: number, payload: unknown): string => {
if (isRecord(payload)) {
const message = payload.message;
if (typeof message === 'string' && message.trim()) {
return message.trim();
}
if (Array.isArray(message)) {
const list = message.filter((item): item is string => typeof item === 'string' && item.trim().length > 0);
if (list.length > 0) {
return list.join(', ');
}
}
if (typeof payload.error === 'string' && payload.error.trim()) {
return payload.error.trim();
}
}
return `Request failed with status ${status}`;
};
const buildHeaders = (authenticated: boolean, extraHeaders?: HeadersInit): HeadersInit => {
const envToken = getEnvValue('FISH_API_TOKEN') || getEnvValue('VITE_FISH_API_TOKEN');
const token = authenticated ? getAccessToken() || envToken || null : null;
return {
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(extraHeaders ?? {}),
};
};
export const requestJson = async <T = unknown>(
path: string,
options: ApiRequestOptions = {},
): Promise<T> => {
const endpoint = `${resolveApiBaseUrl(options.baseUrl)}${path}`;
const response = await fetch(endpoint, {
method: options.method ?? 'GET',
headers: {
...(options.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
...buildHeaders(options.authenticated ?? true, options.headers) as any,
},
...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
signal: options.signal,
});
const raw = await response.text();
const parsed = parseJsonSafe(raw);
if (!response.ok) {
throw new ApiClientError(parseErrorMessage(response.status, parsed), response.status, path, parsed);
}
return parsed as T;
};
export const unwrapApiData = <T>(payload: unknown): T => {
if (isRecord(payload) && 'success' in payload) {
const success = Boolean(payload.success);
if (!success) {
const message = parseErrorMessage(400, payload);
throw new ApiClientError(message, 400, 'wrapped-response', payload);
}
if ('data' in payload) {
return payload.data as T;
}
}
return payload as T;
};
export const asRecord = (value: unknown): JsonRecord | undefined => (isRecord(value) ? value : undefined);
export const asArray = <T = unknown>(value: unknown): T[] =>
Array.isArray(value) ? (value as T[]) : [];
export const asString = (value: unknown): string | undefined => {
if (typeof value !== 'string') {
return undefined;
}
const normalized = value.trim();
return normalized.length > 0 ? normalized : undefined;
};
export const asNumber = (value: unknown): number | undefined => {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string') {
const cleaned = value.replace(/[,%\sA-Za-z]/g, '').trim();
if (!cleaned) {
return undefined;
}
const parsed = Number(cleaned);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return undefined;
};
export const asBoolean = (value: unknown): boolean | undefined => {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
if (value.toLowerCase() === 'true') {
return true;
}
if (value.toLowerCase() === 'false') {
return false;
}
}
return undefined;
};
|