From 3fe48779bee397840c96a74b15ae1c9b84cfd16a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 24 Jul 2023 10:47:40 +0200 Subject: [PATCH] Allow heallth request to be performed for a different domain --- src/api/services/ShlinkApiClient.ts | 93 ++++++++++++------- .../domains/reducers/domainsList.ts | 18 +--- .../short-urls/reducers/shortUrlEdition.ts | 2 +- .../visits/reducers/nonOrphanVisits.ts | 1 - 4 files changed, 62 insertions(+), 52 deletions(-) diff --git a/src/api/services/ShlinkApiClient.ts b/src/api/services/ShlinkApiClient.ts index fcdc583e..b290f6be 100644 --- a/src/api/services/ShlinkApiClient.ts +++ b/src/api/services/ShlinkApiClient.ts @@ -3,6 +3,7 @@ import type { HttpClient } from '../../common/services/HttpClient'; import type { ShortUrl, ShortUrlData } from '../../shlink-web-component/short-urls/data'; import { orderToString } from '../../utils/helpers/ordering'; import { stringifyQuery } from '../../utils/helpers/query'; +import { replaceAuthorityFromUri } from '../../utils/helpers/uri'; import type { OptionalString } from '../../utils/utils'; import type { ShlinkDomainRedirects, @@ -23,7 +24,17 @@ import type { } from '../types'; import { isRegularNotFound, parseApiError } from '../utils'; -const buildShlinkBaseUrl = (url: string, version: 2 | 3) => `${url}/rest/v${version}`; +type ApiVersion = 2 | 3; + +type RequestOptions = { + url: string; + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + query?: object; + body?: object; + domain?: string; +}; + +const buildShlinkBaseUrl = (url: string, version: ApiVersion) => `${url}/rest/v${version}`; const rejectNilProps = reject(isNil); const normalizeListParams = ( { orderBy = {}, excludeMaxVisitsReached, excludePastValidUntil, ...rest }: ShlinkShortUrlsListParams, @@ -35,101 +46,115 @@ const normalizeListParams = ( }); export class ShlinkApiClient { - private apiVersion: 2 | 3; + private apiVersion: ApiVersion; public constructor( private readonly httpClient: HttpClient, - private readonly baseUrl: string, - private readonly apiKey: string, + public readonly baseUrl: string, + public readonly apiKey: string, ) { this.apiVersion = 3; } public readonly listShortUrls = async (params: ShlinkShortUrlsListParams = {}): Promise => - this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>('/short-urls', 'GET', normalizeListParams(params)) - .then(({ shortUrls }) => shortUrls); + this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>( + { url: '/short-urls', query: normalizeListParams(params) }, + ).then(({ shortUrls }) => shortUrls); public readonly createShortUrl = async (options: ShortUrlData): Promise => { - const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options as any); - return this.performRequest('/short-urls', 'POST', {}, filteredOptions); + const body = reject((value) => isEmpty(value) || isNil(value), options as any); + return this.performRequest({ url: '/short-urls', method: 'POST', body }); }; public readonly getShortUrlVisits = async (shortCode: string, query?: ShlinkVisitsParams): Promise => - this.performRequest<{ visits: ShlinkVisits }>(`/short-urls/${shortCode}/visits`, 'GET', query) + this.performRequest<{ visits: ShlinkVisits }>({ url: `/short-urls/${shortCode}/visits`, query }) .then(({ visits }) => visits); public readonly getTagVisits = async (tag: string, query?: Omit): Promise => - this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query).then(({ visits }) => visits); + this.performRequest<{ visits: ShlinkVisits }>({ url: `/tags/${tag}/visits`, query }).then(({ visits }) => visits); public readonly getDomainVisits = async (domain: string, query?: Omit): Promise => - this.performRequest<{ visits: ShlinkVisits }>(`/domains/${domain}/visits`, 'GET', query).then(({ visits }) => visits); + this.performRequest<{ visits: ShlinkVisits }>({ url: `/domains/${domain}/visits`, query }).then(({ visits }) => visits); public readonly getOrphanVisits = async (query?: Omit): Promise => - this.performRequest<{ visits: ShlinkVisits }>('/visits/orphan', 'GET', query).then(({ visits }) => visits); + this.performRequest<{ visits: ShlinkVisits }>({ url: '/visits/orphan', query }).then(({ visits }) => visits); public readonly getNonOrphanVisits = async (query?: Omit): Promise => - this.performRequest<{ visits: ShlinkVisits }>('/visits/non-orphan', 'GET', query).then(({ visits }) => visits); + this.performRequest<{ visits: ShlinkVisits }>({ url: '/visits/non-orphan', query }).then(({ visits }) => visits); public readonly getVisitsOverview = async (): Promise => - this.performRequest<{ visits: ShlinkVisitsOverview }>('/visits').then(({ visits }) => visits); + this.performRequest<{ visits: ShlinkVisitsOverview }>({ url: '/visits' }).then(({ visits }) => visits); public readonly getShortUrl = async (shortCode: string, domain?: OptionalString): Promise => - this.performRequest(`/short-urls/${shortCode}`, 'GET', { domain }); + this.performRequest({ url: `/short-urls/${shortCode}`, query: { domain } }); public readonly deleteShortUrl = async (shortCode: string, domain?: OptionalString): Promise => - this.performEmptyRequest(`/short-urls/${shortCode}`, 'DELETE', { domain }); + this.performEmptyRequest({ url: `/short-urls/${shortCode}`, method: 'DELETE', query: { domain } }); public readonly updateShortUrl = async ( shortCode: string, domain: OptionalString, - edit: ShlinkShortUrlData, + body: ShlinkShortUrlData, ): Promise => - this.performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, edit); + this.performRequest({ url: `/short-urls/${shortCode}`, method: 'PATCH', query: { domain }, body }); public readonly listTags = async (): Promise => - this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' }) + this.performRequest<{ tags: ShlinkTagsResponse }>({ url: '/tags', query: { withStats: 'true' } }) .then(({ tags }) => tags) .then(({ data, stats }) => ({ tags: data, stats })); public readonly tagsStats = async (): Promise => - this.performRequest<{ tags: ShlinkTagsStatsResponse }>('/tags/stats', 'GET') + this.performRequest<{ tags: ShlinkTagsStatsResponse }>({ url: '/tags/stats' }) .then(({ tags }) => tags) .then(({ data }) => ({ tags: data.map(({ tag }) => tag), stats: data })); public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> => - this.performEmptyRequest('/tags', 'DELETE', { tags }).then(() => ({ tags })); + this.performEmptyRequest({ url: '/tags', method: 'DELETE', body: { tags } }).then(() => ({ tags })); public readonly editTag = async (oldName: string, newName: string): Promise<{ oldName: string; newName: string }> => - this.performEmptyRequest('/tags', 'PUT', {}, { oldName, newName }).then(() => ({ oldName, newName })); + this.performEmptyRequest({ + url: '/tags', + method: 'PUT', + body: { oldName, newName }, + }).then(() => ({ oldName, newName })); - public readonly health = async (): Promise => this.performRequest('/health', 'GET'); + public readonly health = async (domain?: string): Promise => this.performRequest( + { url: '/health', domain }, + ); public readonly mercureInfo = async (): Promise => - this.performRequest('/mercure-info', 'GET'); + this.performRequest({ url: '/mercure-info' }); public readonly listDomains = async (): Promise => - this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains').then(({ domains }) => domains); + this.performRequest<{ domains: ShlinkDomainsResponse }>({ url: '/domains' }).then(({ domains }) => domains); public readonly editDomainRedirects = async ( domainRedirects: ShlinkEditDomainRedirects, ): Promise => - this.performRequest('/domains/redirects', 'PATCH', {}, domainRedirects); + this.performRequest({ url: '/domains/redirects', method: 'PATCH', body: domainRedirects }); - private readonly performRequest = async (url: string, method = 'GET', query = {}, body?: object): Promise => - this.httpClient.fetchJson(...this.toFetchParams(url, method, query, body)).catch( - this.handleFetchError(() => this.httpClient.fetchJson(...this.toFetchParams(url, method, query, body))), + private readonly performRequest = async (requestOptions: RequestOptions): Promise => + this.httpClient.fetchJson(...this.toFetchParams(requestOptions)).catch( + this.handleFetchError(() => this.httpClient.fetchJson(...this.toFetchParams(requestOptions))), ); - private readonly performEmptyRequest = async (url: string, method = 'GET', query = {}, body?: object): Promise => - this.httpClient.fetchEmpty(...this.toFetchParams(url, method, query, body)).catch( - this.handleFetchError(() => this.httpClient.fetchEmpty(...this.toFetchParams(url, method, query, body))), + private readonly performEmptyRequest = async (requestOptions: RequestOptions): Promise => + this.httpClient.fetchEmpty(...this.toFetchParams(requestOptions)).catch( + this.handleFetchError(() => this.httpClient.fetchEmpty(...this.toFetchParams(requestOptions))), ); - private readonly toFetchParams = (url: string, method: string, query = {}, body?: object): [string, RequestInit] => { + private readonly toFetchParams = ({ + url, + method = 'GET', + query = {}, + body, + domain, + }: RequestOptions): [string, RequestInit] => { const normalizedQuery = stringifyQuery(rejectNilProps(query)); const stringifiedQuery = isEmpty(normalizedQuery) ? '' : `?${normalizedQuery}`; + const baseUrl = domain ? replaceAuthorityFromUri(this.baseUrl, domain) : this.baseUrl; - return [`${buildShlinkBaseUrl(this.baseUrl, this.apiVersion)}${url}${stringifiedQuery}`, { + return [`${buildShlinkBaseUrl(baseUrl, this.apiVersion)}${url}${stringifiedQuery}`, { method, body: body && JSON.stringify(body), headers: { 'X-Api-Key': this.apiKey }, diff --git a/src/shlink-web-component/domains/reducers/domainsList.ts b/src/shlink-web-component/domains/reducers/domainsList.ts index 6f38d09b..af04da84 100644 --- a/src/shlink-web-component/domains/reducers/domainsList.ts +++ b/src/shlink-web-component/domains/reducers/domainsList.ts @@ -4,7 +4,6 @@ import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient'; import type { ShlinkDomainRedirects } from '../../../api/types'; import type { ProblemDetailsError } from '../../../api/types/errors'; import { parseApiError } from '../../../api/utils'; -import { hasServerData } from '../../../servers/data'; import { createAsyncThunk } from '../../../utils/helpers/redux'; import type { Domain, DomainStatus } from '../data'; import type { EditDomainRedirects } from './domainRedirects'; @@ -58,22 +57,9 @@ export const domainsListReducerCreator = ( const checkDomainHealth = createAsyncThunk( `${REDUCER_PREFIX}/checkDomainHealth`, - async (domain: string, { getState }): Promise => { - const { selectedServer } = getState(); - - if (!hasServerData(selectedServer)) { - return { domain, status: 'invalid' }; - } - + async (domain: string): Promise => { try { - // FIXME This should call different domains - // const { url, ...rest } = selectedServer; - // const { health } = buildShlinkApiClient({ - // ...rest, - // url: replaceAuthorityFromUri(url, domain), - // }); - const { status } = await apiClient.health(); - + const { status } = await apiClient.health(domain); return { domain, status: status === 'pass' ? 'valid' : 'invalid' }; } catch (e) { return { domain, status: 'invalid' }; diff --git a/src/shlink-web-component/short-urls/reducers/shortUrlEdition.ts b/src/shlink-web-component/short-urls/reducers/shortUrlEdition.ts index 69128060..674e9d91 100644 --- a/src/shlink-web-component/short-urls/reducers/shortUrlEdition.ts +++ b/src/shlink-web-component/short-urls/reducers/shortUrlEdition.ts @@ -31,7 +31,7 @@ const initialState: ShortUrlEdition = { export const editShortUrl = (apiClient: ShlinkApiClient) => createAsyncThunk( `${REDUCER_PREFIX}/editShortUrl`, ({ shortCode, domain, data }: EditShortUrl): Promise => - apiClient.updateShortUrl(shortCode, domain, data as any) // FIXME parse dates + apiClient.updateShortUrl(shortCode, domain, data as any) // TODO parse dates , ); diff --git a/src/shlink-web-component/visits/reducers/nonOrphanVisits.ts b/src/shlink-web-component/visits/reducers/nonOrphanVisits.ts index cc97c8fc..7555a5fc 100644 --- a/src/shlink-web-component/visits/reducers/nonOrphanVisits.ts +++ b/src/shlink-web-component/visits/reducers/nonOrphanVisits.ts @@ -1,5 +1,4 @@ import type { ShlinkApiClient } from '../../../api/services/ShlinkApiClient'; -import type { ShlinkApiClientBuilder } from '../../../api/services/ShlinkApiClientBuilder'; import { isBetween } from '../../../utils/helpers/date'; import { createVisitsAsyncThunk, createVisitsReducer, lastVisitLoaderForLoader } from './common'; import type { VisitsInfo } from './types';