Skip to content

Commit

Permalink
Resolve #11: Authenticate API requests (#12)
Browse files Browse the repository at this point in the history
* Upgrade packages
  • Loading branch information
31z4 authored Mar 18, 2018
1 parent 0156064 commit 867c2f7
Show file tree
Hide file tree
Showing 14 changed files with 391 additions and 153 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"es6": true,
"jest": true
},
"globals": {
"OAUTH_GATEWAY_URL": "https://exmaple.com",
"OAUTH_CLIENT_ID": "1234"
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
coverage/
node_modules/
node_modules/
yarn-error.log
8 changes: 4 additions & 4 deletions dist/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Show off your open source contributions</title>
<title>Show off your open source contributions and check out others</title>
</head>
<body>
<div id="root"></div>
Expand Down
17 changes: 16 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "my-contributions.github.io",
"version": "0.0.1",
"description": "Show off your open source contributions",
"description": "Show off your open source contributions and check out others",
"main": "index.js",
"homepage": "https://my-contributions.github.io",
"repository": "github:my-contributions/my-contributions.github.io",
Expand All @@ -23,6 +23,7 @@
"babel-core": "^6.26.0",
"babel-jest": "^21.2.0",
"babel-loader": "^7.1.2",
"babel-plugin-transform-builtin-extend": "^1.1.2",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
Expand All @@ -39,9 +40,23 @@
"presets": [
"env",
"react"
],
"plugins": [
[
"babel-plugin-transform-builtin-extend",
{
"globals": [
"Error"
]
}
]
]
},
"jest": {
"globals": {
"OAUTH_GATEWAY_URL": "https://exmaple.com/",
"OAUTH_CLIENT_ID": "1234"
},
"collectCoverage": true,
"coveragePathIgnorePatterns": [
"<rootDir>/dist/",
Expand Down
251 changes: 142 additions & 109 deletions src/github.js
Original file line number Diff line number Diff line change
@@ -1,139 +1,172 @@
function getLink(headers) {
const result = {
next: null,
last: null,
first: null,
prev: null,
};

const link = headers.get('Link');
if (link == null)
return result;

const urls = /<([^<>]+)>; rel="(next|last|first|prev)"/.exec(link);
if (urls == null || urls.length < 3) {
throw new Error('Pagination error');
}

let i = urls.length - 1;
while (i) {
result[urls[i]] = urls[i - 1];
i -= 2;
import {fetchJSON} from './utils';

async function getAccessToken(code) {
const response = await fetchJSON(
OAUTH_GATEWAY_URL + '?' +
'client_id=' + OAUTH_CLIENT_ID + '&' +
'code=' + code,
{method: 'POST'},
);
if (response.error || !response.access_token) {
throw new Error('Unable to get access token');
}

return result;
return response.access_token;
}

async function fetchURL(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Error fetching ' + url);
class AuthorizationError extends Error {}

class GitHub {
constructor(accessToken) {
this._authorization = {Authorization: 'token ' + accessToken};
}
return response;
}

async function fetchJSON(url) {
const response = await fetchURL(url);
return await response.json();
}
// Parses the Link header and returns a corresponding object.
// Please see https://developer.github.com/v3/guides/traversing-with-pagination/.
static _getPageLinks(headers) {
const result = {
next: null,
last: null,
first: null,
prev: null,
};

const link = headers.get('Link');
if (link == null)
return result;

const urls = /<([^<>]+)>; rel="(next|last|first|prev)"/.exec(link);
if (urls == null || urls.length < 3) {
throw new Error('Pagination error');
}

async function fetchPages(url, items) {
const response = await fetchURL(url);
const result = await response.json();
if (items) {
Array.prototype.push.apply(items, result.items);
let i = urls.length - 1;
while (i) {
result[urls[i]] = urls[i - 1];
i -= 2;
}

return result;
}

const link = getLink(response.headers);
if (link.next) {
await fetchPages(link.next, result.items);
static _htmlURL(type, author, repo) {
const q = encodeURIComponent(`type:${type} author:${author} repo:${repo}`);
return 'https://github.com/search?utf8=✓&q=' + q;
}

return result;
}
static _reducePullRequests(items) {
return items.reduce((result, value) => {
const url = value.repository_url;
const repository = result[url] || {
open: 0,
merged: 0,
closed: 0,
};

async function isMerged(url) {
const pr = await fetchJSON(url);
return pr.merged;
}
repository[value.state] += 1;
result[url] = repository;

async function fetchPullRequests(author) {
const q = encodeURIComponent('type:pr author:' + author);
const pullRequests = await fetchPages('https://api.github.com/search/issues?per_page=100&q=' + q);
return result;
}, {});
}

static _sortPullRequests(items) {
return items.sort((a, b) => {
const aCount = a.open + a.closed + a.merged;
const bCount = b.open + b.closed + b.merged;

const promises = pullRequests.items.map(async (item) => {
if (item.state == 'closed' && await isMerged(item.pull_request.url)) {
item.state = 'merged';
if (aCount == bCount) {
return a.repository.stargazers_count < b.repository.stargazers_count;
}
return aCount < bCount;
});
}

async _fetch(url) {
const response = await fetch(url, {headers: this._authorization});
if (response.status == 401) {
throw new AuthorizationError();
}
if (!response.ok) {
throw new Error('Could not fetch ' + url);
}
return item;
});

return await Promise.all(promises);
}
return response;
}

function htmlURL(type, author, repo) {
const q = encodeURIComponent(`type:${type} author:${author} repo:${repo}`);
return 'https://github.com/search?utf8=✓&q=' + q;
}
async _fetchJSON(url) {
const response = await this._fetch(url);
return await response.json();
}

function reducePullRequests(items) {
return items.reduce((result, value) => {
const url = value.repository_url;
const repository = result[url] || {
open: 0,
merged: 0,
closed: 0,
};
async _fetchRepositoryData(items, author) {
const promises = Object.entries(items).map(async (entry) => {
const repository = await this._fetchJSON(entry[0]);
return {
repository: {
html_url: repository.html_url,
full_name: repository.full_name,
stargazers_count: repository.stargazers_count,
language: repository.language,
},
open: entry[1].open,
closed: entry[1].closed,
merged: entry[1].merged,
html_url: GitHub._htmlURL('pr', author, repository.full_name),
};
});

return await Promise.all(promises);
}

repository[value.state] += 1;
result[url] = repository;
async _fetchPages(url, items) {
const response = await this._fetch(url);
const result = await response.json();
if (items) {
Array.prototype.push.apply(items, result.items);
}

return result;
}, {});
}
const links = GitHub._getPageLinks(response.headers);
if (links.next) {
await this._fetchPages(links.next, result.items);
}

async function fetchRepositoryData(items, author) {
const promises = Object.entries(items).map(async (entry) => {
const repository = await fetchJSON(entry[0]);
return {
repository: {
html_url: repository.html_url,
full_name: repository.full_name,
stargazers_count: repository.stargazers_count,
language: repository.language,
},
open: entry[1].open,
closed: entry[1].closed,
merged: entry[1].merged,
html_url: htmlURL('pr', author, repository.full_name),
};
});
return result;
}

return await Promise.all(promises);
}
async _isMerged(url) {
const pr = await this._fetchJSON(url);
return pr.merged;
}

function sort(items) {
return items.sort((a, b) => {
const aCount = a.open + a.closed + a.merged;
const bCount = b.open + b.closed + b.merged;
async _fetchPullRequests(author) {
const q = encodeURIComponent('type:pr author:' + author);
const pullRequests = await this._fetchPages('https://api.github.com/search/issues?per_page=100&q=' + q);

if (aCount == bCount) {
return a.repository.stargazers_count < b.repository.stargazers_count;
}
return aCount < bCount;
});
}
const promises = pullRequests.items.map(async (item) => {
if (item.state == 'closed' && await this._isMerged(item.pull_request.url)) {
item.state = 'merged';
}
return item;
});

async function aggregatePullRequests(author) {
if (/[ :]/.test(author)) {
throw new Error('Invalid author');
return await Promise.all(promises);
}

const pullRequests = await fetchPullRequests(author);
const reduced = reducePullRequests(pullRequests);
const augmented = await fetchRepositoryData(reduced, author);
async aggregatePullRequests(author) {
if (/[ :]/.test(author)) {
throw new Error('Invalid author');
}

const pullRequests = await this._fetchPullRequests(author);
const reduced = GitHub._reducePullRequests(pullRequests);
const augmented = await this._fetchRepositoryData(reduced, author);

return sort(augmented);
return GitHub._sortPullRequests(augmented);
}
}

export default aggregatePullRequests;
const aggregatePullRequests = (author, accessToken) =>
new GitHub(accessToken).aggregatePullRequests(author);

export {aggregatePullRequests, getAccessToken, AuthorizationError};
Loading

0 comments on commit 867c2f7

Please sign in to comment.