1
|
|
|
import qs from 'qs'; |
2
|
|
|
import { isEmpty, isNil, reject } from 'ramda'; |
3
|
|
|
import { AxiosInstance, AxiosResponse, Method } from 'axios'; |
4
|
|
|
import { ShortUrlsListParams } from '../../short-urls/reducers/shortUrlsListParams'; |
5
|
|
|
import { ShortUrl, ShortUrlData } from '../../short-urls/data'; |
6
|
|
|
import { OptionalString } from '../utils'; |
7
|
|
|
import { |
8
|
|
|
ShlinkHealth, |
9
|
|
|
ShlinkMercureInfo, |
10
|
|
|
ShlinkShortUrlsResponse, |
11
|
|
|
ShlinkTags, |
12
|
|
|
ShlinkTagsResponse, |
13
|
|
|
ShlinkVisits, |
14
|
|
|
ShlinkVisitsParams, |
15
|
|
|
ShlinkShortUrlMeta, |
16
|
|
|
ShlinkDomain, |
17
|
|
|
ShlinkDomainsResponse, |
18
|
|
|
} from './types'; |
19
|
|
|
|
20
|
23 |
|
const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/rest/v${apiVersion}` : ''; |
21
|
2 |
|
const rejectNilProps = reject(isNil); |
22
|
|
|
|
23
|
|
|
export default class ShlinkApiClient { |
24
|
|
|
private apiVersion: number; |
25
|
|
|
|
26
|
|
|
public constructor( |
27
|
|
|
private readonly axios: AxiosInstance, |
28
|
|
|
private readonly baseUrl: string, |
29
|
|
|
private readonly apiKey: string, |
30
|
|
|
) { |
31
|
27 |
|
this.apiVersion = 2; |
32
|
|
|
} |
33
|
|
|
|
34
|
27 |
|
public readonly listShortUrls = async (params: ShortUrlsListParams = {}): Promise<ShlinkShortUrlsResponse> => |
35
|
1 |
|
this.performRequest<{ shortUrls: ShlinkShortUrlsResponse }>('/short-urls', 'GET', params) |
36
|
1 |
|
.then(({ data }) => data.shortUrls); |
37
|
|
|
|
38
|
27 |
|
public readonly createShortUrl = async (options: ShortUrlData): Promise<ShortUrl> => { |
39
|
4 |
|
const filteredOptions = reject((value) => isEmpty(value) || isNil(value), options as any); |
40
|
|
|
|
41
|
2 |
|
return this.performRequest<ShortUrl>('/short-urls', 'POST', {}, filteredOptions) |
42
|
2 |
|
.then((resp) => resp.data); |
43
|
|
|
}; |
44
|
|
|
|
45
|
27 |
|
public readonly getShortUrlVisits = async (shortCode: string, query?: ShlinkVisitsParams): Promise<ShlinkVisits> => |
46
|
1 |
|
this.performRequest<{ visits: ShlinkVisits }>(`/short-urls/${shortCode}/visits`, 'GET', query) |
47
|
1 |
|
.then(({ data }) => data.visits); |
48
|
|
|
|
49
|
27 |
|
public readonly getTagVisits = async (tag: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> => |
50
|
1 |
|
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query) |
51
|
1 |
|
.then(({ data }) => data.visits); |
52
|
|
|
|
53
|
27 |
|
public readonly getShortUrl = async (shortCode: string, domain?: OptionalString): Promise<ShortUrl> => |
54
|
3 |
|
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain }) |
55
|
3 |
|
.then(({ data }) => data); |
56
|
|
|
|
57
|
27 |
|
public readonly deleteShortUrl = async (shortCode: string, domain?: OptionalString): Promise<void> => |
58
|
3 |
|
this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain }) |
59
|
|
|
.then(() => {}); |
60
|
|
|
|
61
|
27 |
|
public readonly updateShortUrlTags = async ( |
62
|
|
|
shortCode: string, |
63
|
|
|
domain: OptionalString, |
64
|
|
|
tags: string[], |
65
|
|
|
): Promise<string[]> => |
66
|
3 |
|
this.performRequest<{ tags: string[] }>(`/short-urls/${shortCode}/tags`, 'PUT', { domain }, { tags }) |
67
|
3 |
|
.then(({ data }) => data.tags); |
68
|
|
|
|
69
|
27 |
|
public readonly updateShortUrlMeta = async ( |
70
|
|
|
shortCode: string, |
71
|
|
|
domain: OptionalString, |
72
|
|
|
meta: ShlinkShortUrlMeta, |
73
|
|
|
): Promise<ShlinkShortUrlMeta> => |
74
|
3 |
|
this.performRequest(`/short-urls/${shortCode}`, 'PATCH', { domain }, meta) |
75
|
3 |
|
.then(() => meta); |
76
|
|
|
|
77
|
27 |
|
public readonly listTags = async (): Promise<ShlinkTags> => |
78
|
1 |
|
this.performRequest<{ tags: ShlinkTagsResponse }>('/tags', 'GET', { withStats: 'true' }) |
79
|
1 |
|
.then((resp) => resp.data.tags) |
80
|
1 |
|
.then(({ data, stats }) => ({ tags: data, stats })); |
81
|
|
|
|
82
|
27 |
|
public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> => |
83
|
1 |
|
this.performRequest('/tags', 'DELETE', { tags }) |
84
|
1 |
|
.then(() => ({ tags })); |
85
|
|
|
|
86
|
27 |
|
public readonly editTag = async (oldName: string, newName: string): Promise<{ oldName: string; newName: string }> => |
87
|
1 |
|
this.performRequest('/tags', 'PUT', {}, { oldName, newName }) |
88
|
1 |
|
.then(() => ({ oldName, newName })); |
89
|
|
|
|
90
|
27 |
|
public readonly health = async (): Promise<ShlinkHealth> => |
91
|
1 |
|
this.performRequest<ShlinkHealth>('/health', 'GET') |
92
|
1 |
|
.then((resp) => resp.data); |
93
|
|
|
|
94
|
27 |
|
public readonly mercureInfo = async (): Promise<ShlinkMercureInfo> => |
95
|
1 |
|
this.performRequest<ShlinkMercureInfo>('/mercure-info', 'GET') |
96
|
1 |
|
.then((resp) => resp.data); |
97
|
|
|
|
98
|
27 |
|
public readonly listDomains = async (): Promise<ShlinkDomain[]> => |
99
|
1 |
|
this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains', 'GET').then(({ data }) => data.domains.data); |
100
|
|
|
|
101
|
27 |
|
private readonly performRequest = async <T>(url: string, method: Method = 'GET', query = {}, body = {}): Promise<AxiosResponse<T>> => { |
102
|
23 |
|
try { |
103
|
23 |
|
return await this.axios({ |
104
|
|
|
method, |
105
|
|
|
url: `${buildShlinkBaseUrl(this.baseUrl, this.apiVersion)}${url}`, |
106
|
|
|
headers: { 'X-Api-Key': this.apiKey }, |
107
|
|
|
params: rejectNilProps(query), |
108
|
|
|
data: body, |
109
|
|
|
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'brackets' }), |
110
|
|
|
}); |
111
|
|
|
} catch (e) { |
112
|
|
|
const { response } = e; |
113
|
|
|
|
114
|
|
|
// Due to a bug on all previous Shlink versions, requests to non-matching URLs will always result on a CORS error |
115
|
|
|
// when performed from the browser (due to the preflight request not returning a 2xx status. |
116
|
|
|
// See https://github.com/shlinkio/shlink/issues/614), which will make the "response" prop not to be set here. |
117
|
|
|
// The bug will be fixed on upcoming Shlink patches, but for other versions, we can consider this situation as |
118
|
|
|
// if a request has been performed to a not supported API version. |
119
|
|
|
const apiVersionIsNotSupported = !response; |
120
|
|
|
|
121
|
|
|
// When the request is not invalid or we have already tried both API versions, throw the error and let the |
122
|
|
|
// caller handle it |
123
|
4 |
|
if (!apiVersionIsNotSupported || this.apiVersion === 1) { |
124
|
|
|
throw e; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
this.apiVersion = this.apiVersion - 1; |
128
|
|
|
|
129
|
|
|
return await this.performRequest(url, method, query, body); |
130
|
|
|
} |
131
|
|
|
}; |
132
|
|
|
} |
133
|
|
|
|