Files
leetcode/apps/api/src/github.ts
T

95 lines
3.2 KiB
TypeScript

/**
* GitHub REST + GraphQL client on GH_PAT. Hand-rolled fetch, no SDK —
* matching the repo's dependency-free automation rule.
*/
export interface GitHub {
repo: string;
rest(path: string, init?: RequestInit): Promise<unknown>;
graphql(query: string, variables?: Record<string, unknown>): Promise<unknown>;
list(path: string): AsyncGenerator<unknown, void, void>;
/** Comment first, then close — a failed close still leaves a visible note. */
closeIssue(issue: number, comment: string): Promise<void>;
comment(issue: number, body: string): Promise<void>;
react(commentId: number, content: string): Promise<void>;
}
const PER_PAGE = 100;
export function github(token: string, repo: string): GitHub {
async function rest(path: string, init: RequestInit = {}): Promise<unknown> {
const res = await fetch(`https://api.github.com${path}`, {
...init,
headers: {
accept: "application/vnd.github+json",
authorization: `Bearer ${token}`,
"user-agent": "srs-api",
"x-github-api-version": "2022-11-28",
...(init.body ? { "content-type": "application/json" } : {}),
},
});
if (!res.ok) {
throw new Error(`${init.method ?? "GET"} ${path} -> ${res.status} ${await res.text()}`);
}
return res.status === 204 ? null : res.json();
}
async function graphql(
query: string,
variables: Record<string, unknown> = {},
): Promise<unknown> {
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
authorization: `Bearer ${token}`,
"content-type": "application/json",
"user-agent": "srs-api",
},
body: JSON.stringify({ query, variables }),
});
if (!res.ok) throw new Error(`graphql -> ${res.status} ${await res.text()}`);
const payload = (await res.json()) as { data?: unknown; errors?: { message: string }[] };
if (payload.errors?.length) throw new Error(payload.errors.map((e) => e.message).join("; "));
return payload.data;
}
async function* list(path: string): AsyncGenerator<unknown, void, void> {
const sep = path.includes("?") ? "&" : "?";
for (let page = 1; ; page++) {
const batch = await rest(`${path}${sep}per_page=${PER_PAGE}&page=${page}`);
if (!Array.isArray(batch)) throw new Error(`unexpected ${path} payload: not an array`);
yield* batch;
if (batch.length < PER_PAGE) return;
}
}
return {
repo,
rest,
graphql,
list,
async closeIssue(issue, comment) {
await rest(`/repos/${repo}/issues/${issue}/comments`, {
method: "POST",
body: JSON.stringify({ body: comment }),
});
await rest(`/repos/${repo}/issues/${issue}`, {
method: "PATCH",
body: JSON.stringify({ state: "closed", state_reason: "completed" }),
});
},
async comment(issue, body) {
await rest(`/repos/${repo}/issues/${issue}/comments`, {
method: "POST",
body: JSON.stringify({ body }),
});
},
async react(commentId, content) {
await rest(`/repos/${repo}/issues/comments/${commentId}/reactions`, {
method: "POST",
body: JSON.stringify({ content }),
});
},
};
}