4.0 KB104 lines
Blame
1// Server-side API client. Every named function here forwards the user's
2// auth token via serverFetch, so private-repo access works the same on
3// SSR as it does in the browser.
4//
5// Pages should NOT call `fetch(${groveApiUrl}/...)` directly — that path
6// is anonymous, and private repos look like 404s to their actual owners.
7// Add a function here instead.
8
9import { serverFetch } from "./server-fetch";
10
11/** All non-OK responses become null. Pages decide whether to render
12 * "not found" or "could not load". */
13async function getJson<T>(path: string): Promise<T | null> {
14 try {
15 const res = await serverFetch(path);
16 if (!res.ok) return null;
17 return (await res.json()) as T;
18 } catch {
19 return null;
20 }
21}
22
23// ---- Repos ----------------------------------------------------------
24
25export type RepoBranches = {
26 branches?: Array<{ name: string }>;
27 bookmarks?: Array<{ name: string }>;
28};
29
30export const getRepoBranches = (owner: string, repo: string) =>
31 getJson<RepoBranches>(`/api/repos/${owner}/${repo}/branches`);
32
33export type RepoTree = {
34 entries: Array<{ name: string; type: string; size?: number }>;
35};
36
37export const getRepoTree = (owner: string, repo: string, ref: string, path?: string) =>
38 getJson<RepoTree>(
39 path
40 ? `/api/repos/${owner}/${repo}/tree/${ref}/${path}`
41 : `/api/repos/${owner}/${repo}/tree/${ref}`,
42 );
43
44// Note: callers assume content + size are present (i.e. text files). For
45// binary blobs the API includes is_binary: true and may omit content;
46// no current page handles that branch. Tighten the type when it does.
47export type RepoBlob = { content: string; size: number; is_binary?: boolean };
48
49export const getRepoBlob = (owner: string, repo: string, ref: string, path: string) =>
50 getJson<RepoBlob>(`/api/repos/${owner}/${repo}/blob/${ref}/${path}`);
51
52export type RepoCommitList = { commits: any[] };
53
54export const getRepoCommits = (
55 owner: string,
56 repo: string,
57 ref: string,
58 opts: { limit?: number } = {},
59) => {
60 const qs = opts.limit ? `?limit=${opts.limit}` : "";
61 return getJson<RepoCommitList>(`/api/repos/${owner}/${repo}/commits/${ref}${qs}`);
62};
63
64export type RepoDiff = { diffs: Array<{ path: string; diff: string; is_binary?: boolean }> };
65
66export const getRepoDiff = (owner: string, repo: string, base: string, head: string) =>
67 getJson<RepoDiff>(`/api/repos/${owner}/${repo}/diff?base=${base}&head=${head}`);
68
69// The endpoints below return nested objects whose shapes are richer than
70// what each caller cares about. The wrapper types below are generic over
71// the row/run/blame element so callers pass their own local Repo/Run/etc.
72// types: `await listRepos<Repo>()` → `{ repos?: Repo[] }`. Defaults to
73// `unknown` so passing nothing forces an explicit cast at the use site.
74
75export const getRepoDiffs = <T = unknown>(owner: string, repo: string, status: string) =>
76 getJson<{ diffs?: T[] } & Record<string, unknown>>(
77 `/api/repos/${owner}/${repo}/diffs?status=${status}`,
78 );
79
80export const getRepoBlame = <T = unknown>(owner: string, repo: string, ref: string, path: string) =>
81 getJson<T>(`/api/repos/${owner}/${repo}/blame/${ref}/${path}`);
82
83export const listRepos = <T = unknown>() =>
84 getJson<{ repos?: T[] }>(`/api/repos`);
85
86export const getCanopyRecentRuns = <T = unknown>(
87 opts: { per_repo?: number; owner?: string; limit?: number } = {},
88) => {
89 const params = new URLSearchParams();
90 if (opts.per_repo) params.set("per_repo", String(opts.per_repo));
91 if (opts.owner) params.set("owner", opts.owner);
92 if (opts.limit) params.set("limit", String(opts.limit));
93 const qs = params.toString();
94 return getJson<{ runs?: T[] }>(`/api/canopy/recent-runs${qs ? `?${qs}` : ""}`);
95};
96
97export const getCanopyRunsForRepo = <T = unknown>(owner: string, repo: string, opts: { limit?: number } = {}) => {
98 const qs = opts.limit ? `?limit=${opts.limit}` : "";
99 return getJson<{ runs?: T[] }>(`/api/repos/${owner}/${repo}/canopy/runs${qs}`);
100};
101
102export const getCanopyRun = <T = unknown>(owner: string, repo: string, runId: string) =>
103 getJson<{ run?: T }>(`/api/repos/${owner}/${repo}/canopy/runs/${runId}`);
104