diff --git a/CHANGELOG.md b/CHANGELOG.md index b89b2d0..e0dce3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org). -## [Unreleased] +## [0.2.0] - 2023-10-27 ### Added * [#2](https://github.com/shlinkio/shlink-js-sdk/issues/2) Implement node.js `HttpClient`. +* Add support for orphan and short URL visits deletion methods. ### Changed * *Nothing* diff --git a/dev/index.mjs b/dev/index.mjs index f730a0c..238232a 100644 --- a/dev/index.mjs +++ b/dev/index.mjs @@ -2,12 +2,18 @@ import { ShlinkApiClient } from '../dist/index.js'; import { NodeHttpClient } from '../dist/node.js'; (async function () { + const [,, baseUrl, apiKey] = process.argv; + if (!baseUrl || !apiKey) { + console.error('Base URL or API key not provided.'); + return; + } + try { const apiClient = new ShlinkApiClient(new NodeHttpClient(), { - baseUrl: process.argv[2], - apiKey: process.argv[3], + baseUrl, + apiKey, }); - console.log('Success:', await apiClient.listShortUrls()); + console.log('Success:', await apiClient.deleteShortUrlVisits('nieRB')); } catch (e) { console.error('Error:', e); } diff --git a/src/api-contract/ShlinkApiClient.ts b/src/api-contract/ShlinkApiClient.ts index 7f1d52d..fecd6e5 100644 --- a/src/api-contract/ShlinkApiClient.ts +++ b/src/api-contract/ShlinkApiClient.ts @@ -1,5 +1,6 @@ import type { ShlinkCreateShortUrlData, + ShlinkDeleteVisitsResponse, ShlinkDomainRedirects, ShlinkDomainsResponse, ShlinkEditDomainRedirects, @@ -22,12 +23,16 @@ export type ShlinkApiClient = { getShortUrlVisits(shortCode: string, query?: ShlinkVisitsParams): Promise; + deleteShortUrlVisits(shortCode: string, domain?: string | null): Promise; + getTagVisits(tag: string, query?: Omit): Promise; getDomainVisits(domain: string, query?: Omit): Promise; getOrphanVisits(query?: Omit): Promise; + deleteOrphanVisits(): Promise; + getNonOrphanVisits(query?: Omit): Promise; getVisitsOverview(): Promise; diff --git a/src/api-contract/types.ts b/src/api-contract/types.ts index c62b5d2..5bc56ec 100644 --- a/src/api-contract/types.ts +++ b/src/api-contract/types.ts @@ -148,6 +148,10 @@ export interface ShlinkVisits { pagination: ShlinkPaginator; } +export type ShlinkDeleteVisitsResponse = { + deletedVisits: number; +}; + export interface ShlinkVisitsOverview { /** Optional only before Shlink 3.5.0 */ nonOrphanVisits?: ShlinkVisitsSummary; diff --git a/src/api/ShlinkApiClient.ts b/src/api/ShlinkApiClient.ts index ed80de1..90030ff 100644 --- a/src/api/ShlinkApiClient.ts +++ b/src/api/ShlinkApiClient.ts @@ -1,6 +1,7 @@ import type { ShlinkApiClient as BaseShlinkApiClient, ShlinkCreateShortUrlData, + ShlinkDeleteVisitsResponse, ShlinkDomainRedirects, ShlinkDomainsResponse, ShlinkEditDomainRedirects, @@ -73,6 +74,13 @@ export class ShlinkApiClient implements BaseShlinkApiClient { .then(({ visits }) => visits); } + public async deleteShortUrlVisits(shortCode: string, domain?: string | null): Promise { + const query = domain ? { domain } : undefined; + return this.performRequest( + { method: 'DELETE', url: `/short-urls/${shortCode}/visits`, query }, + ); + } + public async getTagVisits(tag: string, query?: Omit): Promise { return this.performRequest<{ visits: ShlinkVisits }>({ url: `/tags/${tag}/visits`, query }) .then(({ visits }) => visits); @@ -87,6 +95,10 @@ export class ShlinkApiClient implements BaseShlinkApiClient { return this.performRequest<{ visits: ShlinkVisits }>({ url: '/visits/orphan', query }).then(({ visits }) => visits); } + public async deleteOrphanVisits(): Promise { + return this.performRequest({ method: 'DELETE', url: '/visits/orphan' }); + } + public async getNonOrphanVisits(query?: Omit): Promise { return this.performRequest<{ visits: ShlinkVisits }>({ url: '/visits/non-orphan', query }) .then(({ visits }) => visits); diff --git a/test/api/ShlinkApiClient.test.ts b/test/api/ShlinkApiClient.test.ts index 0766f99..0b67f1e 100644 --- a/test/api/ShlinkApiClient.test.ts +++ b/test/api/ShlinkApiClient.test.ts @@ -115,6 +115,25 @@ describe('ShlinkApiClient', () => { }); }); + describe('deleteShortUrlVisits', () => { + it.each([ + ['the_domain', '?domain=the_domain'], + [null, ''], + [undefined, ''], + ])('deletes visits with params', async (domain, expectedQuery) => { + const response = { deletedVisits: 10 }; + jsonRequest.mockResolvedValue(response); + + const actualVisits = await apiClient.deleteShortUrlVisits('abc123', domain); + + expect(actualVisits).toEqual(response); + expect(jsonRequest).toHaveBeenCalledWith( + expect.stringContaining(`/short-urls/abc123/visits${expectedQuery}`), + expect.objectContaining({ method: 'DELETE' }), + ); + }); + }); + describe('getTagVisits', () => { it('properly returns tag visits', async () => { const expectedVisits = ['foo', 'bar']; @@ -344,6 +363,21 @@ describe('ShlinkApiClient', () => { }); }); + describe('deleteOrphanVisits', () => { + it('deletes visits with params', async () => { + const response = { deletedVisits: 10 }; + jsonRequest.mockResolvedValue(response); + + const actualVisits = await apiClient.deleteOrphanVisits(); + + expect(actualVisits).toEqual(response); + expect(jsonRequest).toHaveBeenCalledWith( + expect.stringContaining('/visits/orphan'), + expect.objectContaining({ method: 'DELETE' }), + ); + }); + }); + describe('getNonOrphanVisits', () => { it('returns non-orphan visits', async () => { jsonRequest.mockResolvedValue({ visits: fromPartial({ data: [] }) });