Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 2 additions & 143 deletions server/modules/github/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,147 +174,6 @@ class GitHubService {
// Per-repo language bytes (top 10 repos only, to avoid rate limit)
const topRepos = (repos || []).sort((a, b) => b.stargazers_count - a.stargazers_count).slice(0, 10);
const repoLanguages = await Promise.all(
topRepos.map(r => ghFetch(`${GH_API}/repos/${r.full_name}/languages`, token)
(topRepos ?? []).map(r => ghFetch(`${GH_API}/repos/${r.full_name}/languages`, token)
.then(langs => ({ repo: r.name, langs: langs || {} })))
);

// Aggregate bytes across all repos
const langBytes = {};
repoLanguages.forEach(({ langs }) => {
Object.entries(langs).forEach(([lang, bytes]) => {
langBytes[lang] = (langBytes[lang] || 0) + bytes;
});
});
const langByBytes = Object.entries(langBytes)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([name, bytes]) => ({ name, bytes }));

// Repo stats
const ownedRepos = (repos || []).filter(r => !r.fork);
const forkedRepos = (repos || []).filter(r => r.fork);
const totalStars = (repos || []).reduce((s, r) => s + r.stargazers_count, 0);
const totalForks = (repos || []).reduce((s, r) => s + r.forks_count, 0);
const topByStars = [...(repos || [])].sort((a, b) => b.stargazers_count - a.stargazers_count).slice(0, 10);
const recentlyPushed = [...(repos || [])].sort((a, b) => new Date(b.pushed_at) - new Date(a.pushed_at)).slice(0, 5);

// Starred repo topics analysis
const starredTopics = {};
const starredLangs = {};
(starred || []).forEach(r => {
if (r.language) starredLangs[r.language] = (starredLangs[r.language] || 0) + 1;
(r.topics || []).forEach(t => { starredTopics[t] = (starredTopics[t] || 0) + 1; });
});

// Compute derived metrics
const metrics = this.#computeMetrics({ repos, contributions, events, prs, issues });

console.log(`[GitHub] ✓ Complete`);

return {
profile,
orgs: orgs || [],
repos: repos || [],
ownedRepos,
forkedRepos,
topByStars,
recentlyPushed,
totalStars,
totalForks,
langByBytes,
repoLanguages,
gists: gists || [],
starred: starred || [],
starredTopics: Object.entries(starredTopics).sort((a, b) => b[1] - a[1]).slice(0, 12).map(([t, c]) => ({ topic: t, count: c })),
starredLangs: Object.entries(starredLangs).sort((a, b) => b[1] - a[1]).slice(0, 8).map(([name, count]) => ({ name, count })),
events: events || [],
contributions,
prs: prs || { total_count: 0, items: [] },
issues: issues || { total_count: 0, items: [] },
metrics,
};
}

/** ── Full dashboard (cached) ────────────────────────────────────────── */
static async getDashboard(userId) {
// Check if we have cached data for this user
let githubData = await GithubData.findOne({ userId });

if (githubData && githubData.data) {
console.log(`[GitHub] ✓ Dashboard returned from cache for user ${userId}`);
return {
...githubData.data,
lastSyncedAt: githubData.lastSyncedAt
};
}

// If no cache, fetch fresh data and store it
const { token, username } = await this.#getToken(userId);
const data = await this.#fetchDashboardData(userId, token, username);

await GithubData.findOneAndUpdate(
{ userId },
{ userId, data, lastSyncedAt: new Date() },
{ upsert: true, new: true }
);

return {
...data,
lastSyncedAt: new Date()
};
}

/** ── Manual Sync Dashboard ─────────────────────────────────────────── */
static async syncDashboard(userId) {
const { token, username } = await this.#getToken(userId);
const data = await this.#fetchDashboardData(userId, token, username);

const githubData = await GithubData.findOneAndUpdate(
{ userId },
{ userId, data, lastSyncedAt: new Date() },
{ upsert: true, new: true }
);

return {
...data,
lastSyncedAt: githubData.lastSyncedAt
};
}

static async getProfile(userId) {
const { token } = await this.#getToken(userId);
const [profile, orgs] = await Promise.all([
ghFetch(`${GH_API}/user`, token),
ghFetch(`${GH_API}/user/orgs`, token, { per_page: 100 }),
]);
return { profile, orgs: orgs || [] };
}

static async getRepositories(userId) {
const { token } = await this.#getToken(userId);
const repos = await ghFetch(`${GH_API}/user/repos`, token, { per_page: 100, sort: "updated", type: "owner" });
const totalStars = (repos || []).reduce((s, r) => s + r.stargazers_count, 0);
const totalForks = (repos || []).reduce((s, r) => s + r.forks_count, 0);
const langs = {};
(repos || []).forEach(r => { if (r.language) langs[r.language] = (langs[r.language] || 0) + 1; });
return {
repos: repos || [],
languages: Object.entries(langs).sort((a, b) => b[1] - a[1]).map(([name, count]) => ({ name, count })),
topByStars: [...(repos || [])].sort((a, b) => b.stargazers_count - a.stargazers_count).slice(0, 10),
totalStars, totalForks,
};
}

static async getContributions(userId) {
const { token, username } = await this.#getToken(userId);
return this.#fetchContributions(username, token);
}

static async getActivity(userId) {
const { token, username } = await this.#getToken(userId);
const events = await ghFetch(`${GH_API}/users/${username}/events`, token, { per_page: 100 });
return events || [];
}
}

export default GitHubService;
.catch(err => console.error(err))
Comment on lines +177 to +179

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Keep .catch() inside each mapped promise and close the outer expression.

Line 178 closes map() before .catch() executes. Line 179 therefore cannot catch a ghFetch() rejection for one repository. The current parentheses also leave the outer expression unclosed, so the file does not parse.

Return an empty language result after logging. This keeps the Promise.all() result stable when one request fails.

Proposed fix
-      (topRepos ?? []).map(r => ghFetch(`${GH_API}/repos/${r.full_name}/languages`, token)
-        .then(langs => ({ repo: r.name, langs: langs || {} })))
-        .catch(err => console.error(err))
+      (topRepos ?? []).map(r => ghFetch(`${GH_API}/repos/${r.full_name}/languages`, token)
+        .then(langs => ({ repo: r.name, langs: langs || {} }))
+        .catch(err => {
+          console.error(err)
+          return { repo: r.name, langs: {} }
+        }))
🧰 Tools
🪛 Biome (2.5.5)

[error] 179-179: expected ) but instead the file ends

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/modules/github/service.js` around lines 177 - 179, Update the mapped
promise in the repository-language fetch expression so each ghFetch call has its
own catch handler before the map closes; log the error and return an empty
language result for that repository. Then close the surrounding map/Promise.all
expression correctly, preserving stable results when individual requests reject.

Source: Linters/SAST tools

Loading