blob: 65e4960eb4379ca12e707b481e98bcb9fdedf1c5 [file] [log] [blame]
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {HttpMethod} from '../../../constants/constants';
import {RequestPayload} from '../../../types/common';
import {ErrorCallback, RestPluginApi} from '../../../api/rest';
import {PluginApi} from '../../../api/plugin';
import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api';
import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
async function getErrorMessage(response: Response): Promise<string> {
const text = await response.text();
return text || `${response.status}`;
}
// This is an internal error, that must never be visible outside of this
// file. It is used only inside GrPluginRestApi.send method. See detailed
// explanation in the GrPluginRestApi.send method.
class ResponseError extends Error {
public constructor(readonly response: Response) {
super();
}
}
export class GrPluginRestApi implements RestPluginApi {
constructor(
private readonly restApi: RestApiService,
private readonly reporting: ReportingService,
readonly plugin: PluginApi,
private readonly prefix = ''
) {
this.reporting.trackApi(this.plugin, 'rest', 'constructor');
}
getLoggedIn() {
this.reporting.trackApi(this.plugin, 'rest', 'getLoggedIn');
return this.restApi.getLoggedIn();
}
getVersion() {
this.reporting.trackApi(this.plugin, 'rest', 'getVersion');
return this.restApi.getVersion();
}
getConfig() {
this.reporting.trackApi(this.plugin, 'rest', 'getConfig');
return this.restApi.getConfig();
}
invalidateReposCache() {
this.reporting.trackApi(this.plugin, 'rest', 'invalidateReposCache');
this.restApi.invalidateReposCache();
}
getAccount() {
this.reporting.trackApi(this.plugin, 'rest', 'getAccount');
return this.restApi.getAccount();
}
getRepos(filter: string, reposPerPage: number, offset?: number) {
this.reporting.trackApi(this.plugin, 'rest', 'getRepos');
return this.restApi.getRepos(filter, reposPerPage, offset);
}
fetch(
method: HttpMethod,
url: string,
payload?: RequestPayload,
errFn?: undefined,
contentType?: string
): Promise<Response>;
fetch(
method: HttpMethod,
url: string,
payload: RequestPayload | undefined,
errFn: ErrorCallback,
contentType?: string
): Promise<Response | void>;
fetch(
method: HttpMethod,
url: string,
payload: RequestPayload | undefined,
errFn?: ErrorCallback,
contentType?: string
): Promise<Response | void>;
/**
* Fetch and return native browser REST API Response.
*/
fetch(
method: HttpMethod,
url: string,
payload?: RequestPayload,
errFn?: ErrorCallback,
contentType?: string
): Promise<Response | void> {
this.reporting.trackApi(this.plugin, 'rest', 'fetch');
return this.restApi.send(
method,
this.prefix + url,
payload,
errFn,
contentType
);
}
/**
* Fetch and parse REST API response, if request succeeds.
*/
send<T>(
method: HttpMethod,
url: string,
payload?: RequestPayload,
errFn?: ErrorCallback,
contentType?: string
) {
this.reporting.trackApi(this.plugin, 'rest', 'send');
// Plugins typically don't want Gerrit to show error dialogs for failed
// requests. So we are defining a default errFn here, even if it is not
// explicitly set by the caller.
// TODO: We are soon getting rid of the `errFn` altogether. There are only
// 2 known usages of errFn in plugins: delete-project and verify-status.
errFn =
errFn ??
((response: Response | null | undefined, error?: Error) => {
if (error) throw error;
// Some plugins show an error message if send is failed, smth like:
// pluginApi.send(...).catch(err => showError(err));
// The response can contain an error text, but getting this text is
// an asynchronous operation. At the same time, the errFn must be a
// synchronous function.
// As a workaround, we throw an ResponseError here and then catch
// it inside a catch block below and read the message.
if (response) throw new ResponseError(response);
throw new Error('Generic REST API error.');
});
return this.fetch(method, url, payload, errFn, contentType)
.then(response => {
// Will typically not happen. The response can only be unset, if the
// errFn handles the error and then returns void or undefined or null.
// But the errFn above always throws.
if (!response) {
throw new Error('plugin rest-api call failed');
}
// Will typically not happen. errFn will have dealt with that and the
// caller will get a rejected promise already.
if (response.status < 200 || response.status >= 300) {
return getErrorMessage(response).then(msg =>
Promise.reject(new Error(msg))
);
} else {
return this.restApi.getResponseObject(response) as Promise<T>;
}
})
.catch(err => {
if (err instanceof ResponseError) {
return getErrorMessage(err.response).then(msg => {
throw new Error(msg);
});
}
throw err;
});
}
get<T>(url: string) {
this.reporting.trackApi(this.plugin, 'rest', 'get');
return this.send<T>(HttpMethod.GET, url);
}
post<T>(
url: string,
payload?: RequestPayload,
errFn?: ErrorCallback,
contentType?: string
) {
this.reporting.trackApi(this.plugin, 'rest', 'post');
return this.send<T>(HttpMethod.POST, url, payload, errFn, contentType);
}
put<T>(
url: string,
payload?: RequestPayload,
errFn?: ErrorCallback,
contentType?: string
) {
this.reporting.trackApi(this.plugin, 'rest', 'put');
return this.send<T>(HttpMethod.PUT, url, payload, errFn, contentType);
}
delete(url: string) {
this.reporting.trackApi(this.plugin, 'rest', 'delete');
return this.fetch(HttpMethod.DELETE, url).then(response => {
if (response.status !== 204) {
return response.text().then(text => {
if (text) {
return Promise.reject(new Error(text));
} else {
return Promise.reject(new Error(`${response.status}`));
}
});
}
return response;
});
}
}