@rmdes/indiekit-endpoint-github 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -0
- package/lib/controllers/contributions.js +99 -17
- package/lib/controllers/dashboard.js +111 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -82,6 +82,7 @@ export default class GitHubEndpoint {
|
|
|
82
82
|
// JSON API for widgets - publicly accessible
|
|
83
83
|
publicRouter.get("/api/commits", commitsController.api);
|
|
84
84
|
publicRouter.get("/api/stars", starsController.api);
|
|
85
|
+
publicRouter.get("/api/contributions", contributionsController.api);
|
|
85
86
|
publicRouter.get("/api/activity", activityController.api);
|
|
86
87
|
publicRouter.get("/api/featured", featuredController.api);
|
|
87
88
|
|
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
import { GitHubClient } from "../github-client.js";
|
|
2
2
|
import * as utils from "../utils.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Fetch contributions using GitHub Search API
|
|
6
|
+
* More reliable than Events API which has 90-day/300-event limits
|
|
7
|
+
* @param {GitHubClient} client - GitHub client
|
|
8
|
+
* @param {string} username - GitHub username
|
|
9
|
+
* @param {number} limit - Max contributions to return
|
|
10
|
+
* @returns {Promise<Array>} - Formatted contributions
|
|
11
|
+
*/
|
|
12
|
+
async function fetchContributionsFromSearch(client, username, limit = 10) {
|
|
13
|
+
try {
|
|
14
|
+
const [prsResult, issuesResult] = await Promise.all([
|
|
15
|
+
client.getUserPRs(username, limit),
|
|
16
|
+
client.getUserIssues(username, limit),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const prs = (prsResult.items || []).map((item) => ({
|
|
20
|
+
type: "pr",
|
|
21
|
+
title: utils.truncate(item.title),
|
|
22
|
+
url: item.html_url,
|
|
23
|
+
repo: item.repository_url?.split("/").slice(-2).join("/"),
|
|
24
|
+
repoUrl: item.html_url?.split("/pull/")[0],
|
|
25
|
+
number: item.number,
|
|
26
|
+
date: item.created_at,
|
|
27
|
+
state: item.state,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const issues = (issuesResult.items || []).map((item) => ({
|
|
31
|
+
type: "issue",
|
|
32
|
+
title: utils.truncate(item.title),
|
|
33
|
+
url: item.html_url,
|
|
34
|
+
repo: item.repository_url?.split("/").slice(-2).join("/"),
|
|
35
|
+
repoUrl: item.html_url?.split("/issues/")[0],
|
|
36
|
+
number: item.number,
|
|
37
|
+
date: item.created_at,
|
|
38
|
+
state: item.state,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
// Combine, sort by date, and limit
|
|
42
|
+
return [...prs, ...issues]
|
|
43
|
+
.toSorted((a, b) => new Date(b.date) - new Date(a.date))
|
|
44
|
+
.slice(0, limit);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("[contributions] Error fetching from search:", error.message);
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
4
51
|
/**
|
|
5
52
|
* Display PRs and issues created by user
|
|
6
53
|
* @type {import("express").RequestHandler}
|
|
@@ -20,27 +67,26 @@ export const contributionsController = {
|
|
|
20
67
|
|
|
21
68
|
const client = new GitHubClient({ token, cacheTtl });
|
|
22
69
|
|
|
23
|
-
|
|
70
|
+
// Try events API first
|
|
71
|
+
let contributions = [];
|
|
24
72
|
try {
|
|
25
|
-
events = await client.getUserEvents(username, 100);
|
|
73
|
+
const events = await client.getUserEvents(username, 100);
|
|
74
|
+
contributions = utils.extractContributions(events);
|
|
26
75
|
} catch (apiError) {
|
|
27
|
-
console.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
76
|
+
console.log("[contributions] Events API error:", apiError.message);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Fallback: use Search API if events didn't have contributions
|
|
80
|
+
if (contributions.length === 0) {
|
|
81
|
+
console.log("[contributions] Events API returned no contributions, using Search API");
|
|
82
|
+
contributions = await fetchContributionsFromSearch(
|
|
83
|
+
client,
|
|
84
|
+
username,
|
|
85
|
+
limits.contributions * 2
|
|
86
|
+
);
|
|
39
87
|
}
|
|
40
88
|
|
|
41
|
-
|
|
42
|
-
.extractContributions(events)
|
|
43
|
-
.slice(0, limits.contributions * 2);
|
|
89
|
+
contributions = contributions.slice(0, limits.contributions * 2);
|
|
44
90
|
|
|
45
91
|
response.render("contributions", {
|
|
46
92
|
title: response.locals.__("github.contributions.title"),
|
|
@@ -57,4 +103,40 @@ export const contributionsController = {
|
|
|
57
103
|
next(error);
|
|
58
104
|
}
|
|
59
105
|
},
|
|
106
|
+
|
|
107
|
+
async api(request, response, next) {
|
|
108
|
+
try {
|
|
109
|
+
const { username, token, cacheTtl, limits } =
|
|
110
|
+
request.app.locals.application.githubConfig;
|
|
111
|
+
|
|
112
|
+
if (!username) {
|
|
113
|
+
return response.status(400).json({ error: "No username configured" });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const client = new GitHubClient({ token, cacheTtl });
|
|
117
|
+
|
|
118
|
+
// Try events API first
|
|
119
|
+
let contributions = [];
|
|
120
|
+
try {
|
|
121
|
+
const events = await client.getUserEvents(username, 100);
|
|
122
|
+
contributions = utils.extractContributions(events);
|
|
123
|
+
} catch (apiError) {
|
|
124
|
+
console.log("[contributions API] Events API error:", apiError.message);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fallback: use Search API if events didn't have contributions
|
|
128
|
+
if (contributions.length === 0) {
|
|
129
|
+
console.log("[contributions API] Events API returned no contributions, using Search API");
|
|
130
|
+
contributions = await fetchContributionsFromSearch(
|
|
131
|
+
client,
|
|
132
|
+
username,
|
|
133
|
+
limits.contributions
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
response.json({ contributions: contributions.slice(0, limits.contributions) });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
next(error);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
60
142
|
};
|
|
@@ -1,6 +1,99 @@
|
|
|
1
1
|
import { GitHubClient } from "../github-client.js";
|
|
2
2
|
import * as utils from "../utils.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Fetch commits from user's recently pushed repos
|
|
6
|
+
* Used as fallback when Events API doesn't include commit details
|
|
7
|
+
* @param {GitHubClient} client - GitHub client
|
|
8
|
+
* @param {string} username - GitHub username
|
|
9
|
+
* @param {number} limit - Max commits to return
|
|
10
|
+
* @returns {Promise<Array>} - Formatted commits
|
|
11
|
+
*/
|
|
12
|
+
async function fetchCommitsFromRepos(client, username, limit = 10) {
|
|
13
|
+
try {
|
|
14
|
+
const repos = await client.getUserRepos(username, 8, "pushed");
|
|
15
|
+
if (!Array.isArray(repos) || repos.length === 0) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const commitPromises = repos.slice(0, 5).map(async (repo) => {
|
|
20
|
+
try {
|
|
21
|
+
const repoCommits = await client.getRepoCommits(
|
|
22
|
+
repo.owner?.login || username,
|
|
23
|
+
repo.name,
|
|
24
|
+
5,
|
|
25
|
+
);
|
|
26
|
+
return repoCommits.map((c) => ({
|
|
27
|
+
sha: c.sha.slice(0, 7),
|
|
28
|
+
message: utils.truncate(c.commit?.message?.split("\n")[0]),
|
|
29
|
+
url: c.html_url,
|
|
30
|
+
repo: repo.full_name,
|
|
31
|
+
repoUrl: repo.html_url,
|
|
32
|
+
date: c.commit?.author?.date || c.commit?.committer?.date,
|
|
33
|
+
author: c.commit?.author?.name,
|
|
34
|
+
}));
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const commitResults = await Promise.all(commitPromises);
|
|
41
|
+
return commitResults
|
|
42
|
+
.flat()
|
|
43
|
+
.toSorted((a, b) => new Date(b.date) - new Date(a.date))
|
|
44
|
+
.slice(0, limit);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error("[dashboard] Error fetching commits from repos:", error.message);
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fetch contributions using GitHub Search API
|
|
53
|
+
* More reliable than Events API which has 90-day/300-event limits
|
|
54
|
+
* @param {GitHubClient} client - GitHub client
|
|
55
|
+
* @param {string} username - GitHub username
|
|
56
|
+
* @param {number} limit - Max contributions to return
|
|
57
|
+
* @returns {Promise<Array>} - Formatted contributions
|
|
58
|
+
*/
|
|
59
|
+
async function fetchContributionsFromSearch(client, username, limit = 10) {
|
|
60
|
+
try {
|
|
61
|
+
const [prsResult, issuesResult] = await Promise.all([
|
|
62
|
+
client.getUserPRs(username, limit),
|
|
63
|
+
client.getUserIssues(username, limit),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const prs = (prsResult.items || []).map((item) => ({
|
|
67
|
+
type: "pr",
|
|
68
|
+
title: utils.truncate(item.title),
|
|
69
|
+
url: item.html_url,
|
|
70
|
+
repo: item.repository_url?.split("/").slice(-2).join("/"),
|
|
71
|
+
repoUrl: item.html_url?.split("/pull/")[0],
|
|
72
|
+
number: item.number,
|
|
73
|
+
date: item.created_at,
|
|
74
|
+
state: item.state,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const issues = (issuesResult.items || []).map((item) => ({
|
|
78
|
+
type: "issue",
|
|
79
|
+
title: utils.truncate(item.title),
|
|
80
|
+
url: item.html_url,
|
|
81
|
+
repo: item.repository_url?.split("/").slice(-2).join("/"),
|
|
82
|
+
repoUrl: item.html_url?.split("/issues/")[0],
|
|
83
|
+
number: item.number,
|
|
84
|
+
date: item.created_at,
|
|
85
|
+
state: item.state,
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
return [...prs, ...issues]
|
|
89
|
+
.toSorted((a, b) => new Date(b.date) - new Date(a.date))
|
|
90
|
+
.slice(0, limit);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error("[dashboard] Error fetching contributions from search:", error.message);
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
4
97
|
/**
|
|
5
98
|
* Display GitHub activity dashboard
|
|
6
99
|
* @type {import("express").RequestHandler}
|
|
@@ -70,11 +163,25 @@ export const dashboardController = {
|
|
|
70
163
|
}
|
|
71
164
|
|
|
72
165
|
console.log("[GitHub Endpoint] Processing data...");
|
|
73
|
-
|
|
74
|
-
console.log("[GitHub Endpoint] Extracted commits:", commits?.length);
|
|
166
|
+
let commits = utils.extractCommits(events);
|
|
167
|
+
console.log("[GitHub Endpoint] Extracted commits from events:", commits?.length);
|
|
168
|
+
|
|
169
|
+
// Fallback: fetch commits directly from repos if events didn't have them
|
|
170
|
+
if (commits.length === 0) {
|
|
171
|
+
console.log("[GitHub Endpoint] Events API returned no commits, fetching from repos");
|
|
172
|
+
commits = await fetchCommitsFromRepos(client, username, limits.commits || 10);
|
|
173
|
+
console.log("[GitHub Endpoint] Fetched commits from repos:", commits?.length);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let contributions = utils.extractContributions(events);
|
|
177
|
+
console.log("[GitHub Endpoint] Extracted contributions from events:", contributions?.length);
|
|
75
178
|
|
|
76
|
-
|
|
77
|
-
|
|
179
|
+
// Fallback: use Search API if events didn't have contributions
|
|
180
|
+
if (contributions.length === 0) {
|
|
181
|
+
console.log("[GitHub Endpoint] Events API returned no contributions, using Search API");
|
|
182
|
+
contributions = await fetchContributionsFromSearch(client, username, limits.contributions || 10);
|
|
183
|
+
console.log("[GitHub Endpoint] Fetched contributions from search:", contributions?.length);
|
|
184
|
+
}
|
|
78
185
|
|
|
79
186
|
const stars = utils.formatStarred(starred);
|
|
80
187
|
console.log("[GitHub Endpoint] Formatted stars:", stars?.length);
|
package/package.json
CHANGED