From f00eb17e27502df1c2c274e973baf2f654ec633c Mon Sep 17 00:00:00 2001 From: Gerardo Arriaga Rendon Date: Tue, 22 Mar 2022 18:20:58 -0400 Subject: [PATCH] Get GitHub issues from dependency repo --- pnpm-lock.yaml | 2 + src/api/dependency-discovery/package.json | 1 + .../src/dependency-list.js | 8 +++ src/api/dependency-discovery/src/github.js | 66 +++++++++++++++++++ src/api/dependency-discovery/src/router.js | 24 ++++++- 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/api/dependency-discovery/src/github.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 139e0de456..b1e02cb315 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,6 +155,7 @@ importers: src/api/dependency-discovery: specifiers: + '@octokit/request': 5.6.3 '@senecacdot/eslint-config-telescope': 1.1.0 '@senecacdot/satellite': ^1.27.0 env-cmd: 10.1.0 @@ -163,6 +164,7 @@ importers: query-registry: 2.2.0 supertest: 6.1.6 dependencies: + '@octokit/request': 5.6.3 '@senecacdot/satellite': 1.27.0 query-registry: 2.2.0 supertest: 6.1.6 diff --git a/src/api/dependency-discovery/package.json b/src/api/dependency-discovery/package.json index a1f2062200..c94be098b6 100644 --- a/src/api/dependency-discovery/package.json +++ b/src/api/dependency-discovery/package.json @@ -21,6 +21,7 @@ }, "homepage": "https://github.com/Seneca-CDOT/telescope#readme", "dependencies": { + "@octokit/request": "5.6.3", "@senecacdot/satellite": "^1.27.0", "query-registry": "2.2.0", "supertest": "6.1.6" diff --git a/src/api/dependency-discovery/src/dependency-list.js b/src/api/dependency-discovery/src/dependency-list.js index e3f8485ea9..f7ffdc7c71 100644 --- a/src/api/dependency-discovery/src/dependency-list.js +++ b/src/api/dependency-discovery/src/dependency-list.js @@ -2,6 +2,7 @@ const { readFile } = require('fs/promises'); const { join } = require('path'); const { cwd } = require('process'); const { getPackument } = require('query-registry'); +const { requestGitHubInfo } = require('./github'); const getDependencies = (function () { let dependencies = null; @@ -51,8 +52,15 @@ async function getNpmPackageInfo(packageName) { return dependencies[packageName]; } +async function getGitHubIssues(packageName) { + const npmPackage = await getNpmPackageInfo(packageName); + + return requestGitHubInfo(packageName, npmPackage.gitRepository.url); +} + module.exports = { getDependencyList, getNpmPackageInfo, isPackageDependency, + getGitHubIssues, }; diff --git a/src/api/dependency-discovery/src/github.js b/src/api/dependency-discovery/src/github.js new file mode 100644 index 0000000000..4d313e0ebb --- /dev/null +++ b/src/api/dependency-discovery/src/github.js @@ -0,0 +1,66 @@ +const { request } = require('@octokit/request'); + +// Check whether a specific time has reached the GitHub time limit. +// @param {Date} datetime - a Date object representing a moment in the past +// @returns {Boolean} +const hasGitHubExpired = (dateTime) => new Date() - dateTime > 60 * 60 * 1000; + +const gitHubInformationRequestor = {}; + +async function constructGitHubRequest(gitHubUrl) { + const labels = ['hacktoberfest', 'good first issue', 'help wanted']; + + const [owner, repo] = new URL(gitHubUrl).pathname.substring(1).split('/'); + + const requestPromises = labels.map((label) => { + return request('GET /repos/{owner}/{repo}/issues{?assignee,state,labels}', { + owner, + repo, + assignee: 'none', + state: 'open', + labels: label, + }); + }); + + // A single request might look like a response in + // https://docs.github.com/en/rest/reference/issues#list-repository-issues + const responses = await Promise.all(requestPromises); + + return responses + .filter((response) => response.status === 200) + .flatMap((response) => + response.data.map((issue) => { + return { + htmlUrl: issue.html_url, + title: issue.title, + body: issue.body, + createdAt: issue.created_at, + }; + }) + ) + .filter((issue, index, array) => array.findIndex((i) => i.htmlUrl === issue.htmlUrl) === index); +} + +function requestGitHubInfo(packageName, gitHubUrl) { + const cachedRequest = gitHubInformationRequestor[packageName]; + + if (cachedRequest && !hasGitHubExpired(cachedRequest.expireAt)) { + return cachedRequest.request; + } + + const newRequest = constructGitHubRequest(gitHubUrl); + + const now = new Date(); + now.setHours(now.getHours() + 1); + + gitHubInformationRequestor[packageName] = { + expireAt: now, + request: newRequest, + }; + + return newRequest; +} + +module.exports = { + requestGitHubInfo, +}; diff --git a/src/api/dependency-discovery/src/router.js b/src/api/dependency-discovery/src/router.js index 2b84133d08..6c62d8850f 100644 --- a/src/api/dependency-discovery/src/router.js +++ b/src/api/dependency-discovery/src/router.js @@ -1,5 +1,10 @@ const { Router } = require('@senecacdot/satellite'); -const { getDependencyList, getNpmPackageInfo, isPackageDependency } = require('./dependency-list'); +const { + getDependencyList, + getNpmPackageInfo, + getGitHubIssues, + isPackageDependency, +} = require('./dependency-list'); const router = Router(); @@ -33,4 +38,21 @@ router.get('/projects/:namespace/:name?', async (req, res, next) => { } }); +router.get('/github/:namespace/:name?', async (req, res, next) => { + const { namespace, name } = req.params; + const packageName = name ? `${namespace}/${name}` : namespace; + + try { + if (!(await isPackageDependency(packageName))) { + res.status(404).json({ msg: `${packageName} is not a Telescope dependency` }); + next(); + } else { + res.set('Cache-Control', 'max-age=3600'); + res.status(200).json(await getGitHubIssues(packageName)); + } + } catch (err) { + next(err); + } +}); + module.exports = router;