diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ac78629..b6afbf8 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -32,22 +32,87 @@ export interface DataResponse { rows: DataRow[]; } +interface FetchOptions { + params?: Record; + method?: string; + body?: unknown; + okStatuses?: number[]; +} + +async function extractDetail(res: Response): Promise { + try { + const body: unknown = await res.json(); + if ( + typeof body === "object" && + body !== null && + "detail" in body && + typeof body.detail === "string" + ) { + return body.detail; + } + } catch { + // non-json error body + } + return null; +} + async function apiFetch( path: string, - params?: Record, - okStatuses: number[] = [200], + options: FetchOptions = {}, ): Promise { + const { params, method = "GET", body, okStatuses = [200] } = options; const query = params ? `?${new URLSearchParams(params)}` : ""; const key = getApiKey(); + const headers: Record = {}; + if (key) headers.Authorization = `Bearer ${key}`; + if (body !== undefined) headers["Content-Type"] = "application/json"; + const res = await fetch(`${BASE}${path}${query}`, { - headers: key ? { Authorization: `Bearer ${key}` } : {}, + method, + headers, + body: body !== undefined ? JSON.stringify(body) : undefined, }); if (!okStatuses.includes(res.status)) { - throw new ApiError(res.status, `request to ${path} failed: ${res.status}`); + const detail = await extractDetail(res); + throw new ApiError( + res.status, + detail ?? `request to ${path} failed: ${res.status}`, + ); } return res.json() as Promise; } +export interface ProposedDependency { + name: string; + version: string; + lookback?: string; +} + +export interface DatasetProposalPayload { + name: string; + version: string; + calendar: string; + granularity: string; + start_date: string; + schema: Record; + dependencies: ProposedDependency[]; + builder_script: string; + author_name: string; + team: string; + discord_user: string; + description: string; + env_vars: boolean; + requirements_txt?: string; + env_template?: string; +} + +export interface ProposalResponse { + dataset_name: string; + dataset_version: string; + pr_url: string; + branch: string; +} + export async function fetchDatasets(): Promise { const body = await apiFetch<{ datasets: DatasetSummary[] }>("/datasets"); return body.datasets; @@ -62,7 +127,20 @@ export function fetchData( ): Promise { return apiFetch( `/data/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, - { start, end, "build-data": "false" }, - [200, 206], + { + params: { start, end, "build-data": "false" }, + okStatuses: [200, 206], + }, ); } + +// nothing is written to the server: the backend validates the submission and +// opens a github pr; the dataset goes live after review + merge + restart +export function proposeDataset( + payload: DatasetProposalPayload, +): Promise { + return apiFetch("/datasets", { + method: "POST", + body: payload, + }); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 981014d..dd953ed 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,4 +1,5 @@ import { + MutationCache, QueryCache, QueryClient, QueryClientProvider, @@ -14,15 +15,16 @@ import { ApiError } from "@/lib/api"; import "@/index.css"; +function handleAuthError(error: unknown) { + if (error instanceof ApiError && error.status === 401) { + toast.error("invalid or missing API key"); + requestApiKey(); + } +} + const queryClient = new QueryClient({ - queryCache: new QueryCache({ - onError: (error) => { - if (error instanceof ApiError && error.status === 401) { - toast.error("invalid or missing API key"); - requestApiKey(); - } - }, - }), + queryCache: new QueryCache({ onError: handleAuthError }), + mutationCache: new MutationCache({ onError: handleAuthError }), defaultOptions: { queries: { // client errors won't fix themselves on retry