GSA/code-gov-integrations

View on GitHub
libs/github/github.js

Summary

Maintainability
A
2 hrs
Test Coverage
const Octokit = require('@octokit/rest')
  .plugin(require('@octokit/plugin-throttling'));
const { parseRateLimit, handleRateLimit, getRateLimit, handleError } = require('./utils');

/**
 * Get an instance of the API client.
 *
 * @param {object} options Object with client creation options.
 * For details on Github client creation visit: https://www.npmjs.com/package/@octokit/rest
 * @returns {object} An initialized API client.
 *
 * @example getClient({ token: 'this-is-not-a-token' })
 */
function getClient(options) {
  return new Octokit({
    auth: `token ${options.token}`,
    throttle: {
      onRateLimit: (retryAfter, options) => {
        console.warn(
          `Request quota exhausted for request ${options.method} ${options.url}`);

        if (options.request.retryCount === 3) { // only retries once
          console.log(`Retrying after ${retryAfter} seconds!`);
          return true;
        }
      },
      onAbuseLimit: (retryAfter, options) => {
        // does not retry, only logs a warning
        console.warn(`Abuse detected for request ${options.method} ${options.url}`);
      }
    }
  });
}

/**
 * Fetch README file contents for the supplied repository.
 *
 * @async
 * @param {object} params Parameters needed by the API client and the client itself.
 * @param {string} params.owner Repository owner
 * @param {string} params.repo Repository name
 * @param {object} params.client API client
 * @returns {Promise<{ readme: string, rateLimit: object, error: object }>} Object with readme text, rate limit object, and error object.
 *
 * @example getRepoReadme({ owner: 'gsa', repo: 'code-gov-integrations', client: apiClient })
 */
async function getRepoReadme({ owner, repo, client }) {
  let readme;
  let rateLimit = {};
  let error = {};

  try {
    await handleRateLimit({
      rateLimit: await getRateLimit(client),
      client
    });

    const response = await client.repos.getReadme({
      owner,
      repo,
      ref: 'master'
    });
    readme = response.data;
    rateLimit = parseRateLimit(response);
  } catch(err) {
    const result = handleError(err);
    rateLimit = result.rateLimit;
    error = result.error;
  }

  return { readme, rateLimit, error };
}
/**
 * Fetch data for the supplied repository.
 *
 * @async
 * @param {object} params Parameters needed by the API client and the client itself.
 * @param {string} params.owner Repository owner
 * @param {string} params.repo Repository name
 * @param {object} params.client API client
 * @returns {Promise<{ repo: object, rateLimit: object, error: object }>} Object with repo data, rate limit, and error objects.
 *
 * @example getRepoData({ owner: 'gsa', repo: 'code-gov-integrations', client: apiClient })
 */
async function getRepoData({ owner, repo, client }) {
  let repoData = {};
  let rateLimit = {};
  let error = {};

  try {
    await handleRateLimit({
      rateLimit: await getRateLimit(client),
      client
    });

    const response = await client.repos.get({ owner, repo });

    repoData = {
      description: response.data.description,
      forks_count: response.data.forks_count,
      watchers_count: response.data.watchers_count,
      stargazers_count: response.data.stargazers_count,
      title: response.data.title,
      topics: response.data.topics,
      full_name: response.data.full_name,
      has_issues: response.data.has_issues,
      organization: {
        login: response.data.organization.login,
        avatar_url: response.data.organization.avatar_url,
        url: response.data.organization.url
      },
      ssh_url: response.data.ssh_url,
      created_at: response.data.created_at,
      updated_at: response.data.updated_at
    };

    rateLimit = parseRateLimit(response);
  } catch(err) {
    const result = handleError(err);
    rateLimit = result.rateLimit;
    error = result.error;
  }

  return {
    repo: repoData,
    rateLimit,
    error
  };
}

/**
 * Fetch all issues for a suplied repository.
 *
 * @async
 * @param {object} params Parameters needed by the API client and the client itself.
 * @param {string} params.owner Repository owner
 * @param {string} params.repo Repository name
 * @param {string} [params.state="open"] Indicates the state of the issues to return. Can be either open, closed, or all
 * @param {string} [params.labels="help wanted, code.gov"] A list of comma separated label names. Example: help wanted, code.gov, good first issue
 * @param {string} [params.since] Only issues updated at or after this time are returned. This is a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ.
 * @param {number} [params.per_page=100] Results per page (max 100). Defaults to 10
 * @param {number} [params.page=1] Page number of the results to fetch.
 * @param {object} params.client API client
 * @returns {Promise<{ issues: Array, rateLimit: object, error: object }>} Object with issues array, rate limit object, and error object.
 *
 * @example getRepoIssues({ owner: 'gsa', repo: 'code-gov-integrations', client: apiClient })
 * @example getRepoIssues({ owner: 'gsa', repo: 'code-gov-integrations', labels: 'help wanted,code.gov', client: apiClient })
 */
