All files you-track-http.ts

100% Statements 50/50
100% Branches 14/14
100% Functions 9/9
100% Lines 48/48

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 134 135 136 137 138 139 140  5x         76x                                     5x   78x 78x 1x 1x     77x 77x 201x 77x 77x 77x 77x 77x 77x 77x   77x 77x                                     5x   5x 5x 5x                                                   5x     20x 20x 20x 20x 20x 20x 27x 27x 27x 7x 7x   20x   27x   20x                   77x   77x 1x   76x 1x   75x 74x 74x   1x   3x    
import { Failure } from './api-types';
import { authorizationFor } from './you-track-oauth';
import { YouTrackError } from './you-track-rest';
 
function isYouTrackError<T>(value: any): value is YouTrackError {
  // noinspection SuspiciousTypeOfGuard
  return value && typeof value.error === 'string' && typeof value.error_description === 'string';
}
 
/**
 * Returns a promise that will be fulfilled with the result of an HTTP GET request to a YouTrack REST resource.
 *
 * This method sets the HTTP Authorization header if it is known due to a previous call to
 * {@link handlePotentialOauthRedirect}(). If no authorization is available, this method rejects the promise
 * immediately.
 *
 * @typeparam T the type of the response by YouTrack (after parsing the JSON)
 * @param baseUrl The YouTrack base URL to which relative paths of form `api/...` will be appended. The base URL is
 *     expected to end in a slash (/). For an InCloud instance without a custom domain, this is of form
 *     `https://<name>.myjetbrains.com/youtrack/`.
 * @param resourcePath relative path to the REST API resource requested
 * @param queryParams parameters that will be added to the query string
 * @return A promise that in case of success will be fulfilled with the retrieved object. In case of any failure, it
 *     will be rejected with a {@link Failure}.
 */
export function httpGet<T>(baseUrl: string, resourcePath: string, queryParams: {[param: string]: string} = {}):
    Promise<T> {
  const authorization: string | undefined = authorizationFor(baseUrl);
  if (authorization === undefined) {
    const failure: Failure = `No valid unexpired OAuth token available for ${baseUrl}.`;
    return Promise.reject(failure);
  }
 
  const url = new URL(resourcePath, baseUrl);
  const urlSearchParams = url.searchParams;
  Object.entries(queryParams).forEach(([key, value]) => urlSearchParams.append(key, value));
  const xhr = new XMLHttpRequest();
  xhr.open('GET', url.toString());
  xhr.setRequestHeader('Authorization', authorization);
  xhr.setRequestHeader('Accept', 'application/json');
  xhr.responseType = 'json';
  const promise = new Promise<T>((resolve, reject) => {
    xhr.onloadend = () => onRequestFinished<T>(url, xhr, resolve, reject);
  });
  xhr.send();
  return promise;
}
 
/**
 * Returns a promise that will be fulfilled with the result of an HTTP GET request to a YouTrack REST array resource.
 *
 * This method sets the HTTP Authorization header if it is known due to a previous call to
 * {@link handlePotentialOauthRedirect}(). If no authorization is available, this method rejects the promise
 * immediately.
 *
 * @typeparam T the element type of the array response by YouTrack (after parsing the JSON)
 * @param baseUrl The YouTrack base URL. See also {@link httpGet}().
 * @param resourcePath relative path to the REST API resource requested
 * @param queryParams parameters that will be added to the query string
 * @param restBatchSize Number of elements per HTTP request. Larger values are faster, but increase the risk of
 *     transmission problems (or outright rejection by future YouTrack versions that may have rate limitations).
 * @return A promise that in case of success will be fulfilled with the retrieved array. In case of any failure, it
 *     will be rejected with a {@link Failure}.
 */
export async function httpGetAll<T>(baseUrl: string, resourcePath: string,
    queryParams: {[param: string]: string}, restBatchSize: number): Promise<T[]> {
  return httpGetAllWithOptions<T, T[]>(baseUrl, resourcePath, queryParams, restBatchSize, (batch, array) => {
    array.push(...batch);
    return array;
  }, []);
}
 
/**
 * Returns a promise that will be fulfilled with a transformation of the result of an HTTP GET request to a YouTrack
 * REST array resource.
 *
 * This method sets the HTTP Authorization header if it is known due to a previous call to
 * {@link handlePotentialOauthRedirect}(). If no authorization is available, this method rejects the promise
 * immediately.
 *
 * @typeparam T the element type of the array response by YouTrack (after parsing the JSON)
 * @typeparam U the return type of `processBatch()` and therefore also this function
 * @param baseUrl The YouTrack base URL. See also {@link httpGet}().
 * @param resourcePath relative path to the REST API resource requested
 * @param queryParams parameters that will be added to the query string
 * @param restBatchSize Number of elements per HTTP request. See also {@link httpGetAll}().
 * @param processBatch callback called for the result of each individual HTTP request
 * @param processBatch.batch the retrieved array
 * @param processBatch.previous the state returned by the previous invocation of `processBatch()`, or the value of
 *     `initial` if this is the first invocation
 * @param initial the value passed to the first invocation of `processBatch()` as argument `initial`
 * @return A promise that in case of success will be fulfilled with the last result of `processBatch()`. In case of any
 *     failure, it will be rejected with a {@link Failure}.
 */
export async function httpGetAllWithOptions<T, U>(baseUrl: string, resourcePath: string,
    queryParams: {[param: string]: string}, restBatchSize: number,
    processBatch: (batch: T[], previous: U) => U, initial: U): Promise<U> {
  let numElementsRetrieved: number = 0;
  let busy = true;
  queryParams.$top = restBatchSize.toString();
  let batchPromise: Promise<T[]> = httpGet<T[]>(baseUrl, resourcePath, queryParams);
  let state: U = initial;
  do {
    const batch: T[] = await batchPromise;
    numElementsRetrieved += batch.length;
    if (batch.length >= restBatchSize) {
      queryParams.$skip = numElementsRetrieved.toString();
      batchPromise = httpGet<T[]>(baseUrl, resourcePath, queryParams);
    } else {
      busy = false;
    }
    state = processBatch(batch, state);
  } while (busy);
  return state;
}
 
/**
 * Handles completion of an {@link XMLHttpRequest}, whether successful or not.
 */
function onRequestFinished<T>(url: URL, xhr: XMLHttpRequest, resolve: (result: T) => void,
    reject: (failure: Failure) => void): void {
  // From https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response:
  // "The value is null if the request is not yet complete or was unsuccessful"
  const response: T | YouTrackError | null = xhr.response;
  let failure: Failure;
  if (response === null) {
    failure =
        `The YouTrack server could not be reached (URL: ${url}). Please check your network connection.`;
  } else if (isYouTrackError(response)) {
    failure =
        `The YouTrack server returned the following error (URL: ${url}): ${response.error_description}`;
  } else if (xhr.status === 200) {
    resolve(response);
    return;
  } else {
    failure = `The YouTrack server returned an unexpected error (URL: ${url}, HTTP status: ${xhr.status}).`;
  }
  reject(failure);
}