| 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 | |
| 9 | import { serverFetch } from "./server-fetch"; |
| 10 | |
| 11 | /** All non-OK responses become null. Pages decide whether to render |
| 12 | * "not found" or "could not load". */ |
| 13 | async 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 | |
| 25 | export type RepoBranches = { |
| 26 | branches?: Array<{ name: string }>; |
| 27 | bookmarks?: Array<{ name: string }>; |
| 28 | }; |
| 29 | |
| 30 | export const getRepoBranches = (owner: string, repo: string) => |
| 31 | getJson<RepoBranches>(`/api/repos/${owner}/${repo}/branches`); |
| 32 | |
| 33 | export type RepoTree = { |
| 34 | entries: Array<{ name: string; type: string; size?: number }>; |
| 35 | }; |
| 36 | |
| 37 | export 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. |
| 47 | export type RepoBlob = { content: string; size: number; is_binary?: boolean }; |
| 48 | |
| 49 | export const getRepoBlob = (owner: string, repo: string, ref: string, path: string) => |
| 50 | getJson<RepoBlob>(`/api/repos/${owner}/${repo}/blob/${ref}/${path}`); |
| 51 | |
| 52 | export type RepoCommitList = { commits: any[] }; |
| 53 | |
| 54 | export 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 | |
| 64 | export type RepoDiff = { diffs: Array<{ path: string; diff: string; is_binary?: boolean }> }; |
| 65 | |
| 66 | export 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 | |
| 75 | export 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 | |
| 80 | export const getRepoBlame = <T = unknown>(owner: string, repo: string, ref: string, path: string) => |
| 81 | getJson<T>(`/api/repos/${owner}/${repo}/blame/${ref}/${path}`); |
| 82 | |
| 83 | export const listRepos = <T = unknown>() => |
| 84 | getJson<{ repos?: T[] }>(`/api/repos`); |
| 85 | |
| 86 | export 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 | |
| 97 | export 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 | |
| 102 | export const getCanopyRun = <T = unknown>(owner: string, repo: string, runId: string) => |
| 103 | getJson<{ run?: T }>(`/api/repos/${owner}/${repo}/canopy/runs/${runId}`); |
| 104 | |