@baqhub/sdk
Version:
The official JavaScript SDK for the BAQ federated app platform.
232 lines (231 loc) • 6.44 kB
JavaScript
import memoize from "lodash/memoize.js";
import pickBy from "lodash/pickBy.js";
import snakeCase from "lodash/snakeCase.js";
import { Constants } from "../constants.js";
import { AbortedError } from "../helpers/async.js";
import { CustomError } from "../helpers/customError.js";
import { Str } from "../helpers/string.js";
import { isDefined } from "../helpers/type.js";
import { Uuid } from "../helpers/uuid.js";
import { HttpMethod } from "../model/core/httpMethod.js";
import { fetchEventSource } from "./eventSource.js";
const getClientId = memoize(() => Uuid.new());
let api = {
fetch: (...args) => fetch(...args),
eventSourceFetch: (...args) => fetch(...args),
};
export function setHttpApi(newApi) {
api = newApi;
}
export class RequestError extends CustomError {
options;
status;
headers;
constructor(options, status, headers) {
super("Request error.");
this.options = options;
this.status = status;
this.headers = headers;
}
}
export class RequestFailedError extends CustomError {
baseError;
constructor(baseError) {
super("Request failed.");
this.baseError = baseError;
}
}
function isError(error, statuses) {
if (error instanceof RequestFailedError && !statuses) {
return true;
}
if (!(error instanceof RequestError)) {
return false;
}
if (!statuses) {
return true;
}
return statuses.includes(error.status);
}
async function httpHead(url, options) {
const response = await sendAsync({
...options,
method: HttpMethod.HEAD,
url,
});
return response.headers;
}
async function httpGet(url, options) {
const response = await sendAsync({
...options,
method: HttpMethod.GET,
url,
});
const json = await response.json();
return [response.headers, json];
}
async function httpDownload(url, options) {
const response = await sendAsync({
...options,
headers: {
Accept: "*/*",
...options?.headers,
},
method: HttpMethod.GET,
url,
});
const blob = await response.blob();
return [response.headers, blob];
}
async function httpPost(body, url, options = {}) {
const headers = {
"Content-Type": "application/json; charset=utf-8",
...options.headers,
};
const response = await sendAsync({
...options,
headers,
method: HttpMethod.POST,
url,
body,
});
const json = await response.json();
return [response.headers, json];
}
async function httpPut(body, url, options = {}) {
const headers = {
"Content-Type": "application/json; charset=utf-8",
...options.headers,
};
const response = await sendAsync({
...options,
headers,
method: HttpMethod.PUT,
url,
body,
});
const json = await response.json();
return [response.headers, json];
}
async function httpPatch(body, url, options = {}) {
const headers = {
"Content-Type": "application/json; charset=utf-8",
...options.headers,
};
const response = await sendAsync({
...options,
headers,
method: HttpMethod.PATCH,
url,
body,
});
const json = await response.json();
return [response.headers, json];
}
async function httpDelete(url, options) {
return sendAsync({
...options,
method: HttpMethod.DELETE,
url,
});
}
async function httpDeleteBody(body, url, options = {}) {
const headers = {
"Content-Type": "application/json; charset=utf-8",
...options.headers,
};
const response = await sendAsync({
...options,
headers,
method: HttpMethod.DELETE,
url,
body,
});
const json = await response.json();
return [response.headers, json];
}
function buildUrl(url, query) {
const queryArray = Object.entries(query || {})
.map(([key, value]) => value ? [snakeCase(key), value] : undefined)
.filter(isDefined);
return url + Str.buildQuery(queryArray);
}
function conformBody(body) {
if (body instanceof File) {
return body;
}
if (body instanceof Blob) {
return body;
}
if (body) {
return JSON.stringify(body);
}
return null;
}
async function sendAsync(options) {
const { method, url, body } = options;
const fullUrl = buildUrl(url, options.query);
const initialHeaders = {
Accept: "application/json",
...options.headers,
[Constants.clientIdHeader]: getClientId(),
};
const headers = {
...initialHeaders,
Authorization: options.authorizationBuilder?.(method, fullUrl, initialHeaders),
};
try {
const response = await api.fetch(fullUrl, {
method,
headers: pickBy(headers, isDefined),
body: conformBody(body),
signal: options.signal,
});
if (!response.ok) {
throw new RequestError(options, response.status, response.headers);
}
return response;
}
catch (error) {
// If this request was aborted, throw a custom error.
if (options.signal?.aborted) {
throw new AbortedError();
}
if (error instanceof RequestError) {
throw error;
}
// Otherwise, bubble up.
throw new RequestFailedError(error);
}
}
function httpEventSource(onMessage, url, options = {}) {
const buildHeaders = lastEventId => {
const initialHeaders = pickBy({
...options.headers,
[Constants.clientIdHeader]: getClientId(),
[Constants.lastEventIdHeader]: lastEventId,
}, isDefined);
return pickBy({
...initialHeaders,
Authorization: options.authorizationBuilder?.(HttpMethod.GET, url, initialHeaders),
}, isDefined);
};
fetchEventSource(api.eventSourceFetch, buildUrl(url, options.query), {
buildHeaders,
onMessage: onMessage,
openWhenHidden: true,
signal: options.signal,
});
}
export const Http = {
isError,
head: httpHead,
get: httpGet,
post: httpPost,
put: httpPut,
patch: httpPatch,
delete: httpDelete,
deleteBody: httpDeleteBody,
download: httpDownload,
eventSource: httpEventSource,
};