/*
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
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { useSWRConfig } from "swr";
import axios, { AxiosError, AxiosResponse } from "axios";
import { MerchantBackend } from "../declaration.js";
import { useBackendContext } from "../context/backend.js";
import { useEffect, useState } from "preact/hooks";
import { DEFAULT_REQUEST_TIMEOUT } from "../utils/constants.js";
import {
axiosHandler,
removeAxiosCancelToken,
} from "../utils/switchableAxios.js";
export function useMatchMutate(): (
re: RegExp,
value?: unknown,
) => Promise {
const { cache, mutate } = useSWRConfig();
if (!(cache instanceof Map)) {
throw new Error(
"matchMutate requires the cache provider to be a Map instance",
);
}
return function matchRegexMutate(re: RegExp, value?: unknown) {
const allKeys = Array.from(cache.keys());
// console.log(allKeys)
const keys = allKeys.filter((key) => re.test(key));
// console.log(allKeys.length, keys.length)
const mutations = keys.map((key) => {
// console.log(key)
mutate(key, value, true);
});
return Promise.all(mutations);
};
}
export type HttpResponse =
| HttpResponseOk
| HttpResponseLoading
| HttpError;
export type HttpResponsePaginated =
| HttpResponseOkPaginated
| HttpResponseLoading
| HttpError;
export interface RequestInfo {
url: string;
hasToken: boolean;
params: unknown;
data: unknown;
status: number;
}
interface HttpResponseLoading {
ok?: false;
loading: true;
clientError?: false;
serverError?: false;
data?: T;
}
export interface HttpResponseOk {
ok: true;
loading?: false;
clientError?: false;
serverError?: false;
data: T;
info?: RequestInfo;
}
export type HttpResponseOkPaginated = HttpResponseOk & WithPagination;
export interface WithPagination {
loadMore: () => void;
loadMorePrev: () => void;
isReachingEnd?: boolean;
isReachingStart?: boolean;
}
export type HttpError =
| HttpResponseClientError
| HttpResponseServerError
| HttpResponseUnexpectedError;
export interface SwrError {
info: unknown;
status: number;
message: string;
}
export interface HttpResponseServerError {
ok?: false;
loading?: false;
clientError?: false;
serverError: true;
error?: MerchantBackend.ErrorDetail;
status: number;
message: string;
info?: RequestInfo;
}
interface HttpResponseClientError {
ok?: false;
loading?: false;
clientError: true;
serverError?: false;
info?: RequestInfo;
isUnauthorized: boolean;
isNotfound: boolean;
status: number;
error?: MerchantBackend.ErrorDetail;
message: string;
}
interface HttpResponseUnexpectedError {
ok?: false;
loading?: false;
clientError?: false;
serverError?: false;
info?: RequestInfo;
status?: number;
error: unknown;
message: string;
}
type Methods = "get" | "post" | "patch" | "delete" | "put";
interface RequestOptions {
method?: Methods;
token?: string;
data?: unknown;
params?: unknown;
}
function buildRequestOk(
res: AxiosResponse,
url: string,
hasToken: boolean,
): HttpResponseOk {
return {
ok: true,
data: res.data,
info: {
params: res.config.params,
data: res.config.data,
url,
hasToken,
status: res.status,
},
};
}
// function buildResponse(data?: T, error?: MerchantBackend.ErrorDetail, isValidating?: boolean): HttpResponse {
// if (isValidating) return {loading: true}
// if (error) return buildRequestFailed()
// }
function buildRequestFailed(
ex: AxiosError,
url: string,
hasToken: boolean,
):
| HttpResponseClientError
| HttpResponseServerError
| HttpResponseUnexpectedError {
const status = ex.response?.status;
const info: RequestInfo = {
data: ex.request?.data,
params: ex.request?.params,
url,
hasToken,
status: status || 0,
};
if (status && status >= 400 && status < 500) {
const error: HttpResponseClientError = {
clientError: true,
isNotfound: status === 404,
isUnauthorized: status === 401,
status,
info,
message: ex.response?.data?.hint || ex.message,
error: ex.response?.data,
};
return error;
}
if (status && status >= 500 && status < 600) {
const error: HttpResponseServerError = {
serverError: true,
status,
info,
message:
`${ex.response?.data?.hint} (code ${ex.response?.data?.code})` ||
ex.message,
error: ex.response?.data,
};
return error;
}
const error: HttpResponseUnexpectedError = {
info,
status,
error: ex,
message: ex.message,
};
return error;
}
const CancelToken = axios.CancelToken;
let source = CancelToken.source();
export function cancelPendingRequest(): void {
source.cancel("canceled by the user");
source = CancelToken.source();
}
export function isAxiosError(
error: AxiosError | any,
): error is AxiosError {
return error && error.isAxiosError;
}
export async function request(
url: string,
options: RequestOptions = {},
): Promise> {
const headers = options.token
? { Authorization: `Bearer ${options.token}` }
: undefined;
try {
const res = await axiosHandler({
url,
responseType: "json",
headers,
cancelToken: !removeAxiosCancelToken ? source.token : undefined,
method: options.method || "get",
data: options.data,
params: options.params,
timeout: DEFAULT_REQUEST_TIMEOUT * 1000,
});
return buildRequestOk(res, url, !!options.token);
} catch (e) {
if (isAxiosError(e)) {
const error = buildRequestFailed(e, url, !!options.token);
throw error;
}
throw e;
}
}
export function multiFetcher(
urls: string[],
token: string,
backend: string,
): Promise[]> {
return Promise.all(urls.map((url) => fetcher(url, token, backend)));
}
export function fetcher(
url: string,
token: string,
backend: string,
): Promise> {
return request(`${backend}${url}`, { token });
}
export function useBackendInstancesTestForAdmin(): HttpResponse {
const { url, token } = useBackendContext();
type Type = MerchantBackend.Instances.InstancesResponse;
const [result, setResult] = useState>({ loading: true });
useEffect(() => {
request(`${url}/management/instances`, { token })
.then((data) => setResult(data))
.catch((error) => setResult(error));
}, [url, token]);
return result;
}
export function useBackendConfig(): HttpResponse {
const { url, token } = useBackendContext();
type Type = MerchantBackend.VersionResponse;
const [result, setResult] = useState>({ loading: true });
useEffect(() => {
request(`${url}/config`, { token })
.then((data) => setResult(data))
.catch((error) => setResult(error));
}, [url, token]);
return result;
}