async function getRepoIssues({ owner, repo, state='open', labels='help wanted', since, per_page=100, page=1, client }) {
  let requestParams = { owner, repo, state, labels, per_page, page};
  let issues = [];
  let rateLimit = {};
  let error = {};

  if(since) {
    requestParams = Object.assign(requestParams, {since});
  }

  try {

    const data = await client.paginate(client.issues.listForRepo.endpoint(requestParams));

    // List have to be filtered to remove Pull Requests.
    // Github API v3 considers all Pull Requests issues. See: https://developer.github.com/v3/issues/#list-issues-for-a-repository
    const repoIssues = data.filter(issue => Object.prototype.hasOwnProperty.call(issue, 'pull_request') === false);

    issues = repoIssues.map(issue => {
      return {
        url: issue.html_url,
        state: issue.state,
        title: issue.title,
        description: issue.description,
        body: issue.body,
        labels: issue.labels.map(label => label.name),
        repository: repo,
        updated_at: issue.updated_at,
        merged_at: issue.merged_at,
        created_at: issue.created_at
      };
    });
  } catch(err) {
    const result = handleError(err);
    rateLimit = result.rateLimit;
    error = result.error;
  }

  return { issues, rateLimit, error };
}

/**
 * Fetch all languages used in the supplied repository.
 *
 * @async
 * @param {Object} params Parameters needed by the API client and the client itself.
 * @param {string} params.owner Repository owner
 * @param {string} params.repo Repository name
 * @param {object} params.client API client
 * @returns {Promise<{languages: Array, rateLimit: object, error: object}>} Object with langauges array, rate limit object, and error object.
 *
 * @example getRepoLanguages({ owner: 'gsa', repo: 'code-gov-integrations', client: apiClient })
 */
async function getRepoLanguages({ owner, repo, client }){
  let languages = [];
  let rateLimit = {};
  let error = {};

  try {
    const response = await client.repos.listLanguages({ owner, repo });

    languages = Object.keys(response.data);
    rateLimit = parseRateLimit(response);
  } catch(err) {
    const result = handleError(err);
    rateLimit = result.rateLimit;
    error = result.error;
  }

  return { languages, rateLimit, error };
}

/**
 * Fetch all contributors for the supplied repository.
 *
 * @async
 * @param {object} params Parameters needed by the API client and the client itself.
 * @param {string} params.owner Repository owner
 * @param {string} params.repo Repository name
 * @param {boolean} params.anon Include anonymous contributors. Defaults to false.
 * @param {number} params.per_page Results per page (max 100). Defaults to 10
 * @param {number} params.page Page number of the results to fetch.
 * @param {object} params.client Github API client
 * @returns {Promise<{contributors: Array, rateLimit: object, error: object }>} List of contributors to the passed repository
 *
 * @example getRepoContributors({ owner: 'gsa', repo: 'code-gov-integrations', client: apiClient })
 * @example getRepoContributors({ owner: 'gsa', repo: 'code-gov-integrations', per_page: 100, client: apiClient })
 */
async function getRepoContributors({ owner, repo, anon=false, per_page=10, page=1, client}) {
  const requestParams = { owner, repo, anon, per_page, page };

  let contributors = [];
  let rateLimit = {};
  let error = {};

  try {

    const results = await client.paginate(client.repos.listContributors.endpoint(requestParams));

    contributors = await Promise.all(
      results.map(async contributor => {
        const username = contributor.login;

        rateLimit = await getRateLimit(client);
        rateLimit = await handleRateLimit({ rateLimit, client });

        const user = await client.users.getByUsername({ username });

        return {
          username: contributor.login,
          contributions: contributor.contributions,
          full_name: user.data.name,
          email: user.data.email,
          gh_profile: user.data.html_url,
          gh_avatar_url: user.data.avatar_url,
          company: user.data.company
        };
      })
    );
  } catch(err) {
    const result = handleError(err);
    rateLimit = result.rateLimit;
    error = result.error;
  }

  return { contributors, rateLimit, error };
}

/**
 * Fetch all data for the supplied repository.
 * This includes general repository data, issues, contributors, languages, and README file.
 *
 * @param {object} params Parameters needed by the API client and the client itself.
 * @param {string} params.owner Repository owner
 * @param {string} params.repoName Repository name
 * @param {object} params.client API client instance.
 * @returns {Promise<object>} Object with repository data, repository issues, repository README,
 * and repository contributors
 *
 * @example getAllDataForRepo({ owner: 'gsa', repo: 'code-gov-integrations', client: apiClient })
 */
async function getAllDataForRepo({ owner, repoName, client }) {
  let repo;

  try {
    const repoData = await getRepoData({ owner, repo: repoName, client });
    repo = repoData.repo;
  } catch(error) {
    repo.error = JSON.stringify(error);
    throw error;
  }

  try {
    const readmeData = await getRepoReadme({ owner, repoName, client });
    repo.readMe = readmeData.readme;
  } catch(error) {
    repo.readMe = '';
  }

  try {
    const repoLanguages = await getRepoLanguages({ owner, repoName, client });
    repo.languages = repoLanguages.languages;
  } catch(error) {
    repo.languages = [];
  }

  if(repo.has_issues) {
    try {
      const issuesData = await getRepoIssues({ owner, repo: repoName, per_page:10, client });
      repo.issues = issuesData.issues;
    } catch(error) {
      repo.issues = [];
    }
  }

  try {
    repo.contributors = await getRepoContributors({owner, repo: repoName, per_page:10, client});
  } catch (error) {
    repo.contributions = [];
  }

  return repo;
}

module.exports = {
  getAllDataForRepo,
  getRepoReadme,
  getRepoData,
  getRepoIssues,
  getRepoLanguages,
  getClient,
  getRepoContributors
};