web/lib/grove-api.tsblame
View source
bc1b2ba1// Server-side API client. Every named function here forwards the user's
bc1b2ba2// auth token via serverFetch, so private-repo access works the same on
bc1b2ba3// SSR as it does in the browser.
bc1b2ba4//
bc1b2ba5// Pages should NOT call `fetch(${groveApiUrl}/...)` directly — that path
bc1b2ba6// is anonymous, and private repos look like 404s to their actual owners.
bc1b2ba7// Add a function here instead.
bc1b2ba8
bc1b2ba9import { serverFetch } from "./server-fetch";
bc1b2ba10
bc1b2ba11/** All non-OK responses become null. Pages decide whether to render
bc1b2ba12 * "not found" or "could not load". */
bc1b2ba13async function getJson<T>(path: string): Promise<T | null> {
bc1b2ba14 try {
bc1b2ba15 const res = await serverFetch(path);
bc1b2ba16 if (!res.ok) return null;
bc1b2ba17 return (await res.json()) as T;
bc1b2ba18 } catch {
bc1b2ba19 return null;
bc1b2ba20 }
bc1b2ba21}
bc1b2ba22
bc1b2ba23// ---- Repos ----------------------------------------------------------
bc1b2ba24
bc1b2ba25export type RepoBranches = {
bc1b2ba26 branches?: Array<{ name: string }>;
bc1b2ba27 bookmarks?: Array<{ name: string }>;
bc1b2ba28};
bc1b2ba29
bc1b2ba30export const getRepoBranches = (owner: string, repo: string) =>
bc1b2ba31 getJson<RepoBranches>(`/api/repos/${owner}/${repo}/branches`);
bc1b2ba32
bc1b2ba33export type RepoTree = {
bc1b2ba34 entries: Array<{ name: string; type: string; size?: number }>;
bc1b2ba35};
bc1b2ba36
bc1b2ba37export const getRepoTree = (owner: string, repo: string, ref: string, path?: string) =>
bc1b2ba38 getJson<RepoTree>(
bc1b2ba39 path
bc1b2ba40 ? `/api/repos/${owner}/${repo}/tree/${ref}/${path}`
bc1b2ba41 : `/api/repos/${owner}/${repo}/tree/${ref}`,
bc1b2ba42 );
bc1b2ba43
bc1b2ba44// Note: callers assume content + size are present (i.e. text files). For
bc1b2ba45// binary blobs the API includes is_binary: true and may omit content;
bc1b2ba46// no current page handles that branch. Tighten the type when it does.
bc1b2ba47export type RepoBlob = { content: string; size: number; is_binary?: boolean };
bc1b2ba48
bc1b2ba49export const getRepoBlob = (owner: string, repo: string, ref: string, path: string) =>
bc1b2ba50 getJson<RepoBlob>(`/api/repos/${owner}/${repo}/blob/${ref}/${path}`);
bc1b2ba51
bc1b2ba52export type RepoCommitList = { commits: any[] };
bc1b2ba53
bc1b2ba54export const getRepoCommits = (
bc1b2ba55 owner: string,
bc1b2ba56 repo: string,
bc1b2ba57 ref: string,
bc1b2ba58 opts: { limit?: number } = {},
bc1b2ba59) => {
bc1b2ba60 const qs = opts.limit ? `?limit=${opts.limit}` : "";
bc1b2ba61 return getJson<RepoCommitList>(`/api/repos/${owner}/${repo}/commits/${ref}${qs}`);
bc1b2ba62};
bc1b2ba63
bc1b2ba64export type RepoDiff = { diffs: Array<{ path: string; diff: string; is_binary?: boolean }> };
bc1b2ba65
bc1b2ba66export const getRepoDiff = (owner: string, repo: string, base: string, head: string) =>
bc1b2ba67 getJson<RepoDiff>(`/api/repos/${owner}/${repo}/diff?base=${base}&head=${head}`);
bc1b2ba68
bc1b2ba69// The endpoints below return nested objects whose shapes are richer than
bc1b2ba70// what each caller cares about. The wrapper types below are generic over
bc1b2ba71// the row/run/blame element so callers pass their own local Repo/Run/etc.
bc1b2ba72// types: `await listRepos<Repo>()` → `{ repos?: Repo[] }`. Defaults to
bc1b2ba73// `unknown` so passing nothing forces an explicit cast at the use site.
bc1b2ba74
bc1b2ba75export const getRepoDiffs = <T = unknown>(owner: string, repo: string, status: string) =>
bc1b2ba76 getJson<{ diffs?: T[] } & Record<string, unknown>>(
bc1b2ba77 `/api/repos/${owner}/${repo}/diffs?status=${status}`,
bc1b2ba78 );
bc1b2ba79
bc1b2ba80export const getRepoBlame = <T = unknown>(owner: string, repo: string, ref: string, path: string) =>
bc1b2ba81 getJson<T>(`/api/repos/${owner}/${repo}/blame/${ref}/${path}`);
bc1b2ba82
bc1b2ba83export const listRepos = <T = unknown>() =>
bc1b2ba84 getJson<{ repos?: T[] }>(`/api/repos`);
bc1b2ba85
bc1b2ba86export const getCanopyRecentRuns = <T = unknown>(
bc1b2ba87 opts: { per_repo?: number; owner?: string; limit?: number } = {},
bc1b2ba88) => {
bc1b2ba89 const params = new URLSearchParams();
bc1b2ba90 if (opts.per_repo) params.set("per_repo", String(opts.per_repo));
bc1b2ba91 if (opts.owner) params.set("owner", opts.owner);
bc1b2ba92 if (opts.limit) params.set("limit", String(opts.limit));
bc1b2ba93 const qs = params.toString();
bc1b2ba94 return getJson<{ runs?: T[] }>(`/api/canopy/recent-runs${qs ? `?${qs}` : ""}`);
bc1b2ba95};
bc1b2ba96
bc1b2ba97export const getCanopyRunsForRepo = <T = unknown>(owner: string, repo: string, opts: { limit?: number } = {}) => {
bc1b2ba98 const qs = opts.limit ? `?limit=${opts.limit}` : "";
bc1b2ba99 return getJson<{ runs?: T[] }>(`/api/repos/${owner}/${repo}/canopy/runs${qs}`);
bc1b2ba100};
bc1b2ba101
bc1b2ba102export const getCanopyRun = <T = unknown>(owner: string, repo: string, runId: string) =>
bc1b2ba103 getJson<{ run?: T }>(`/api/repos/${owner}/${repo}/canopy/runs/${runId}`);