Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 84 additions & 6 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,87 @@ export interface DataResponse {
rows: DataRow[];
}

interface FetchOptions {
params?: Record<string, string>;
method?: string;
body?: unknown;
okStatuses?: number[];
}

async function extractDetail(res: Response): Promise<string | null> {
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<T>(
path: string,
params?: Record<string, string>,
okStatuses: number[] = [200],
options: FetchOptions = {},
): Promise<T> {
const { params, method = "GET", body, okStatuses = [200] } = options;
const query = params ? `?${new URLSearchParams(params)}` : "";
const key = getApiKey();
const headers: Record<string, string> = {};
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<T>;
}

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<string, string>;
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<DatasetSummary[]> {
const body = await apiFetch<{ datasets: DatasetSummary[] }>("/datasets");
return body.datasets;
Expand All @@ -62,7 +127,20 @@ export function fetchData(
): Promise<DataResponse> {
return apiFetch<DataResponse>(
`/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<ProposalResponse> {
return apiFetch<ProposalResponse>("/datasets", {
method: "POST",
body: payload,
});
}
18 changes: 10 additions & 8 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
MutationCache,
QueryCache,
QueryClient,
QueryClientProvider,
Expand All @@ -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
Expand Down
Loading