Define Shlink API contract

This commit is contained in:
Alejandro Celaya
2023-07-24 17:30:58 +02:00
parent 3fe48779be
commit 5f6dc186e3
42 changed files with 161 additions and 126 deletions

View File

@@ -1,5 +1,5 @@
import type { ProblemDetailsError } from './types/errors';
import { isInvalidArgumentError } from './utils';
import type { ProblemDetailsError } from '../shlink-web-component/api-contract';
import { isInvalidArgumentError } from '../shlink-web-component/api-contract/utils';
export interface ShlinkApiErrorProps {
errorData?: ProblemDetailsError;

View File

@@ -1,11 +1,7 @@
import { isEmpty, isNil, reject } from 'ramda';
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 {
ShlinkApiClient as BaseShlinkApiClient,
ShlinkDomainRedirects,
ShlinkDomainsResponse,
ShlinkEditDomainRedirects,
@@ -20,9 +16,13 @@ import type {
ShlinkTagsStatsResponse,
ShlinkVisits,
ShlinkVisitsOverview,
ShlinkVisitsParams,
} from '../types';
import { isRegularNotFound, parseApiError } from '../utils';
ShlinkVisitsParams } from '../../shlink-web-component/api-contract';
import { isRegularNotFound, parseApiError } from '../../shlink-web-component/api-contract/utils';
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';
type ApiVersion = 2 | 3;
@@ -45,7 +45,7 @@ const normalizeListParams = (
orderBy: orderToString(orderBy),
});
export class ShlinkApiClient {
export class ShlinkApiClient implements BaseShlinkApiClient {
private apiVersion: ApiVersion;
public constructor(

View File

@@ -1,54 +0,0 @@
export enum ErrorTypeV2 {
INVALID_ARGUMENT = 'INVALID_ARGUMENT',
INVALID_SHORT_URL_DELETION = 'INVALID_SHORT_URL_DELETION',
DOMAIN_NOT_FOUND = 'DOMAIN_NOT_FOUND',
FORBIDDEN_OPERATION = 'FORBIDDEN_OPERATION',
INVALID_URL = 'INVALID_URL',
INVALID_SLUG = 'INVALID_SLUG',
INVALID_SHORTCODE = 'INVALID_SHORTCODE',
TAG_CONFLICT = 'TAG_CONFLICT',
TAG_NOT_FOUND = 'TAG_NOT_FOUND',
MERCURE_NOT_CONFIGURED = 'MERCURE_NOT_CONFIGURED',
INVALID_AUTHORIZATION = 'INVALID_AUTHORIZATION',
INVALID_API_KEY = 'INVALID_API_KEY',
NOT_FOUND = 'NOT_FOUND',
}
export enum ErrorTypeV3 {
INVALID_ARGUMENT = 'https://shlink.io/api/error/invalid-data',
INVALID_SHORT_URL_DELETION = 'https://shlink.io/api/error/invalid-short-url-deletion',
DOMAIN_NOT_FOUND = 'https://shlink.io/api/error/domain-not-found',
FORBIDDEN_OPERATION = 'https://shlink.io/api/error/forbidden-tag-operation',
INVALID_URL = 'https://shlink.io/api/error/invalid-url',
INVALID_SLUG = 'https://shlink.io/api/error/non-unique-slug',
INVALID_SHORTCODE = 'https://shlink.io/api/error/short-url-not-found',
TAG_CONFLICT = 'https://shlink.io/api/error/tag-conflict',
TAG_NOT_FOUND = 'https://shlink.io/api/error/tag-not-found',
MERCURE_NOT_CONFIGURED = 'https://shlink.io/api/error/mercure-not-configured',
INVALID_AUTHORIZATION = 'https://shlink.io/api/error/missing-authentication',
INVALID_API_KEY = 'https://shlink.io/api/error/invalid-api-key',
NOT_FOUND = 'https://shlink.io/api/error/not-found',
}
export interface ProblemDetailsError {
type: string;
detail: string;
title: string;
status: number;
[extraProps: string]: any;
}
export interface InvalidArgumentError extends ProblemDetailsError {
type: ErrorTypeV2.INVALID_ARGUMENT | ErrorTypeV3.INVALID_ARGUMENT;
invalidElements: string[];
}
export interface InvalidShortUrlDeletion extends ProblemDetailsError {
type: 'INVALID_SHORTCODE_DELETION' | ErrorTypeV2.INVALID_SHORT_URL_DELETION | ErrorTypeV3.INVALID_SHORT_URL_DELETION;
threshold: number;
}
export interface RegularNotFound extends ProblemDetailsError {
type: ErrorTypeV2.NOT_FOUND | ErrorTypeV3.NOT_FOUND;
status: 404;
}

View File

@@ -1,133 +0,0 @@
import type { ShortUrl, ShortUrlMeta } from '../../shlink-web-component/short-urls/data';
import type { Visit } from '../../shlink-web-component/visits/types';
import type { Order } from '../../utils/helpers/ordering';
import type { OptionalString } from '../../utils/utils';
export interface ShlinkShortUrlsResponse {
data: ShortUrl[];
pagination: ShlinkPaginator;
}
export interface ShlinkMercureInfo {
token: string;
mercureHubUrl: string;
}
export interface ShlinkHealth {
status: 'pass' | 'fail';
version: string;
}
export interface ShlinkTagsStats {
tag: string;
shortUrlsCount: number;
visitsSummary?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
/** @deprecated */
visitsCount: number;
}
export interface ShlinkTags {
tags: string[];
stats: ShlinkTagsStats[];
}
export interface ShlinkTagsResponse {
data: string[];
/** @deprecated Present only when withStats=true is provided, which is deprecated */
stats: ShlinkTagsStats[];
}
export interface ShlinkTagsStatsResponse {
data: ShlinkTagsStats[];
}
export interface ShlinkPaginator {
currentPage: number;
pagesCount: number;
totalItems: number;
}
export interface ShlinkVisitsSummary {
total: number;
nonBots: number;
bots: number;
}
export interface ShlinkVisits {
data: Visit[];
pagination: ShlinkPaginator;
}
export interface ShlinkVisitsOverview {
nonOrphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
orphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
/** @deprecated */
visitsCount: number;
/** @deprecated */
orphanVisitsCount: number;
}
export interface ShlinkVisitsParams {
domain?: OptionalString;
page?: number;
itemsPerPage?: number;
startDate?: string;
endDate?: string;
excludeBots?: boolean;
}
export interface ShlinkShortUrlData extends ShortUrlMeta {
longUrl?: string;
title?: string;
validateUrl?: boolean;
tags?: string[];
}
export interface ShlinkDomainRedirects {
baseUrlRedirect: string | null;
regular404Redirect: string | null;
invalidShortUrlRedirect: string | null;
}
export interface ShlinkEditDomainRedirects extends Partial<ShlinkDomainRedirects> {
domain: string;
}
export interface ShlinkDomain {
domain: string;
isDefault: boolean;
redirects?: ShlinkDomainRedirects; // Optional only for Shlink older than 2.8
}
export interface ShlinkDomainsResponse {
data: ShlinkDomain[];
defaultRedirects?: ShlinkDomainRedirects; // Optional only for Shlink older than 2.10
}
export type TagsFilteringMode = 'all' | 'any';
type ShlinkShortUrlsOrderableFields = 'dateCreated' | 'shortCode' | 'longUrl' | 'title' | 'visits' | 'nonBotVisits';
export type ShlinkShortUrlsOrder = Order<ShlinkShortUrlsOrderableFields>;
export interface ShlinkShortUrlsListParams {
page?: string;
itemsPerPage?: number;
searchTerm?: string;
tags?: string[];
tagsMode?: TagsFilteringMode;
orderBy?: ShlinkShortUrlsOrder;
startDate?: string;
endDate?: string;
excludeMaxVisitsReached?: boolean;
excludePastValidUntil?: boolean;
}
export interface ShlinkShortUrlsListNormalizedParams extends
Omit<ShlinkShortUrlsListParams, 'orderBy' | 'excludeMaxVisitsReached' | 'excludePastValidUntil'> {
orderBy?: string;
excludeMaxVisitsReached?: 'true';
excludePastValidUntil?: 'true';
}

View File

@@ -1,25 +0,0 @@
import type {
InvalidArgumentError,
InvalidShortUrlDeletion,
ProblemDetailsError,
RegularNotFound } from '../types/errors';
import {
ErrorTypeV2,
ErrorTypeV3,
} from '../types/errors';
const isProblemDetails = (e: unknown): e is ProblemDetailsError =>
!!e && typeof e === 'object' && ['type', 'detail', 'title', 'status'].every((prop) => prop in e);
export const parseApiError = (e: unknown): ProblemDetailsError | undefined => (isProblemDetails(e) ? e : undefined);
export const isInvalidArgumentError = (error?: ProblemDetailsError): error is InvalidArgumentError =>
error?.type === ErrorTypeV2.INVALID_ARGUMENT || error?.type === ErrorTypeV3.INVALID_ARGUMENT;
export const isInvalidDeletionError = (error?: ProblemDetailsError): error is InvalidShortUrlDeletion =>
error?.type === 'INVALID_SHORTCODE_DELETION'
|| error?.type === ErrorTypeV2.INVALID_SHORT_URL_DELETION
|| error?.type === ErrorTypeV3.INVALID_SHORT_URL_DELETION;
export const isRegularNotFound = (error?: ProblemDetailsError): error is RegularNotFound =>
(error?.type === ErrorTypeV2.NOT_FOUND || error?.type === ErrorTypeV3.NOT_FOUND) && error?.status === 404;