chore: establish typed SvelteKit frontend foundation

This commit is contained in:
2026-08-20 19:47:25 +03:30
parent e1e4ed8467
commit c719a11ad7
18 changed files with 5705 additions and 1 deletions

100
src/lib/api/client.ts Normal file
View File

@@ -0,0 +1,100 @@
import type { components } from './schema';
export type AuthSession = components['schemas']['AuthSession'];
export type ReportingPeriod = components['schemas']['ReportingPeriod'];
export type AnalyticsRun = components['schemas']['AnalyticsRun'];
export type SessionDetailPage = components['schemas']['SessionDetailPage'];
export type DetailView = components['schemas']['DetailView'];
export type FailureStage = components['schemas']['FailureStage'];
export type Problem = components['schemas']['Problem'];
const apiBase = (import.meta.env.PUBLIC_API_BASE_URL || '/api/v1').replace(/\/$/, '');
export class ApiError extends Error {
constructor(
public status: number,
public problem?: Problem,
public retryAfter?: number,
) {
super(problem?.detail ?? 'در ارتباط با سرویس مشکلی پیش آمد.');
}
}
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${apiBase}${path}`, {
...init,
credentials: 'include',
headers: {
Accept: 'application/json',
...(init.body ? { 'Content-Type': 'application/json' } : {}),
...init.headers,
},
});
if (!response.ok) {
const type = response.headers.get('content-type');
const problem = type?.includes('application/problem+json')
? ((await response.json()) as Problem)
: undefined;
throw new ApiError(response.status, problem, Number(response.headers.get('retry-after')) || undefined);
}
return response.status === 204 ? (undefined as T) : ((await response.json()) as T);
}
export const api = {
login(username: string, password: string) {
return request<AuthSession>('/auth/sessions', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
},
session() {
return request<AuthSession>('/auth/session');
},
logout(csrfToken: string) {
return request<void>('/auth/session', {
method: 'DELETE',
headers: { 'X-CSRF-Token': csrfToken },
});
},
periods() {
return request<components['schemas']['PeriodList']>('/analytics/periods');
},
startRun(periodId: string, csrfToken: string) {
return request<AnalyticsRun>('/analytics/runs', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({ periodId }),
});
},
run(runId: string) {
return request<AnalyticsRun>(`/analytics/runs/${encodeURIComponent(runId)}`);
},
retry(runId: string, sectionId: string, csrfToken: string) {
return request<AnalyticsRun>(
`/analytics/runs/${encodeURIComponent(runId)}/sections/${encodeURIComponent(sectionId)}/retry-attempts`,
{
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken,
'Idempotency-Key': crypto.randomUUID(),
},
},
);
},
details(periodId: string, view: DetailView, cursor?: string, failureStage?: FailureStage) {
const query = new URLSearchParams({ periodId, view, limit: '25' });
if (cursor) query.set('cursor', cursor);
if (failureStage) query.set('failureStage', failureStage);
return request<SessionDetailPage>(`/analytics/session-details?${query}`);
},
exportUrl(periodId: string, view: DetailView, failureStage?: FailureStage) {
const query = new URLSearchParams({ periodId, view });
if (failureStage) query.set('failureStage', failureStage);
return `${apiBase}/analytics/session-details.csv?${query}`;
},
};

1355
src/lib/api/schema.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff