Skip to content

Commit 06c283f

Browse files
authored
feat: implement fetch for github sponsors sponsorhip
1 parent 0555e2f commit 06c283f

File tree

4 files changed

+165
-3
lines changed

4 files changed

+165
-3
lines changed

apps/site/components/Common/Supporters/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Supporter } from '#site/types';
44
import type { FC } from 'react';
55

66
type SupportersListProps = {
7-
supporters: Array<Supporter<'opencollective'>>;
7+
supporters: Array<Supporter<'opencollective' | 'github'>>;
88
};
99

1010
const SupportersList: FC<SupportersListProps> = ({ supporters }) => (

apps/site/next-data/generators/supportersData.mjs

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { OPENCOLLECTIVE_MEMBERS_URL } from '#site/next.constants.mjs';
1+
import {
2+
OPENCOLLECTIVE_MEMBERS_URL,
3+
GITHUB_GRAPHQL_URL,
4+
GITHUB_API_KEY,
5+
} from '#site/next.constants.mjs';
26
import { fetchWithRetry } from '#site/util/fetch';
37

48
/**
@@ -26,4 +30,156 @@ async function fetchOpenCollectiveData() {
2630
return members;
2731
}
2832

29-
export default fetchOpenCollectiveData;
33+
/**
34+
* Fetches supporters data from Github API, filters active backers,
35+
* and maps it to the Supporters type.
36+
*
37+
* @returns {Promise<Array<import('#site/types/supporters').GithubSponsorSupporter>>} Array of supporters
38+
*/
39+
async function fetchGithubSponsorsData() {
40+
if (!GITHUB_API_KEY) {
41+
return [];
42+
}
43+
44+
const sponsors = [];
45+
46+
// Fetch sponsorship pages
47+
let cursor = null;
48+
49+
while (true) {
50+
const query = sponsorshipsQuery(cursor);
51+
const data = await graphql(query);
52+
53+
if (data.errors) {
54+
throw new Error(JSON.stringify(data.errors));
55+
}
56+
57+
const nodeRes = data.data.user?.sponsorshipsAsMaintainer;
58+
if (!nodeRes) {
59+
break;
60+
}
61+
62+
const { nodes, pageInfo } = nodeRes;
63+
const mapped = nodes.map(n => {
64+
const s = n.sponsor || n.sponsorEntity || n.sponsorEntity; // support different field names
65+
return {
66+
id: s?.id || null,
67+
login: s?.login || null,
68+
name: s?.name || s?.login || null,
69+
avatar: s?.avatarUrl || null,
70+
url: s?.websiteUrl || s?.url || null,
71+
};
72+
});
73+
74+
sponsors.push(...mapped);
75+
76+
if (!pageInfo.hasNextPage) {
77+
break;
78+
}
79+
80+
cursor = pageInfo.endCursor;
81+
}
82+
return sponsors;
83+
}
84+
85+
function sponsorshipsQuery(cursor = null) {
86+
return `
87+
query {
88+
organization(login: "nodejs") {
89+
sponsorshipsAsMaintainer (first: 100, includePrivate: false, after: "${cursor}") {
90+
nodes {
91+
sponsor: sponsorEntity {
92+
...on User {
93+
id: databaseId,
94+
name,
95+
login,
96+
avatarUrl,
97+
url,
98+
websiteUrl
99+
}
100+
...on Organization {
101+
id: databaseId,
102+
name,
103+
login,
104+
avatarUrl,
105+
url,
106+
websiteUrl
107+
}
108+
},
109+
}
110+
pageInfo {
111+
endCursor
112+
startCursor
113+
hasNextPage
114+
hasPreviousPage
115+
}
116+
}
117+
}
118+
}`;
119+
}
120+
121+
// function donationsQuery(cursor = null) {
122+
// return `
123+
// query {
124+
// organization(login: "nodejs") {
125+
// sponsorsActivities (first: 100, includePrivate: false, after: "${cursor}") {
126+
// nodes {
127+
// id
128+
// sponsor {
129+
// ...on User {
130+
// id: databaseId,
131+
// name,
132+
// login,
133+
// avatarUrl,
134+
// url,
135+
// websiteUrl
136+
// }
137+
// ...on Organization {
138+
// id: databaseId,
139+
// name,
140+
// login,
141+
// avatarUrl,
142+
// url,
143+
// websiteUrl
144+
// }
145+
// },
146+
// timestamp
147+
// }
148+
// }
149+
// }
150+
// }`;
151+
// }
152+
153+
const graphql = async (query, variables = {}) => {
154+
const res = await fetch(GITHUB_GRAPHQL_URL, {
155+
method: 'POST',
156+
headers: {
157+
'Content-Type': 'application/json',
158+
Authorization: `Bearer ${GITHUB_API_KEY}`,
159+
},
160+
body: JSON.stringify({ query, variables }),
161+
});
162+
163+
if (!res.ok) {
164+
const text = await res.text();
165+
throw new Error(`GitHub API error: ${res.status} ${text}`);
166+
}
167+
168+
return res.json();
169+
};
170+
171+
/**
172+
* Fetches supporters data from Open Collective API and GitHub Sponsors, filters active backers,
173+
* and maps it to the Supporters type.
174+
*
175+
* @returns {Promise<Array<import('#site/types/supporters').OpenCollectiveSupporter | import('#site/types/supporters').GithubSponsorSupporter>>} Array of supporters
176+
*/
177+
async function sponsorsData() {
178+
const sponsors = await Promise.all([
179+
fetchGithubSponsorsData(),
180+
fetchOpenCollectiveData(),
181+
]);
182+
return sponsors.flat();
183+
}
184+
185+
export default sponsorsData;

apps/site/next.constants.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,8 @@ export const VULNERABILITIES_URL =
219219
*/
220220
export const OPENCOLLECTIVE_MEMBERS_URL =
221221
'https://opencollective.com/nodejs/members/all.json';
222+
223+
/**
224+
* The location of Github Graphql API
225+
*/
226+
export const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql';

apps/site/types/supporters.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export type Supporter<T extends string> = {
77
};
88

99
export type OpenCollectiveSupporter = Supporter<'opencollective'>;
10+
export type GithubSponsorSupporter = Supporter<'github'>;

0 commit comments

Comments
 (0)