/
supporters.js
133 lines (122 loc) · 3.6 KB
/
supporters.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#!/usr/bin/env node
'use strict';
const debug = require('debug')('mocha:docs:data:supporters');
const needle = require('needle');
const imageSize = require('image-size');
const blacklist = new Set(require('./blacklist.json'));
const API_ENDPOINT = 'https://api.opencollective.com/graphql/v2';
const query = `query account($limit: Int, $offset: Int, $slug: String) {
account(slug: $slug) {
orders(limit: $limit, offset: $offset) {
limit
offset
totalCount
nodes {
fromAccount {
name
slug
website
imgUrlMed: imageUrl(height:64)
imgUrlSmall: imageUrl(height:32)
type
}
totalDonations {
value
}
createdAt
}
}
}
}`;
const graphqlPageSize = 1000;
const nodeToSupporter = node => ({
name: node.fromAccount.name,
slug: node.fromAccount.slug,
website: node.fromAccount.website,
imgUrlMed: node.fromAccount.imgUrlMed,
imgUrlSmall: node.fromAccount.imgUrlSmall,
firstDonation: node.createdAt,
totalDonations: node.totalDonations.value * 100,
type: node.fromAccount.type
});
/**
* Retrieves donation data from OC
*
* Handles pagination
* @param {string} slug - Collective slug to get donation data from
* @returns {Promise<Object[]>} Array of raw donation data
*/
const getAllOrders = async (slug = 'mochajs') => {
let allOrders = [];
const variables = {limit: graphqlPageSize, offset: 0, slug};
// Handling pagination if necessary (2 pages for ~1400 results in May 2019)
while (true) {
const result = await needle(
'post',
API_ENDPOINT,
{query, variables},
{json: true}
);
const orders = result.body.data.account.orders.nodes;
allOrders = [...allOrders, ...orders];
variables.offset += graphqlPageSize;
if (orders.length < graphqlPageSize) {
debug('retrieved %d orders', allOrders.length);
return allOrders;
} else {
debug(
'loading page %d of orders...',
Math.floor(variables.offset / graphqlPageSize)
);
}
}
};
module.exports = async () => {
const orders = await getAllOrders();
// Deduplicating supporters with multiple orders
const uniqueSupporters = new Map();
const supporters = orders
.map(nodeToSupporter)
.filter(supporter => !blacklist.has(supporter.slug))
.reduce((supporters, supporter) => {
if (uniqueSupporters.has(supporter.slug)) {
// aggregate donation totals
uniqueSupporters.get(supporter.slug).totalDonations +=
supporter.totalDonations;
return supporters;
}
uniqueSupporters.set(supporter.slug, supporter);
return [...supporters, supporter];
}, [])
.sort((a, b) => b.totalDonations - a.totalDonations)
.reduce(
(supporters, supporter) => {
if (supporter.type === 'INDIVIDUAL') {
supporters.backers.push({
...supporter,
avatar: supporter.imgUrlSmall
});
} else {
supporters.sponsors.push({...supporter, avatar: supporter.imgUrlMed});
}
return supporters;
},
{sponsors: [], backers: []}
);
// Fetch images for sponsors and save their image dimensions
await Promise.all(
supporters.sponsors.map(async sponsor => {
for await (const chunk of needle.get(sponsor.avatar)) {
sponsor.dimensions = imageSize(chunk);
break;
}
})
);
debug(
'found %d valid backers and %d valid sponsors (%d total)',
supporters.backers.length,
supporters.sponsors.length,
supporters.backers.length + supporters.sponsors.length
);
return supporters;
};