mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 23:26:41 +02:00
✨ feat: add @manacore/shared-api-client package
Create unified API client for all web apps with: - createApiClient factory function - ApiResult<T> Go-style error handling - HTTP methods: get, post, put, patch, delete, upload - Auto token handling via getAuthToken callback - Timeout support with AbortController - Retry logic with exponential backoff - Runtime URL injection for Docker - FormData support for file uploads Migrate clock app as proof of concept: - Replace local fetchApi with shared createApiClient - Update stores to use ApiError.message Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2b3210df85
commit
e23d1194d8
12 changed files with 562 additions and 292 deletions
|
|
@ -33,6 +33,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@clock/shared": "workspace:*",
|
||||
"@manacore/shared-api-client": "workspace:*",
|
||||
"@manacore/shared-auth": "workspace:*",
|
||||
"@manacore/shared-auth-ui": "workspace:*",
|
||||
"@manacore/shared-branding": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,80 +1,26 @@
|
|||
/**
|
||||
* API Client for Clock backend
|
||||
* Uses @manacore/shared-api-client for consistent error handling
|
||||
*/
|
||||
|
||||
import { createApiClient, type ApiResult } from '@manacore/shared-api-client';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
|
||||
const API_URL = 'http://localhost:3017/api/v1';
|
||||
const API_URL = 'http://localhost:3017';
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
/**
|
||||
* Clock API client instance
|
||||
* - Auto token handling via authStore.getValidToken()
|
||||
* - Consistent ApiResult<T> response format
|
||||
* - Automatic retry on server errors (configurable)
|
||||
*/
|
||||
export const api = createApiClient({
|
||||
baseUrl: API_URL,
|
||||
apiPrefix: '/api/v1',
|
||||
getAuthToken: () => authStore.getValidToken(),
|
||||
timeout: 30000,
|
||||
debug: import.meta.env.DEV,
|
||||
});
|
||||
|
||||
export async function fetchApi<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
const token = await authStore.getAccessToken();
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {}),
|
||||
};
|
||||
|
||||
if (token) {
|
||||
(headers as Record<string, string>)['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
return {
|
||||
error: errorData.message || `HTTP error ${response.status}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle 204 No Content
|
||||
if (response.status === 204) {
|
||||
return { data: undefined as T };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { data };
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : 'Network error',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
export const api = {
|
||||
get: <T>(endpoint: string) => fetchApi<T>(endpoint, { method: 'GET' }),
|
||||
|
||||
post: <T>(endpoint: string, body?: unknown) =>
|
||||
fetchApi<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
|
||||
put: <T>(endpoint: string, body?: unknown) =>
|
||||
fetchApi<T>(endpoint, {
|
||||
method: 'PUT',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
|
||||
patch: <T>(endpoint: string, body?: unknown) =>
|
||||
fetchApi<T>(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
|
||||
delete: <T>(endpoint: string) => fetchApi<T>(endpoint, { method: 'DELETE' }),
|
||||
};
|
||||
// Re-export types for convenience
|
||||
export type { ApiResult };
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ export const alarmsStore = {
|
|||
const response = await api.get<Alarm[]>('/alarms');
|
||||
|
||||
if (response.error) {
|
||||
error = response.error;
|
||||
error = response.error.message;
|
||||
loading = false;
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
alarms = response.data || [];
|
||||
|
|
@ -73,7 +73,7 @@ export const alarmsStore = {
|
|||
const response = await api.post<Alarm>('/alarms', input);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
|
|
@ -101,7 +101,7 @@ export const alarmsStore = {
|
|||
const response = await api.patch<Alarm>(`/alarms/${id}`, input);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
|
|
@ -136,7 +136,7 @@ export const alarmsStore = {
|
|||
const response = await api.delete(`/alarms/${id}`);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
alarms = alarms.filter((a) => a.id !== id);
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ export const timersStore = {
|
|||
const response = await api.get<Timer[]>('/timers');
|
||||
|
||||
if (response.error) {
|
||||
error = response.error;
|
||||
error = response.error.message;
|
||||
loading = false;
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
timers = response.data || [];
|
||||
|
|
@ -73,7 +73,7 @@ export const timersStore = {
|
|||
const response = await api.post<Timer>('/timers', input);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
|
|
@ -101,7 +101,7 @@ export const timersStore = {
|
|||
const response = await api.patch<Timer>(`/timers/${id}`, input);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
|
|
@ -129,7 +129,7 @@ export const timersStore = {
|
|||
const response = await api.post<Timer>(`/timers/${id}/start`);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
|
|
@ -157,7 +157,7 @@ export const timersStore = {
|
|||
const response = await api.post<Timer>(`/timers/${id}/pause`);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
|
|
@ -185,7 +185,7 @@ export const timersStore = {
|
|||
const response = await api.post<Timer>(`/timers/${id}/reset`);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
|
|
@ -210,7 +210,7 @@ export const timersStore = {
|
|||
const response = await api.delete(`/timers/${id}`);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
timers = timers.filter((t) => t.id !== id);
|
||||
|
|
|
|||
|
|
@ -32,9 +32,9 @@ export const worldClocksStore = {
|
|||
const response = await api.get<WorldClock[]>('/world-clocks');
|
||||
|
||||
if (response.error) {
|
||||
error = response.error;
|
||||
error = response.error.message;
|
||||
loading = false;
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
worldClocks = response.data || [];
|
||||
|
|
@ -49,7 +49,7 @@ export const worldClocksStore = {
|
|||
const response = await api.post<WorldClock>('/world-clocks', input);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
|
|
@ -65,7 +65,7 @@ export const worldClocksStore = {
|
|||
const response = await api.delete(`/world-clocks/${id}`);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
worldClocks = worldClocks.filter((wc) => wc.id !== id);
|
||||
|
|
@ -79,7 +79,7 @@ export const worldClocksStore = {
|
|||
const response = await api.put('/world-clocks/reorder', { ids });
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error };
|
||||
return { success: false, error: response.error.message };
|
||||
}
|
||||
|
||||
// Update local order
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue