@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 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
- let events = [];
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.error("GitHub API error:", apiError);
28
- return response.render("contributions", {
29
- title: response.locals.__("github.contributions.title"),
30
- actions: [],
31
- parent: {
32
- href: request.baseUrl,
33
- text: response.locals.__("github.title"),
34
- },
35
- error: {
36
- message: apiError.message || "Failed to fetch contributions",
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
- const contributions = utils
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
- const commits = utils.extractCommits(events);
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
- const contributions = utils.extractContributions(events);
77
- console.log("[GitHub Endpoint] Extracted contributions:", contributions?.length);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-github",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "GitHub activity endpoint for Indiekit. Display commits, stars, contributions, and featured repositories.",
5
5
  "keywords": [
6
6
  "indiekit",