aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-02-08 17:36:26 -0300
committerSebastian <sebasjm@gmail.com>2023-02-08 17:36:26 -0300
commitbe01d1479cf650fe8eb0c8e567620abfa4544e1e (patch)
treea1a0e6f45e6e2fdacca9df77aeb7cf1a065b312d /packages
parentf7982ed99672709908d378c7abc02300440a4ac2 (diff)
move request api to web-util
Diffstat (limited to 'packages')
-rw-r--r--packages/web-util/package.json2
-rw-r--r--packages/web-util/src/context/api.ts43
-rw-r--r--packages/web-util/src/context/index.ts4
-rw-r--r--packages/web-util/src/index.browser.ts1
-rw-r--r--packages/web-util/src/utils/base64.ts243
-rw-r--r--packages/web-util/src/utils/request.ts319
6 files changed, 610 insertions, 2 deletions
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index ad44ed67f..1d3dcfca6 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -37,7 +37,7 @@
"preact-render-to-string": "^5.2.6",
"prettier": "^2.5.1",
"rimraf": "^3.0.2",
- "swr": "1.3.0",
+ "swr": "2.0.3",
"tslib": "^2.4.0",
"typescript": "^4.9.4",
"ws": "7.4.5"
diff --git a/packages/web-util/src/context/api.ts b/packages/web-util/src/context/api.ts
new file mode 100644
index 000000000..81586bd35
--- /dev/null
+++ b/packages/web-util/src/context/api.ts
@@ -0,0 +1,43 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { defaultRequestHandler } from "../utils/request.js";
+
+interface Type {
+ request: typeof defaultRequestHandler;
+}
+
+const Context = createContext<Type>({
+ request: defaultRequestHandler,
+});
+
+export const useApiContext = (): Type => useContext(Context);
+export const ApiContextProvider = ({
+ children,
+ value,
+}: {
+ value: Type;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, { value, children });
+};
diff --git a/packages/web-util/src/context/index.ts b/packages/web-util/src/context/index.ts
index 4bc1b22f2..9ed3ef645 100644
--- a/packages/web-util/src/context/index.ts
+++ b/packages/web-util/src/context/index.ts
@@ -1,5 +1,7 @@
+export { ApiContextProvider, useApiContext } from "./api.js";
export {
InternationalizationAPI,
TranslationProvider,
- useTranslationContext,
+ useTranslationContext
} from "./translation.js";
+
diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts
index d3aeae168..2ae3f2a0b 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -1,4 +1,5 @@
export * from "./hooks/index.js";
+export * from "./utils/request.js";
export * from "./context/index.js";
export * from "./components/index.js";
export * as tests from "./tests/index.js";
diff --git a/packages/web-util/src/utils/base64.ts b/packages/web-util/src/utils/base64.ts
new file mode 100644
index 000000000..0e075880f
--- /dev/null
+++ b/packages/web-util/src/utils/base64.ts
@@ -0,0 +1,243 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+
+export function base64encode(str: string): string {
+ return base64EncArr(strToUTF8Arr(str))
+}
+
+export function base64decode(str: string): string {
+ return UTF8ArrToStr(base64DecToArr(str))
+}
+
+// from https://developer.mozilla.org/en-US/docs/Glossary/Base64
+
+// Array of bytes to Base64 string decoding
+function b64ToUint6(nChr: number): number {
+ return nChr > 64 && nChr < 91
+ ? nChr - 65
+ : nChr > 96 && nChr < 123
+ ? nChr - 71
+ : nChr > 47 && nChr < 58
+ ? nChr + 4
+ : nChr === 43
+ ? 62
+ : nChr === 47
+ ? 63
+ : 0;
+}
+
+function base64DecToArr(sBase64: string, nBlocksSize?: number): Uint8Array {
+ const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ""); // Only necessary if the base64 includes whitespace such as line breaks.
+ const nInLen = sB64Enc.length;
+ const nOutLen = nBlocksSize
+ ? Math.ceil(((nInLen * 3 + 1) >> 2) / nBlocksSize) * nBlocksSize
+ : (nInLen * 3 + 1) >> 2;
+ const taBytes = new Uint8Array(nOutLen);
+
+ let nMod3;
+ let nMod4;
+ let nUint24 = 0;
+ let nOutIdx = 0;
+ for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) {
+ nMod4 = nInIdx & 3;
+ nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << (6 * (3 - nMod4));
+ if (nMod4 === 3 || nInLen - nInIdx === 1) {
+ nMod3 = 0;
+ while (nMod3 < 3 && nOutIdx < nOutLen) {
+ taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255;
+ nMod3++;
+ nOutIdx++;
+ }
+ nUint24 = 0;
+ }
+ }
+
+ return taBytes;
+}
+
+/* Base64 string to array encoding */
+function uint6ToB64(nUint6: number): number {
+ return nUint6 < 26
+ ? nUint6 + 65
+ : nUint6 < 52
+ ? nUint6 + 71
+ : nUint6 < 62
+ ? nUint6 - 4
+ : nUint6 === 62
+ ? 43
+ : nUint6 === 63
+ ? 47
+ : 65;
+}
+
+function base64EncArr(aBytes: Uint8Array): string {
+ let nMod3 = 2;
+ let sB64Enc = "";
+
+ const nLen = aBytes.length;
+ let nUint24 = 0;
+ for (let nIdx = 0; nIdx < nLen; nIdx++) {
+ nMod3 = nIdx % 3;
+ // To break your base64 into several 80-character lines, add:
+ // if (nIdx > 0 && ((nIdx * 4) / 3) % 76 === 0) {
+ // sB64Enc += "\r\n";
+ // }
+
+ nUint24 |= aBytes[nIdx] << ((16 >>> nMod3) & 24);
+ if (nMod3 === 2 || aBytes.length - nIdx === 1) {
+ sB64Enc += String.fromCodePoint(
+ uint6ToB64((nUint24 >>> 18) & 63),
+ uint6ToB64((nUint24 >>> 12) & 63),
+ uint6ToB64((nUint24 >>> 6) & 63),
+ uint6ToB64(nUint24 & 63)
+ );
+ nUint24 = 0;
+ }
+ }
+ return (
+ sB64Enc.substring(0, sB64Enc.length - 2 + nMod3) +
+ (nMod3 === 2 ? "" : nMod3 === 1 ? "=" : "==")
+ );
+}
+
+/* UTF-8 array to JS string and vice versa */
+
+function UTF8ArrToStr(aBytes: Uint8Array): string {
+ let sView = "";
+ let nPart;
+ const nLen = aBytes.length;
+ for (let nIdx = 0; nIdx < nLen; nIdx++) {
+ nPart = aBytes[nIdx];
+ sView += String.fromCodePoint(
+ nPart > 251 && nPart < 254 && nIdx + 5 < nLen /* six bytes */
+ ? /* (nPart - 252 << 30) may be not so safe in ECMAScript! So…: */
+ (nPart - 252) * 1073741824 +
+ ((aBytes[++nIdx] - 128) << 24) +
+ ((aBytes[++nIdx] - 128) << 18) +
+ ((aBytes[++nIdx] - 128) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
+ : nPart > 247 && nPart < 252 && nIdx + 4 < nLen /* five bytes */
+ ? ((nPart - 248) << 24) +
+ ((aBytes[++nIdx] - 128) << 18) +
+ ((aBytes[++nIdx] - 128) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
+ : nPart > 239 && nPart < 248 && nIdx + 3 < nLen /* four bytes */
+ ? ((nPart - 240) << 18) +
+ ((aBytes[++nIdx] - 128) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
+ : nPart > 223 && nPart < 240 && nIdx + 2 < nLen /* three bytes */
+ ? ((nPart - 224) << 12) +
+ ((aBytes[++nIdx] - 128) << 6) +
+ aBytes[++nIdx] -
+ 128
+ : nPart > 191 && nPart < 224 && nIdx + 1 < nLen /* two bytes */
+ ? ((nPart - 192) << 6) + aBytes[++nIdx] - 128
+ : /* nPart < 127 ? */ /* one byte */
+ nPart
+ );
+ }
+ return sView;
+}
+
+function strToUTF8Arr(sDOMStr: string): Uint8Array {
+ let nChr;
+ const nStrLen = sDOMStr.length;
+ let nArrLen = 0;
+
+ /* mapping… */
+ for (let nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) {
+ nChr = sDOMStr.codePointAt(nMapIdx);
+ if (nChr === undefined) {
+ throw Error(`No char at ${nMapIdx} on string with length: ${sDOMStr.length}`)
+ }
+
+ if (nChr >= 0x10000) {
+ nMapIdx++;
+ }
+
+ nArrLen +=
+ nChr < 0x80
+ ? 1
+ : nChr < 0x800
+ ? 2
+ : nChr < 0x10000
+ ? 3
+ : nChr < 0x200000
+ ? 4
+ : nChr < 0x4000000
+ ? 5
+ : 6;
+ }
+
+ const aBytes = new Uint8Array(nArrLen);
+
+ /* transcription… */
+ let nIdx = 0;
+ let nChrIdx = 0;
+ while (nIdx < nArrLen) {
+ nChr = sDOMStr.codePointAt(nChrIdx);
+ if (nChr === undefined) {
+ throw Error(`No char at ${nChrIdx} on string with length: ${sDOMStr.length}`)
+ }
+ if (nChr < 128) {
+ /* one byte */
+ aBytes[nIdx++] = nChr;
+ } else if (nChr < 0x800) {
+ /* two bytes */
+ aBytes[nIdx++] = 192 + (nChr >>> 6);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ } else if (nChr < 0x10000) {
+ /* three bytes */
+ aBytes[nIdx++] = 224 + (nChr >>> 12);
+ aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ } else if (nChr < 0x200000) {
+ /* four bytes */
+ aBytes[nIdx++] = 240 + (nChr >>> 18);
+ aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ nChrIdx++;
+ } else if (nChr < 0x4000000) {
+ /* five bytes */
+ aBytes[nIdx++] = 248 + (nChr >>> 24);
+ aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ nChrIdx++;
+ } /* if (nChr <= 0x7fffffff) */ else {
+ /* six bytes */
+ aBytes[nIdx++] = 252 + (nChr >>> 30);
+ aBytes[nIdx++] = 128 + ((nChr >>> 24) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 18) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 12) & 63);
+ aBytes[nIdx++] = 128 + ((nChr >>> 6) & 63);
+ aBytes[nIdx++] = 128 + (nChr & 63);
+ nChrIdx++;
+ }
+ nChrIdx++;
+ }
+
+ return aBytes;
+}
diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts
new file mode 100644
index 000000000..24342bb80
--- /dev/null
+++ b/packages/web-util/src/utils/request.ts
@@ -0,0 +1,319 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2023 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { base64encode } from "./base64.js";
+
+/**
+ *
+ * @param baseUrl URL where the service is located
+ * @param endpoint endpoint of the service to be called
+ * @param options auth, method and params
+ * @returns
+ */
+export async function defaultRequestHandler<T>(
+ baseUrl: string,
+ endpoint: string,
+ options: RequestOptions = {},
+): Promise<HttpResponseOk<T>> {
+ const requestHeaders: Record<string, string> = {};
+ if (options.token) {
+ requestHeaders.Authorization = `Bearer ${options.token}`
+ } else if (options.basicAuth) {
+ requestHeaders.Authorization = `Basic ${base64encode(`${options.basicAuth.username}:${options.basicAuth.password}`)}`
+ }
+ requestHeaders["Content-Type"] = options.contentType === "json" ? "application/json" : "text/plain"
+
+ const requestMethod = options?.method ?? "GET";
+ const requestBody = options?.data;
+ const requestTimeout = options?.timeout ?? 2 * 1000;
+ const requestParams = options.params ?? {};
+
+ const _url = new URL(`${baseUrl}${endpoint}`);
+
+ Object.entries(requestParams).forEach(([key, value]) => {
+ _url.searchParams.set(key, String(value));
+ });
+
+ let payload: BodyInit | undefined = undefined;
+ if (requestBody != null) {
+ if (typeof requestBody === "string") {
+ payload = requestBody;
+ } else if (requestBody instanceof ArrayBuffer) {
+ payload = requestBody;
+ } else if (ArrayBuffer.isView(requestBody)) {
+ payload = requestBody;
+ } else if (typeof requestBody === "object") {
+ payload = JSON.stringify(requestBody);
+ } else {
+ throw Error("unsupported request body type");
+ }
+ }
+
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => {
+ controller.abort("HTTP_REQUEST_TIMEOUT");
+ }, requestTimeout);
+
+ let response;
+ try {
+ response = await fetch(_url.href, {
+ headers: requestHeaders,
+ method: requestMethod,
+ credentials: "omit",
+ mode: "cors",
+ body: payload,
+ signal: controller.signal,
+ });
+ } catch (ex) {
+ const info: RequestInfo = {
+ payload,
+ url: _url.href,
+ hasToken: !!options.token,
+ status: 0,
+ };
+ const error: HttpResponseUnexpectedError = {
+ info,
+ status: 0,
+ error: ex,
+ message: "Request timeout",
+ };
+ throw new RequestError(error);
+ }
+
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+ const headerMap = new Headers();
+ response.headers.forEach((value, key) => {
+ headerMap.set(key, value);
+ });
+
+ if (response.ok) {
+ const result = await buildRequestOk<T>(
+ response,
+ _url.href,
+ payload,
+ !!options.token,
+ );
+ return result;
+ } else {
+ const error = await buildRequestFailed(
+ response,
+ _url.href,
+ payload,
+ !!options.token,
+ );
+ throw new RequestError(error);
+ }
+}
+
+export type HttpResponse<T, ErrorDetail> =
+ | HttpResponseOk<T>
+ | HttpResponseLoading<T>
+ | HttpError<ErrorDetail>;
+
+export type HttpResponsePaginated<T, ErrorDetail> =
+ | HttpResponseOkPaginated<T>
+ | HttpResponseLoading<T>
+ | HttpError<ErrorDetail>;
+
+export interface RequestInfo {
+ url: string;
+ hasToken: boolean;
+ payload: any;
+ status: number;
+}
+
+interface HttpResponseLoading<T> {
+ ok?: false;
+ loading: true;
+ clientError?: false;
+ serverError?: false;
+
+ data?: T;
+}
+export interface HttpResponseOk<T> {
+ ok: true;
+ loading?: false;
+ clientError?: false;
+ serverError?: false;
+
+ data: T;
+ info?: RequestInfo;
+}
+
+export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination;
+
+export interface WithPagination {
+ loadMore: () => void;
+ loadMorePrev: () => void;
+ isReachingEnd?: boolean;
+ isReachingStart?: boolean;
+}
+
+export type HttpError<ErrorDetail> =
+ | HttpResponseClientError<ErrorDetail>
+ | HttpResponseServerError<ErrorDetail>
+ | HttpResponseUnexpectedError;
+
+export interface HttpResponseServerError<ErrorDetail> {
+ ok?: false;
+ loading?: false;
+ clientError?: false;
+ serverError: true;
+
+ error?: ErrorDetail;
+ status: HttpStatusCode;
+ message: string;
+ info?: RequestInfo;
+}
+interface HttpResponseClientError<ErrorDetail> {
+ ok?: false;
+ loading?: false;
+ clientError: true;
+ serverError?: false;
+
+ info?: RequestInfo;
+ isUnauthorized: boolean;
+ isNotfound: boolean;
+ status: HttpStatusCode;
+ error?: ErrorDetail;
+ message: string;
+}
+
+interface HttpResponseUnexpectedError {
+ ok?: false;
+ loading?: false;
+ clientError?: false;
+ serverError?: false;
+
+ info?: RequestInfo;
+ status?: HttpStatusCode;
+ error: unknown;
+ message: string;
+}
+
+export class RequestError<ErrorDetail> extends Error {
+ info: HttpError<ErrorDetail>;
+ constructor(d: HttpError<ErrorDetail>) {
+ super(d.message)
+ this.info = d
+ }
+}
+
+type Methods = "GET" | "POST" | "PATCH" | "DELETE" | "PUT";
+
+export interface RequestOptions {
+ method?: Methods;
+ token?: string;
+ basicAuth?: {
+ username: string,
+ password: string,
+ }
+ data?: any;
+ params?: unknown;
+ timeout?: number,
+ contentType?: "text" | "json"
+}
+
+async function buildRequestOk<T>(
+ response: Response,
+ url: string,
+ payload: any,
+ hasToken: boolean,
+): Promise<HttpResponseOk<T>> {
+ const dataTxt = await response.text();
+ const data = dataTxt ? JSON.parse(dataTxt) : undefined;
+ return {
+ ok: true,
+ data,
+ info: {
+ payload,
+ url,
+ hasToken,
+ status: response.status,
+ },
+ };
+}
+
+async function buildRequestFailed<ErrorDetail>(
+ response: Response,
+ url: string,
+ payload: any,
+ hasToken: boolean,
+): Promise<
+ | HttpResponseClientError<ErrorDetail>
+ | HttpResponseServerError<ErrorDetail>
+ | HttpResponseUnexpectedError
+> {
+ const status = response?.status;
+
+ const info: RequestInfo = {
+ payload,
+ url,
+ hasToken,
+ status: status || 0,
+ };
+
+ try {
+ const dataTxt = await response.text();
+ const data = dataTxt ? JSON.parse(dataTxt) : undefined;
+ if (status && status >= 400 && status < 500) {
+ const error: HttpResponseClientError<ErrorDetail> = {
+ clientError: true,
+ isNotfound: status === 404,
+ isUnauthorized: status === 401,
+ status,
+ info,
+ message: data?.hint,
+ error: data,
+ };
+ return error;
+ }
+ if (status && status >= 500 && status < 600) {
+ const error: HttpResponseServerError<ErrorDetail> = {
+ serverError: true,
+ status,
+ info,
+ message: `${data?.hint} (code ${data?.code})`,
+ error: data,
+ };
+ return error;
+ }
+ return {
+ info,
+ status,
+ error: {},
+ message: "NOT DEFINED",
+ };
+ } catch (ex) {
+ const error: HttpResponseUnexpectedError = {
+ info,
+ status,
+ error: ex,
+ message: "NOT DEFINED",
+ };
+
+ return error;
+ }
+}
+
+// export function isAxiosError<T>(
+// error: AxiosError | any,
+// ): error is AxiosError<T> {
+// return error && error.isAxiosError;
+// }