As it turns out, in GitHub API paginated requests, page numbering is 1-based. (https://developer.github.com/v3/#pagination) Starting at page 0 (which returns the first page), results in making the same request twice and logging incorrect numbers (since the first 100 items are listed twice). PR Close #25671
		
			
				
	
	
		
			112 lines
		
	
	
		
			3.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			112 lines
		
	
	
		
			3.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
// Imports
 | 
						|
import {IncomingMessage} from 'http';
 | 
						|
import * as https from 'https';
 | 
						|
import {assertNotMissingOrEmpty} from './utils';
 | 
						|
 | 
						|
// Constants
 | 
						|
const GITHUB_HOSTNAME = 'api.github.com';
 | 
						|
 | 
						|
// Interfaces - Types
 | 
						|
interface RequestParams {
 | 
						|
  [key: string]: string | number;
 | 
						|
}
 | 
						|
 | 
						|
type RequestParamsOrNull = RequestParams | null;
 | 
						|
 | 
						|
// Classes
 | 
						|
export class GithubApi {
 | 
						|
  protected requestHeaders: {[key: string]: string};
 | 
						|
 | 
						|
  // Constructor
 | 
						|
  constructor(githubToken: string) {
 | 
						|
    assertNotMissingOrEmpty('githubToken', githubToken);
 | 
						|
 | 
						|
    this.requestHeaders = {
 | 
						|
      'Authorization': `token ${githubToken}`,
 | 
						|
      'User-Agent': `Node/${process.versions.node}`,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  // Methods - Public
 | 
						|
  public get<T = any>(pathname: string, params?: RequestParamsOrNull): Promise<T> {
 | 
						|
    const path = this.buildPath(pathname, params);
 | 
						|
    return this.request<T>('get', path);
 | 
						|
  }
 | 
						|
 | 
						|
  public post<T = any>(pathname: string, params?: RequestParamsOrNull, data?: any): Promise<T> {
 | 
						|
    const path = this.buildPath(pathname, params);
 | 
						|
    return this.request<T>('post', path, data);
 | 
						|
  }
 | 
						|
 | 
						|
  // In GitHub API paginated requests, page numbering is 1-based. (https://developer.github.com/v3/#pagination)
 | 
						|
  public getPaginated<T>(pathname: string, baseParams: RequestParams = {}, currentPage: number = 1): Promise<T[]> {
 | 
						|
    const perPage = 100;
 | 
						|
    const params = {
 | 
						|
      ...baseParams,
 | 
						|
      page: currentPage,
 | 
						|
      per_page: perPage,
 | 
						|
    };
 | 
						|
 | 
						|
    return this.get<T[]>(pathname, params).then(items => {
 | 
						|
      if (items.length < perPage) {
 | 
						|
        return items;
 | 
						|
      }
 | 
						|
 | 
						|
      return this.getPaginated<T>(pathname, baseParams, currentPage + 1).then(moreItems => [...items, ...moreItems]);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  // Methods - Protected
 | 
						|
  protected buildPath(pathname: string, params?: RequestParamsOrNull): string {
 | 
						|
    if (params == null) {
 | 
						|
      return pathname;
 | 
						|
    }
 | 
						|
 | 
						|
    const search = (params === null) ? '' : this.serializeSearchParams(params);
 | 
						|
    const joiner = search && '?';
 | 
						|
 | 
						|
    return `${pathname}${joiner}${search}`;
 | 
						|
  }
 | 
						|
 | 
						|
  protected request<T>(method: string, path: string, data: any = null): Promise<T> {
 | 
						|
    return new Promise<T>((resolve, reject) => {
 | 
						|
      const options = {
 | 
						|
        headers: {...this.requestHeaders},
 | 
						|
        host: GITHUB_HOSTNAME,
 | 
						|
        method,
 | 
						|
        path,
 | 
						|
      };
 | 
						|
 | 
						|
      const onError = (statusCode: number, responseText: string) => {
 | 
						|
        const url = `https://${GITHUB_HOSTNAME}${path}`;
 | 
						|
        reject(`Request to '${url}' failed (status: ${statusCode}): ${responseText}`);
 | 
						|
      };
 | 
						|
      const onSuccess = (responseText: string) => {
 | 
						|
        try { resolve(responseText && JSON.parse(responseText)); } catch (err) { reject(err); }
 | 
						|
      };
 | 
						|
      const onResponse = (res: IncomingMessage) => {
 | 
						|
        const statusCode = res.statusCode || -1;
 | 
						|
        const isSuccess = (200 <= statusCode) && (statusCode < 400);
 | 
						|
        let responseText = '';
 | 
						|
 | 
						|
        res.
 | 
						|
          on('data', d => responseText += d).
 | 
						|
          on('end', () => isSuccess ? onSuccess(responseText) : onError(statusCode, responseText)).
 | 
						|
          on('error', reject);
 | 
						|
      };
 | 
						|
 | 
						|
      https.
 | 
						|
        request(options, onResponse).
 | 
						|
        on('error', reject).
 | 
						|
        end(data && JSON.stringify(data));
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  protected serializeSearchParams(params: RequestParams): string {
 | 
						|
    return Object.keys(params).
 | 
						|
      filter(key => params[key] != null).
 | 
						|
      map(key => `${key}=${encodeURIComponent(String(params[key]))}`).
 | 
						|
      join('&');
 | 
						|
  }
 | 
						|
}
 |