diff --git a/packages/utils/fetchers/github.ts b/packages/utils/fetchers/github.ts index defa7c1..cbaea55 100644 --- a/packages/utils/fetchers/github.ts +++ b/packages/utils/fetchers/github.ts @@ -1,71 +1,186 @@ +import { useQuery, useMutation, UseQueryOptions, UseMutationOptions, QueryKey } from '@tanstack/react-query' + interface GitHubFetcherOptions { baseURL?: string + timeout?: number userAgent?: string authToken?: string } -const defaultOptions: GitHubFetcherOptions = { - baseURL: 'https://api.github.com', - userAgent: 'InfinityBotList', - authToken: process.env.GITHUB_TOKEN -} - -export interface GitHubResponse { +interface GitHubResponse { data: T headers: Record status: number } -/** - * Fetch function for GitHub API. - * Designed to work directly with TanStack Query. - */ -export async function fetchGitHub( - endpoint: string, - options: GitHubFetcherOptions = defaultOptions, - method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', - body?: any -): Promise> { - if (!options.authToken) { - throw new Error('GitHub token not configured. Please set GITHUB_TOKEN environment variable.') +// Rate limit state (module-level, not per-request) +let rateLimitRemaining = 60 +let rateLimitReset = 0 + +function updateRateLimits(headers: Record) { + const remaining = headers['x-ratelimit-remaining'] + const reset = headers['x-ratelimit-reset'] + if (remaining) rateLimitRemaining = parseInt(remaining, 10) + if (reset) rateLimitReset = parseInt(reset, 10) +} + +export function getRateLimitInfo() { + return { + remaining: rateLimitRemaining, + reset: rateLimitReset, + resetTime: new Date(rateLimitReset * 1000) } +} - const url = `${options.baseURL}${endpoint}` +function getHeaders(options?: GitHubFetcherOptions) { const headers: Record = { 'Accept': 'application/vnd.github.v3+json', - 'User-Agent': options.userAgent || 'FixFX-Wiki', - 'Authorization': `Bearer ${options.authToken}`, - 'Content-Type': 'application/json' + 'User-Agent': options?.userAgent || 'InfinityBotList' } + const token = process.env.GITHUB_TOKEN + if (token) headers['Authorization'] = `Bearer ${token}` + return headers +} - const response = await fetch(url, { +async function githubFetch( + method: 'GET' | 'POST' | 'PUT' | 'DELETE', + endpoint: string, + data?: any, + options?: GitHubFetcherOptions & { fetchOptions?: RequestInit } +): Promise> { + const baseURL = options?.baseURL || 'https://api.github.com' + const url = endpoint.startsWith('http') ? endpoint : `${baseURL}${endpoint}` + const headers = { + ...getHeaders(options), + ...(options?.fetchOptions?.headers || {}) + } + const fetchOptions: RequestInit = { method, headers, - body: body ? JSON.stringify(body) : undefined + ...options?.fetchOptions + } + if (data && method !== 'GET') { + fetchOptions.body = JSON.stringify(data) + // Ensure headers is a Record + if (typeof headers === 'object' && headers !== null && !Array.isArray(headers)) { + ;(headers as Record)['Content-Type'] = 'application/json' + } + } + const res = await fetch(url, fetchOptions) + const resHeaders: Record = {} + res.headers.forEach((v, k) => { + resHeaders[k] = v }) - - const jsonData = await response.json().catch(() => ({})) - - if (!response.ok) { - const message = jsonData?.message || response.statusText - switch (response.status) { + updateRateLimits(resHeaders) + const contentType = res.headers.get('content-type') || '' + let responseData: any = undefined + if (contentType.includes('application/json')) { + responseData = await res.json() + } else { + responseData = await res.text() + } + if (!res.ok) { + const status = res.status + const message = responseData?.message || res.statusText + switch (status) { case 401: case 403: - throw new Error(`GitHub Authentication Error: ${message}`) + throw new Error( + `GitHub API Authentication Error: ${message}. Please check your GitHub token configuration.` + ) case 404: - throw new Error(`GitHub Resource Not Found: ${message}`) + throw new Error(`GitHub API Resource Not Found: ${message}`) case 429: - const resetTime = response.headers.get('x-ratelimit-reset') + const resetTime = res.headers.get('x-ratelimit-reset') const retryAfter = resetTime ? new Date(parseInt(resetTime) * 1000) : 'unknown' - throw new Error(`GitHub Rate Limit Exceeded. Retry after ${retryAfter}`) + throw new Error(`GitHub API Rate Limit Exceeded. Please try again after ${retryAfter}`) default: - throw new Error(`GitHub API Error (${response.status}): ${message}`) + throw new Error(`GitHub API Error (${status}): ${message}`) } } - return { - data: jsonData, - headers: Object.fromEntries(response.headers.entries()), - status: response.status + data: responseData, + headers: resHeaders, + status: res.status } } + +// --- React Query hooks --- + +export function useGitHubQuery( + key: QueryKey, + endpoint: string, + options?: UseQueryOptions, E> & { fetcherOptions?: GitHubFetcherOptions } +) { + return useQuery, E>({ + queryKey: key, + queryFn: () => githubFetch('GET', endpoint, undefined, options?.fetcherOptions), + ...options + }) +} + +export function useGitHubMutation( + method: 'POST' | 'PUT' | 'DELETE', + endpoint: string, + options?: UseMutationOptions, E, V> & { fetcherOptions?: GitHubFetcherOptions } +) { + return useMutation, E, V>({ + mutationFn: (variables: V) => githubFetch(method, endpoint, variables, options?.fetcherOptions), + ...options + }) +} + +// Convenience hooks +export function useGitHubPost( + endpoint: string, + options?: UseMutationOptions, E, V> & { fetcherOptions?: GitHubFetcherOptions } +) { + return useGitHubMutation('POST', endpoint, options) +} + +export function useGitHubPut( + endpoint: string, + options?: UseMutationOptions, E, V> & { fetcherOptions?: GitHubFetcherOptions } +) { + return useGitHubMutation('PUT', endpoint, options) +} + +export function useGitHubDelete( + endpoint: string, + options?: UseMutationOptions, E, V> & { fetcherOptions?: GitHubFetcherOptions } +) { + return useGitHubMutation('DELETE', endpoint, options) +} + +// --- Class-based API --- +class GitHubFetcher { + async get(endpoint: string, options?: GitHubFetcherOptions & { fetchOptions?: RequestInit }) { + return githubFetch('GET', endpoint, undefined, options) + } + async post( + endpoint: string, + data?: V, + options?: GitHubFetcherOptions & { fetchOptions?: RequestInit } + ) { + return githubFetch('POST', endpoint, data, options) + } + async put( + endpoint: string, + data?: V, + options?: GitHubFetcherOptions & { fetchOptions?: RequestInit } + ) { + return githubFetch('PUT', endpoint, data, options) + } + async delete( + endpoint: string, + data?: V, + options?: GitHubFetcherOptions & { fetchOptions?: RequestInit } + ) { + return githubFetch('DELETE', endpoint, data, options) + } +} + +const githubFetcher = new GitHubFetcher() + +export { GitHubFetcher, githubFetcher } +export default githubFetcher