ワンクリックで
github-graphql
GitHub GraphQL API 最佳實踐指南。當需要使用 GraphQL 查詢使用者資料、處理 cursor pagination、計算 rate limit、或除錯 GraphQL errors 時使用。
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
GitHub GraphQL API 最佳實踐指南。當需要使用 GraphQL 查詢使用者資料、處理 cursor pagination、計算 rate limit、或除錯 GraphQL errors 時使用。
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Zod v4 schema validation 最佳實踐指南。當需要定義 schema、驗證/解析 JSON 資料、type inference、或處理 unknown data 時使用。
Svelte 5 + Astro 整合最佳實踐指南。當需要建立 Svelte 元件、使用 runes API、整合 Astro islands、或用 Testing Library 測試 Svelte 元件時使用。
gayanvoice/top-github-users 架構參考指南。當需要了解 GitHub 使用者排行榜的資料抓取管線、國家設定、排行計算邏輯、已知問題、或社群需求時使用。
Commander.js v14 CLI 框架最佳實踐。當需要建立 CLI 工具、解析命令列參數、設計 subcommands 時使用。
GitHub Actions CI/CD 最佳實踐指南。當需要設定 workflow、cron 排程、GitHub Pages 部署、使用 Octokit API、或處理 rate limiting 時使用。
Globe.GL 3D 地球視覺化指南。當需要建立互動式 3D 地球、國家熱力圖、點擊導航、或整合 Astro 時使用。
| name | github-graphql |
| description | GitHub GraphQL API 最佳實踐指南。當需要使用 GraphQL 查詢使用者資料、處理 cursor pagination、計算 rate limit、或除錯 GraphQL errors 時使用。 |
GitHub GraphQL API 使用 points 系統,不是 REST 的 request count。
| 認證方式 | Points/hr |
|---|---|
| Personal Access Token | 5,000 |
| GitHub Enterprise Cloud | 10,000 |
| GitHub App(非 Enterprise) | 5,000 base + 50/repo + 50/user(cap 12,500) |
| GitHub Actions | 1,000/repo(Enterprise 15,000) |
| 端點 | 限制 |
|---|---|
| GraphQL | 2,000 points/min |
| REST | 900 points/min |
| 並發請求 | 最多 100 個 |
Secondary limit 的計算不同於 primary:
import { graphql } from "@octokit/graphql";
const graphqlWithAuth = graphql.defaults({
headers: { authorization: `token ${process.env.GITHUB_TOKEN}` },
});
const { rateLimit } = await graphqlWithAuth<{
rateLimit: { limit: number; cost: number; remaining: number; resetAt: string };
}>(`
query {
rateLimit {
limit
cost
remaining
resetAt
nodeCount
}
}
`);
console.log(`Cost: ${rateLimit.cost}, Remaining: ${rateLimit.remaining}`);
Response headers 也有:
x-ratelimit-limit:最大 points/hrx-ratelimit-remaining:剩餘 pointsx-ratelimit-used:已用 pointsx-ratelimit-reset:重置時間(UTC epoch)公式:加總每個 connection 需要的 requests,每個 request 假設達到 first/last 上限,除以 100 後取最近整數,最低 1 point。
query {
search(query: "location:Taiwan", type: USER, first: 100) { # 100/100 = 1
nodes {
... on User {
repositories(first: 10) { # 100 * 10 / 100 = 10
nodes { name }
}
}
}
}
}
# 總 cost = 1 + 10 = 11 points
最佳化原則:減少巢狀 connection 的 first 值可大幅降低 cost。
first/last 值必須在 1–100 之間first/last 相乘
import { graphql } from "@octokit/graphql";
const graphqlWithAuth = graphql.defaults({
headers: { authorization: `token ${process.env.GITHUB_TOKEN}` },
});
interface SearchUsersResult {
search: {
userCount: number;
pageInfo: { hasNextPage: boolean; endCursor: string | null };
nodes: Array<{
login: string;
name: string | null;
followers: { totalCount: number };
repositories: { totalCount: number };
contributionsCollection: {
contributionCalendar: { totalContributions: number };
};
}>;
};
}
const SEARCH_USERS_QUERY = `
query SearchUsers($query: String!, $first: Int!, $after: String) {
search(query: $query, type: USER, first: $first, after: $after) {
userCount
pageInfo {
hasNextPage
endCursor
}
nodes {
... on User {
login
name
followers { totalCount }
repositories(ownerAffiliations: OWNER) { totalCount }
contributionsCollection {
contributionCalendar { totalContributions }
}
}
}
}
rateLimit { cost remaining resetAt }
}
`;
const result = await graphqlWithAuth<SearchUsersResult>(SEARCH_USERS_QUERY, {
query: "location:Taiwan followers:>50",
first: 100,
after: null,
});
location:Taiwan — 位置followers:>100 / followers:50..200 — 追蹤者範圍repos:>10 — repo 數language:TypeScript — 主要語言type:user / type:org — 帳號類型created:>2020-01-01 — 建立日期Search API 硬性限制最多回傳 1000 筆結果,無論 pagination 如何設定。這是 GitHub 後端限制,REST 和 GraphQL 皆然。
// 策略:用 followers 範圍切分查詢
const followerRanges = [
"followers:>1000",
"followers:501..1000",
"followers:201..500",
"followers:101..200",
"followers:51..100",
"followers:21..50",
"followers:11..20",
"followers:1..10",
];
async function searchAllUsers(location: string): Promise<User[]> {
const allUsers: User[] = [];
for (const range of followerRanges) {
const query = `location:${location} ${range}`;
const users = await paginateSearch(query);
allUsers.push(...users);
// 如果某個範圍還是 >1000,需要再細分
if (users.length >= 1000) {
console.warn(`Range "${range}" hit 1000 cap, needs finer splitting`);
}
}
return deduplicateByLogin(allUsers);
}
async function adaptiveSearch(
location: string,
minFollowers: number,
maxFollowers: number,
): Promise<User[]> {
const query = `location:${location} followers:${minFollowers}..${maxFollowers}`;
// 先查 userCount
const { search } = await graphqlWithAuth<SearchUsersResult>(SEARCH_USERS_QUERY, {
query,
first: 1,
after: null,
});
if (search.userCount <= 1000) {
return paginateSearch(query);
}
// 超過 1000,二分法細分
const mid = Math.floor((minFollowers + maxFollowers) / 2);
const lower = await adaptiveSearch(location, minFollowers, mid);
const upper = await adaptiveSearch(location, mid + 1, maxFollowers);
return [...lower, ...upper];
}
async function paginateSearch(searchQuery: string): Promise<User[]> {
const allUsers: User[] = [];
let cursor: string | null = null;
let hasNextPage = true;
while (hasNextPage) {
const result = await graphqlWithAuth<SearchUsersResult>(SEARCH_USERS_QUERY, {
query: searchQuery,
first: 100, // 永遠用最大值 100
after: cursor,
});
const { search, rateLimit } = result as any;
// 過濾 null nodes(Organization 不符合 User fragment 時會是 null)
const users = search.nodes.filter((node: any) => node !== null && node.login);
allUsers.push(...users);
hasNextPage = search.pageInfo.hasNextPage;
cursor = search.pageInfo.endCursor;
// 監控 rate limit
if (rateLimit.remaining < 100) {
const resetAt = new Date(rateLimit.resetAt);
const waitMs = resetAt.getTime() - Date.now() + 1000;
if (waitMs > 0) {
console.log(`Rate limit low (${rateLimit.remaining}), waiting ${waitMs}ms`);
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
}
return allUsers;
}
first: 100:減少 API 呼叫次數first + after + endCursor + hasNextPagelast + before + startCursor + hasPreviousPageafter: null 表示第一頁endCursor、startCursor、hasNextPage、hasPreviousPagefunction buildBatchUserQuery(logins: string[]): string {
const fragments = logins.map(
(login, i) => `
user${i}: user(login: "${login}") {
login
name
followers { totalCount }
repositories(ownerAffiliations: OWNER) { totalCount }
contributionsCollection {
contributionCalendar { totalContributions }
}
}
`
);
return `query BatchUsers {
${fragments.join("\n")}
rateLimit { cost remaining resetAt }
}`;
}
// 每批最多 ~50 個使用者(避免超過 node limit 和 query 大小限制)
async function batchFetchUsers(logins: string[]): Promise<Map<string, User>> {
const BATCH_SIZE = 50;
const results = new Map<string, User>();
for (let i = 0; i < logins.length; i += BATCH_SIZE) {
const batch = logins.slice(i, i + BATCH_SIZE);
const query = buildBatchUserQuery(batch);
const data = await graphqlWithAuth(query);
for (let j = 0; j < batch.length; j++) {
const userData = (data as any)[`user${j}`];
if (userData) {
results.set(userData.login, userData);
}
}
}
return results;
}
Alias 查詢中,每個 user(login:) 是獨立 node request,cost = alias 數量 / 100(最低 1)。比 N 次獨立查詢(N points)便宜很多。
import { GraphqlResponseError } from "@octokit/graphql";
interface GraphQLError {
type: string; // "RATE_LIMITED" | "NOT_FOUND" | "FORBIDDEN" 等
message: string;
path?: string[];
locations?: Array<{ line: number; column: number }>;
}
async function safeGraphqlQuery<T>(query: string, variables: Record<string, unknown>): Promise<T | null> {
try {
return await graphqlWithAuth<T>(query, variables);
} catch (error) {
if (error instanceof GraphqlResponseError) {
// GraphQL 回傳了 data + errors
const errors = error.errors ?? [];
for (const err of errors) {
switch (err.type) {
case "RATE_LIMITED":
console.error("Rate limited, waiting...");
await waitForRateLimit();
return safeGraphqlQuery<T>(query, variables);
case "NOT_FOUND":
console.warn(`Resource not found: ${err.path?.join(".")}`);
return error.data as T; // partial data 仍可用
case "FORBIDDEN":
console.error(`Permission denied: ${err.message}`);
return null;
default:
console.error(`GraphQL error: ${err.type} - ${err.message}`);
}
}
}
// Network error 或其他
if ((error as any).status === 502) {
console.warn("GitHub 502, retrying...");
await new Promise((r) => setTimeout(r, 2000));
return safeGraphqlQuery<T>(query, variables);
}
throw error;
}
}
| Error Type | 原因 | 處理方式 |
|---|---|---|
RATE_LIMITED | 超過 rate limit | 等待 resetAt 後重試 |
NOT_FOUND | User/Repo 不存在 | 跳過,partial data 可能可用 |
FORBIDDEN | Token 權限不足 | 檢查 scope |
MAX_NODE_LIMIT_EXCEEDED | 超過 500K nodes | 減少 first/巢狀深度 |
QUERY_COMPLEXITY | Query 太複雜 | 拆分 query |
| Network 502/503 | GitHub 暫時錯誤 | 指數退避重試 |
GraphQL 可能回傳 data + errors 同時存在。例如查詢 10 個 user,其中 1 個不存在:
// response.data 有 9 個 user 的資料
// response.errors 有 1 個 NOT_FOUND
// 不要因為 errors 就丟掉全部 data!
| Scope | 用途 |
|---|---|
read:user | 讀取使用者 profile(必要) |
user:email | 讀取使用者 email(可選) |
repo(public_repo) | 讀取 public repo 資料 |
read:org | 讀取 organization 成員資料(可選) |
Fine-grained token 可以更精確控制權限:
import { graphql } from "@octokit/graphql";
// 方式 1:直接設定
const graphqlWithAuth = graphql.defaults({
headers: {
authorization: `token ${process.env.GITHUB_TOKEN}`,
},
});
// 方式 2:透過 Octokit
import { Octokit } from "@octokit/rest";
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const data = await octokit.graphql(query, variables);
first/last 最大 100 — 設超過會 errortype: USER 查詢仍可能包含 Organization,需過濾userCount 是估計值 — Search API 的 userCount 不一定精確contributionsCollection 的 from/to 跨度最多一年| 需求 | REST | GraphQL |
|---|---|---|
| 搜尋使用者 | GET /search/users(30 req/min) | search(type: USER)(points 系統) |
| 使用者 profile | GET /users/{login} | user(login:) |
| Contribution 資料 | ❌ 不支援 | contributionsCollection |
| 批次查詢多個使用者 | N 個 requests | 1 個 query with aliases |
| Rate limit | 5000 req/hr | 5000 points/hr |
| 自訂回傳欄位 | ❌ 固定格式 | ✅ 只取需要的 |