/* This file is part of GNU Taler (C) 2021 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; }