woodpecker/web/src/lib/api/client.ts

123 lines
3.1 KiB
TypeScript
Raw Normal View History

export interface ApiError {
status: number;
message: string;
}
type QueryParams = Record<string, string | number | boolean>;
export function encodeQueryString(_params: unknown = {}): string {
const __params = _params as QueryParams;
const params: QueryParams = {};
Object.keys(__params).forEach((key) => {
const val = __params[key];
if (val !== undefined) {
params[key] = val;
}
});
return Object.keys(params)
.sort()
.map((key) => {
const val = params[key];
return `${encodeURIComponent(key)}=${encodeURIComponent(val)}`;
})
.join('&');
}
export default class ApiClient {
server: string;
token: string | null;
csrf: string | null;
onerror: ((err: ApiError) => void) | undefined;
constructor(server: string, token: string | null, csrf: string | null) {
this.server = server;
this.token = token;
this.csrf = csrf;
}
2024-06-10 10:13:13 +00:00
private async _request(method: string, path: string, data?: unknown): Promise<unknown> {
2023-10-08 15:49:13 +00:00
const res = await fetch(`${this.server}${path}`, {
method,
headers: {
...(method !== 'GET' && this.csrf !== null ? { 'X-CSRF-TOKEN': this.csrf } : {}),
...(this.token !== null ? { Authorization: `Bearer ${this.token}` } : {}),
...(data !== undefined ? { 'Content-Type': 'application/json' } : {}),
2023-10-08 15:49:13 +00:00
},
body: data !== undefined ? JSON.stringify(data) : undefined,
2023-10-08 15:49:13 +00:00
});
2023-10-08 15:49:13 +00:00
if (!res.ok) {
2024-08-07 16:47:13 +00:00
let message = res.statusText;
const resText = await res.text();
if (resText) {
message = `${res.statusText}: ${resText}`;
}
2023-10-08 15:49:13 +00:00
const error: ApiError = {
status: res.status,
2024-08-07 16:47:13 +00:00
message,
2023-10-08 15:49:13 +00:00
};
if (this.onerror) {
this.onerror(error);
}
2024-08-07 16:47:13 +00:00
throw new Error(message);
}
2023-10-08 15:49:13 +00:00
const contentType = res.headers.get('Content-Type');
if (contentType !== null && contentType.startsWith('application/json')) {
2023-10-08 15:49:13 +00:00
return res.json();
}
2023-10-08 15:49:13 +00:00
return res.text();
}
async _get(path: string) {
2024-06-10 10:13:13 +00:00
return this._request('GET', path);
}
async _post(path: string, data?: unknown) {
return this._request('POST', path, data);
}
async _patch(path: string, data?: unknown) {
return this._request('PATCH', path, data);
}
async _delete(path: string) {
2024-06-10 10:13:13 +00:00
return this._request('DELETE', path);
}
_subscribe<T>(path: string, callback: (data: T) => void, opts = { reconnect: true }) {
const query = encodeQueryString({
access_token: this.token ?? undefined,
});
let _path = this.server ? this.server + path : path;
_path = this.token !== null ? `${_path}?${query}` : _path;
const events = new EventSource(_path);
events.onmessage = (event) => {
const data = JSON.parse(event.data as string) as T;
2024-08-17 06:09:48 +00:00
// eslint-disable-next-line promise/prefer-await-to-callbacks
callback(data);
};
if (!opts.reconnect) {
events.onerror = (err) => {
// TODO: check if such events really have a data property
if ((err as Event & { data: string }).data === 'eof') {
events.close();
}
};
}
return events;
}
setErrorHandler(onerror: (err: ApiError) => void) {
this.onerror = onerror;
}
}