Skip to content
Closed
Show file tree
Hide file tree
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
13 changes: 7 additions & 6 deletions src/app/api/metrics/repo-explorer/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getSessionWithToken } from "@/lib/get-session-token";
import { fetchUserRepos } from "@/lib/github";
import { fetchUserReposPaginated } from "@/lib/github";
import { NextRequest } from "next/server";
import { isMetricsCacheBypassed, metricsCacheKey, withMetricsCache } from "@/lib/metrics-cache";
import { ExplorerRepoCardData } from "@/lib/repo-analytics-types";
Expand All @@ -16,14 +16,15 @@ export async function GET(req: NextRequest) {
const session = sessionData.session;
const accessToken = sessionData.accessToken;

const page = parseInt(req.nextUrl.searchParams.get("page") ?? "1", 10);
const perPage = parseInt(req.nextUrl.searchParams.get("per_page") ?? "20", 10);

const bypass = isMetricsCacheBypassed(req);
const key = metricsCacheKey(session.githubId ?? session.githubLogin!, "repo-explorer-v2" as any, { days: 7 });
const key = metricsCacheKey(session.githubId ?? session.githubLogin!, `repo-explorer-v2-p${page}-pp${perPage}` as any, { days: 7 });

try {
const data = await withMetricsCache({ bypass, key, ttlSeconds: 30 * 60 }, async () => {
// Paginate through all pages (up to 1000 repos) so users with more
// than 100 repositories see their complete list — fixes #2843.
const repos = await fetchUserRepos(accessToken, { perPage: 100, maxPages: 10 });
const { repos, hasNextPage } = await fetchUserReposPaginated(accessToken, page, perPage);
const since = new Date();
since.setDate(since.getDate() - 30);
const sinceStr = since.toISOString().slice(0, 10);
Expand Down Expand Up @@ -88,7 +89,7 @@ export async function GET(req: NextRequest) {

result.sort((a, b) => b.commitCount - a.commitCount || new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());

return { repos: result };
return { repos: result, hasNextPage };
});
return Response.json(data);
} catch (error) {
Expand Down
42 changes: 42 additions & 0 deletions src/lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,48 @@ export async function fetchUserRepos(
return repos;
}

export interface PaginatedReposResult {
repos: GitHubRepo[];
hasNextPage: boolean;
}

/**
* Fetches a single page of repositories for the authenticated user.
* @param token - The user's GitHub personal access token.
* @param page - The page number to fetch.
* @param perPage - The number of items per page.
* @returns A promise that resolves to the paginated result.
*/
export async function fetchUserReposPaginated(
token: string,
page: number = 1,
perPage: number = 20
): Promise<PaginatedReposResult> {
const res = await githubFetch(
`${GITHUB_API}/user/repos?visibility=all&sort=pushed&direction=desc&per_page=${perPage}&page=${page}`,
{
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
},
}
);

if (!res.ok) {
if (res.status === 401) throw new GitHubAuthError();
throwIfGitHubRateLimited(res);
throw new Error(`GitHub API error: ${res.status}`);
}

const repos = (await res.json()) as GitHubRepo[];

// Check if there's a next page by looking at the Link header
const linkHeader = res.headers.get("Link") || "";
const hasNextPage = linkHeader.includes('rel="next"');

return { repos, hasNextPage };
}

export interface GitHubEvent {
id: string;
type: string;
Expand Down
12 changes: 12 additions & 0 deletions test/github.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
CommitItem,
GitHubIssueItem,
IssuesMetrics,
PaginatedReposResult,
} from "../src/lib/github";

describe("github types and interfaces", () => {
Expand Down Expand Up @@ -141,4 +142,15 @@ describe("github types and interfaces", () => {
expect(metrics.mostActiveRepo).toBeNull();
});
});

describe("PaginatedReposResult", () => {
it("accepts valid paginated result structure", () => {
const result: PaginatedReposResult = {
repos: [],
hasNextPage: true,
};
expect(result.hasNextPage).toBe(true);
expect(Array.isArray(result.repos)).toBe(true);
});
});
});
Loading