一键导入
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 |
| 自訂回傳欄位 | ❌ 固定格式 | ✅ 只取需要的 |