gitbeaker/src/infrastructure/RequestHelper.ts
2019-03-06 14:11:26 -05:00

283 lines
7.6 KiB
TypeScript

import Humps from 'humps';
import LinkParser from 'parse-link-header';
import QS from 'qs';
import URLJoin from 'url-join';
import StreamableRequest from 'request';
import { BaseService } from '.';
import { CommitAction } from '../services/Commits';
export interface RequestParametersInput {
url?: string;
headers: import('./BaseService').default['headers'];
json?: boolean;
body?: Object;
qs?: Object;
qsStringifyOptions? : Object;
formData?: temporaryAny;
resolveWithFullResponse?: boolean;
rejectUnauthorized?: boolean;
}
interface GetPaginatedOptions {
showPagination?: boolean;
maxPages?: number;
perPage?: number;
page?: number;
position?: temporaryAny;
}
type RequestParametersOutput = RequestParametersInput &
Required<Pick<RequestParametersInput, 'url'>>;
export async function wait(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function defaultRequest(
{ url, useXMLHttpRequest, rejectUnauthorized }: BaseService,
endpoint: string,
{ headers, body, qs, formData, resolveWithFullResponse = false }: RequestParametersInput,
): RequestParametersOutput {
const params: RequestParametersOutput = {
url: URLJoin(url, endpoint),
headers,
json: true,
};
if (body) params.body = Humps.decamelizeKeys(body);
if (qs) {
if (useXMLHttpRequest) {
// The xhr package doesn't have a way of passing in a qs object until v3
params.url = URLJoin(params.url, `?${QS.stringify(Humps.decamelizeKeys(qs), { arrayFormat: 'brackets' })}`);
} else {
params.qs = Humps.decamelizeKeys(qs);
params.qsStringifyOptions = { arrayFormat: 'brackets' };
}
}
if (formData) params.formData = formData;
params.resolveWithFullResponse = resolveWithFullResponse;
params.rejectUnauthorized = rejectUnauthorized;
return params;
}
function getStream(service: BaseService, endpoint: string, options: RequestOptions = {}) {
if (service.useXMLHttpRequest) {
throw new Error(
`Cannot use streaming functionality with XMLHttpRequest. Please instantiate without this
option to use streaming`,
);
}
const requestOptions = defaultRequest(service, endpoint, {
headers: service.headers,
qs: options,
});
return StreamableRequest.get(requestOptions);
}
async function getPaginated(
service: BaseService,
endpoint: string,
options: GetPaginatedOptions = {},
) {
const { showPagination, maxPages, ...queryOptions } = options;
const requestOptions = defaultRequest(service, endpoint, {
headers: service.headers,
qs: queryOptions,
resolveWithFullResponse: true,
});
const response = await service.requester.get(requestOptions);
const links = LinkParser(response.headers.link) || {};
const page = response.headers['x-page'];
const underMaxPageLimit = maxPages ? page < maxPages : true;
let more = [];
let data: temporaryAny;
// If not looking for a singular page and still under the max pages limit
// AND their is a next page, paginate
if (!queryOptions.page && underMaxPageLimit && links.next) {
// If redirected from http:// to https://, need to update service.url to avoid url inception
if (service.url.slice(0, 5) === 'http:' && links.next.url.slice(0, 5) === 'https') {
service.url = service.url.replace('http:', 'https:');
}
more = await getPaginated(service, links.next.url.replace(service.url, ''), options);
data = [...response.body, ...more];
} else {
data = response.body;
}
if ((queryOptions.page || maxPages) && showPagination) {
return {
data,
pagination: {
total: response.headers['x-total'],
next: response.headers['x-next-page'] || null,
current: response.headers['x-page'] || null,
previous: response.headers['x-prev-page'] || null,
perPage: response.headers['x-per-page'],
totalPages: response.headers['x-total-pages'],
},
};
}
return data;
}
type RequestType = 'post' | 'get' | 'put' | 'delete';
export interface RequestOptions {
targetIssueId?: string;
targetProjectId?: string;
content?: string;
id?: string;
sourceBranch?: string;
targetBranch?: string;
/** The duration in human format. e.g: 3h30m */
duration?: string;
domain?: string;
cron?: temporaryAny;
description?: string;
file?: {
value: Buffer;
options: {
filename: string;
contentType: 'application/octet-stream';
};
};
path?: string;
namespace?: string;
visibility?: string;
code?: string;
fileName?: string;
from?: string;
to?: string;
sha?: string;
runnerId?: string;
ref?: string;
scope?: string;
url?: string;
scopes?: temporaryAny;
expiresAt?: string;
note?: string;
actions?: CommitAction[];
commitMessage?: string;
branch?: string;
body?: string | temporaryAny;
title?: string;
name?: string;
labelId?: temporaryAny;
accessLevel?: number;
userId?: UserId;
position?: temporaryAny;
value?: string;
linkUrl?: string;
imageUrl?: string;
key?: string;
action?: string;
targetType?: string;
email?: string;
password?: string;
search?: string;
public?: boolean;
text?: string;
token?: string;
}
class RequestHelper {
static async request(
type: RequestType,
service: BaseService,
endpoint: string,
options: RequestOptions = {},
form = false,
stream = false,
): Promise<temporaryAny> {
try {
switch (type) {
case 'get':
if (stream) return await getStream(service, endpoint, options);
return await getPaginated(service, endpoint, options);
case 'post': {
const requestOptions = defaultRequest(service, endpoint, {
headers: service.headers,
[form ? 'formData' : 'body']: options,
});
return await service.requester.post(requestOptions);
}
case 'put': {
const requestOptions = defaultRequest(service, endpoint, {
headers: service.headers,
body: options,
});
return await service.requester.put(requestOptions);
}
case 'delete': {
const requestOptions = defaultRequest(service, endpoint, {
headers: service.headers,
qs: options,
});
return await service.requester.delete(requestOptions);
}
default:
throw new Error(`Unknown request type ${type}`);
}
} catch (err) {
await RequestHelper.handleRequestError(err);
return RequestHelper.request(type, service, endpoint, options, form, stream);
}
}
static async handleRequestError(err: temporaryAny) {
if (
!err.response ||
!err.response.headers ||
!err.response.headers['retry-after'] ||
parseInt(err.statusCode, 10) !== 429
) {
throw err;
}
const sleepTime = parseInt(err.response.headers['retry-after'], 10);
if (!sleepTime) throw err;
return wait(sleepTime * 1000);
}
static get(
service: BaseService,
endpoint: string,
options: RequestOptions = {},
{ stream = false } = {},
) {
return RequestHelper.request('get', service, endpoint, options, false, stream);
}
static post(service: BaseService, endpoint: string, options: RequestOptions = {}, form = false) {
return RequestHelper.request('post', service, endpoint, options, form);
}
static put(service: BaseService, endpoint: string, options: RequestOptions = {}) {
return RequestHelper.request('put', service, endpoint, options);
}
static delete(service: BaseService, endpoint: string, options: RequestOptions = {}) {
return RequestHelper.request('delete', service, endpoint, options);
}
}
export default RequestHelper;