jo3t / app /lib /services /githubApiService.ts
samifalouti1
Fresh start without binaries
55d48a7
import type {
GitHubUserResponse,
GitHubRepoInfo,
GitHubBranch,
GitHubOrganization,
GitHubStats,
GitHubLanguageStats,
} from '~/types/GitHub';
export interface GitHubApiServiceConfig {
token?: string;
tokenType?: 'classic' | 'fine-grained';
baseURL?: string;
}
export interface DetailedRepoInfo extends GitHubRepoInfo {
branches_count?: number;
contributors_count?: number;
issues_count?: number;
pull_requests_count?: number;
}
export interface GitHubApiError {
message: string;
status: number;
code?: string;
}
export class GitHubApiServiceClass {
private _config: GitHubApiServiceConfig;
private _baseURL: string;
constructor(config: GitHubApiServiceConfig = {}) {
this._config = config;
this._baseURL = config.baseURL || 'https://api.github.com';
}
/**
* Configure the service with authentication details
*/
configure(config: GitHubApiServiceConfig): void {
this._config = { ...this._config, ...config };
this._baseURL = config.baseURL || this._baseURL;
}
private async _makeRequestInternal<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
if (!this._config.token) {
throw new Error('GitHub token is required. Call configure() first.');
}
const response = await fetch(`${this._baseURL}${endpoint}`, {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `${this._config.tokenType === 'classic' ? 'token' : 'Bearer'} ${this._config.token}`,
'User-Agent': 'Bolt.diy',
...options.headers,
},
...options,
});
if (!response.ok) {
const errorData: any = await response.json().catch(() => ({ message: response.statusText }));
const error: GitHubApiError = {
message: errorData.message || response.statusText,
status: response.status,
code: errorData.code,
};
throw error;
}
return response.json();
}
/**
* Fetch all user repositories with pagination
*/
async getAuthenticatedUser(): Promise<GitHubUserResponse> {
return this._makeRequestInternal<GitHubUserResponse>('/user');
}
async getAllUserRepositories(): Promise<GitHubRepoInfo[]> {
const allRepos: GitHubRepoInfo[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const repos = await this._makeRequestInternal<GitHubRepoInfo[]>(
`/user/repos?per_page=100&page=${page}&sort=updated`,
);
allRepos.push(...repos);
hasMore = repos.length === 100; // If we got 100 repos, there might be more
page++;
}
return allRepos;
}
/**
* Fetch detailed information for a repository including additional metrics
*/
async getDetailedRepositoryInfo(owner: string, repo: string): Promise<DetailedRepoInfo> {
const [repoInfo, branches] = await Promise.all([
this._makeRequestInternal<GitHubRepoInfo>(`/repos/${owner}/${repo}`),
this.getRepositoryBranches(owner, repo).catch(() => []),
]);
// Try to get additional metrics
const [contributors, issues, pullRequests] = await Promise.allSettled([
this._getRepositoryContributorsCount(owner, repo),
this._getRepositoryIssuesCount(owner, repo),
this._getRepositoryPullRequestsCount(owner, repo),
]);
const detailedInfo: DetailedRepoInfo = {
...repoInfo,
branches_count: branches.length,
contributors_count: contributors.status === 'fulfilled' ? contributors.value : undefined,
issues_count: issues.status === 'fulfilled' ? issues.value : undefined,
pull_requests_count: pullRequests.status === 'fulfilled' ? pullRequests.value : undefined,
};
return detailedInfo;
}
/**
* Get repository branches
*/
async getRepositoryBranches(owner: string, repo: string): Promise<GitHubBranch[]> {
return this._makeRequestInternal<GitHubBranch[]>(`/repos/${owner}/${repo}/branches`);
}
/**
* Get contributors count using Link header pagination info
*/
private async _getRepositoryContributorsCount(owner: string, repo: string): Promise<number> {
const response = await fetch(`${this._baseURL}/repos/${owner}/${repo}/contributors?per_page=1`, {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `${this._config.tokenType === 'classic' ? 'token' : 'Bearer'} ${this._config.token}`,
'User-Agent': 'Bolt.diy',
},
});
if (!response.ok) {
return 0;
}
const linkHeader = response.headers.get('Link');
if (linkHeader) {
const match = linkHeader.match(/page=(\d+)>; rel="last"/);
return match ? parseInt(match[1], 10) : 1;
}
const data = await response.json();
return Array.isArray(data) ? data.length : 0;
}
/**
* Get issues count using Link header pagination info
*/
private async _getRepositoryIssuesCount(owner: string, repo: string): Promise<number> {
const response = await fetch(`${this._baseURL}/repos/${owner}/${repo}/issues?state=all&per_page=1`, {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `${this._config.tokenType === 'classic' ? 'token' : 'Bearer'} ${this._config.token}`,
'User-Agent': 'Bolt.diy',
},
});
if (!response.ok) {
return 0;
}
const linkHeader = response.headers.get('Link');
if (linkHeader) {
const match = linkHeader.match(/page=(\d+)>; rel="last"/);
return match ? parseInt(match[1], 10) : 1;
}
const data = await response.json();
return Array.isArray(data) ? data.length : 0;
}
/**
* Get pull requests count using Link header pagination info
*/
private async _getRepositoryPullRequestsCount(owner: string, repo: string): Promise<number> {
const response = await fetch(`${this._baseURL}/repos/${owner}/${repo}/pulls?state=all&per_page=1`, {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `${this._config.tokenType === 'classic' ? 'token' : 'Bearer'} ${this._config.token}`,
'User-Agent': 'Bolt.diy',
},
});
if (!response.ok) {
return 0;
}
const linkHeader = response.headers.get('Link');
if (linkHeader) {
const match = linkHeader.match(/page=(\d+)>; rel="last"/);
return match ? parseInt(match[1], 10) : 1;
}
const data = await response.json();
return Array.isArray(data) ? data.length : 0;
}
/**
* Fetch detailed information for multiple repositories in batches
*/
async getDetailedRepositoriesInfo(
repos: GitHubRepoInfo[],
batchSize: number = 5,
delayMs: number = 100,
): Promise<DetailedRepoInfo[]> {
const detailedRepos: DetailedRepoInfo[] = [];
for (let i = 0; i < repos.length; i += batchSize) {
const batch = repos.slice(i, i + batchSize);
const batchResults = await Promise.allSettled(
batch.map((repo) => {
const [owner, repoName] = repo.full_name.split('/');
return this.getDetailedRepositoryInfo(owner, repoName);
}),
);
// Collect successful results
batchResults.forEach((result, index) => {
if (result.status === 'fulfilled') {
detailedRepos.push(result.value);
} else {
console.error(`Failed to fetch details for ${batch[index].full_name}:`, result.reason);
// Fallback to original repo data
detailedRepos.push(batch[index]);
}
});
// Add delay between batches to be respectful to the API
if (i + batchSize < repos.length) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
return detailedRepos;
}
/**
* Calculate comprehensive statistics from repositories
*/
calculateRepositoryStats(repos: DetailedRepoInfo[]): {
languages: GitHubLanguageStats;
mostUsedLanguages: Array<{ language: string; bytes: number; repos: number }>;
totalBranches: number;
totalContributors: number;
totalIssues: number;
totalPullRequests: number;
repositoryHealth: {
healthy: number;
active: number;
archived: number;
forked: number;
};
} {
const languages: GitHubLanguageStats = {};
const languageBytes: Record<string, number> = {};
const languageRepos: Record<string, number> = {};
let totalBranches = 0;
let totalContributors = 0;
let totalIssues = 0;
let totalPullRequests = 0;
let healthyRepos = 0;
let activeRepos = 0;
let archivedRepos = 0;
let forkedRepos = 0;
repos.forEach((repo) => {
// Language statistics
if (repo.language) {
languages[repo.language] = (languages[repo.language] || 0) + 1;
languageBytes[repo.language] = (languageBytes[repo.language] || 0) + (repo.size || 0);
languageRepos[repo.language] = (languageRepos[repo.language] || 0) + 1;
}
// Aggregate metrics
totalBranches += repo.branches_count || 0;
totalContributors += repo.contributors_count || 0;
totalIssues += repo.issues_count || 0;
totalPullRequests += repo.pull_requests_count || 0;
// Repository health analysis
const daysSinceUpdate = Math.floor((Date.now() - new Date(repo.updated_at).getTime()) / (1000 * 60 * 60 * 24));
if (repo.archived) {
archivedRepos++;
} else if (repo.fork) {
forkedRepos++;
} else if (daysSinceUpdate < 7) {
activeRepos++;
} else if (daysSinceUpdate < 30 && repo.stargazers_count > 0) {
healthyRepos++;
}
});
// Create most used languages array sorted by bytes
const mostUsedLanguages = Object.entries(languageBytes)
.map(([language, bytes]) => ({
language,
bytes,
repos: languageRepos[language] || 0,
}))
.sort((a, b) => b.bytes - a.bytes)
.slice(0, 20);
return {
languages,
mostUsedLanguages,
totalBranches,
totalContributors,
totalIssues,
totalPullRequests,
repositoryHealth: {
healthy: healthyRepos,
active: activeRepos,
archived: archivedRepos,
forked: forkedRepos,
},
};
}
/**
* Generate comprehensive GitHub stats for a user
*/
async generateComprehensiveStats(userData: GitHubUserResponse): Promise<GitHubStats> {
try {
// Fetch all repositories
const allRepos = await this.getAllUserRepositories();
// Get detailed information for repositories (in batches)
const detailedRepos = await this.getDetailedRepositoriesInfo(allRepos);
// Calculate statistics
const stats = this.calculateRepositoryStats(detailedRepos);
// Fetch additional data in parallel
const [organizations, recentActivity] = await Promise.allSettled([
this._makeRequestInternal<GitHubOrganization[]>('/user/orgs'),
this._makeRequestInternal<any[]>(`/users/${userData.login}/events?per_page=10`),
]);
// Calculate aggregated metrics
const totalStars = detailedRepos.reduce((sum, repo) => sum + repo.stargazers_count, 0);
const totalForks = detailedRepos.reduce((sum, repo) => sum + repo.forks_count, 0);
const privateRepos = detailedRepos.filter((repo) => repo.private).length;
const githubStats: GitHubStats = {
repos: detailedRepos,
recentActivity:
recentActivity.status === 'fulfilled'
? recentActivity.value.slice(0, 10).map((event: any) => ({
id: event.id,
type: event.type,
repo: { name: event.repo.name, url: event.repo.url },
created_at: event.created_at,
payload: event.payload || {},
}))
: [],
languages: stats.languages,
totalGists: userData.public_gists || 0,
publicRepos: userData.public_repos || 0,
privateRepos,
stars: totalStars,
forks: totalForks,
followers: userData.followers || 0,
publicGists: userData.public_gists || 0,
privateGists: 0, // This would need additional API call
lastUpdated: new Date().toISOString(),
totalStars,
totalForks,
organizations: organizations.status === 'fulfilled' ? organizations.value : [],
totalBranches: stats.totalBranches,
totalContributors: stats.totalContributors,
totalIssues: stats.totalIssues,
totalPullRequests: stats.totalPullRequests,
mostUsedLanguages: stats.mostUsedLanguages,
};
return githubStats;
} catch (error) {
console.error('Error generating comprehensive stats:', error);
throw error;
}
}
/**
* Fetch authenticated user and rate limit info
*/
async fetchUser(
token: string,
tokenType: 'classic' | 'fine-grained' = 'classic',
): Promise<{ user: GitHubUserResponse; rateLimit: any }> {
this.configure({ token, tokenType });
const [user, rateLimit] = await Promise.all([
this.getAuthenticatedUser(),
this._makeRequestInternal('/rate_limit'),
]);
return { user, rateLimit };
}
/**
* Fetch comprehensive GitHub stats for authenticated user
*/
async fetchStats(token: string, tokenType: 'classic' | 'fine-grained' = 'classic'): Promise<GitHubStats> {
this.configure({ token, tokenType });
const user = await this.getAuthenticatedUser();
return this.generateComprehensiveStats(user);
}
/**
* Clear all cached data
*/
clearCache(): void {
// This is a placeholder - implement caching if needed
}
/**
* Clear user-specific cache
*/
clearUserCache(_token: string): void {
// This is a placeholder - implement user-specific caching if needed
}
}
// Export an instance of the service
export const gitHubApiService = new GitHubApiServiceClass();