mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-25 18:45:27 -06:00
Created TagsStats endpoint
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
"Tags"
|
"Tags"
|
||||||
],
|
],
|
||||||
"summary": "List existing tags",
|
"summary": "List existing tags",
|
||||||
"description": "Returns the list of all tags used in any short URL, ordered by name",
|
"description": "Returns the list of all tags used in any short URL",
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"ApiKey": []
|
"ApiKey": []
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "withStats",
|
"name": "withStats",
|
||||||
"description": "Whether you want to include also a list with general stats by tag or not. Defaults to false.",
|
"deprecated": true,
|
||||||
|
"description": "**[Deprecated]** Use [GET /tags/stats](#/Tags/tagsWithStats) endpoint to get tags with their stats.",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"required": false,
|
"required": false,
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -101,53 +102,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"examples": {
|
"example": {
|
||||||
"Without stats": {
|
"tags": {
|
||||||
"value": {
|
"data": [
|
||||||
"tags": {
|
"games",
|
||||||
"data": [
|
"php",
|
||||||
"games",
|
"shlink",
|
||||||
"php",
|
"tech"
|
||||||
"shlink",
|
],
|
||||||
"tech"
|
"pagination": {
|
||||||
],
|
"currentPage": 5,
|
||||||
"pagination": {
|
"pagesCount": 10,
|
||||||
"currentPage": 5,
|
"itemsPerPage": 4,
|
||||||
"pagesCount": 10,
|
"itemsInCurrentPage": 4,
|
||||||
"itemsPerPage": 4,
|
"totalItems": 38
|
||||||
"itemsInCurrentPage": 4,
|
|
||||||
"totalItems": 38
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"With stats": {
|
|
||||||
"value": {
|
|
||||||
"tags": {
|
|
||||||
"data": [
|
|
||||||
"games",
|
|
||||||
"shlink"
|
|
||||||
],
|
|
||||||
"stats": [
|
|
||||||
{
|
|
||||||
"tag": "games",
|
|
||||||
"shortUrlsCount": 10,
|
|
||||||
"visitsCount": 521
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tag": "shlink",
|
|
||||||
"shortUrlsCount": 7,
|
|
||||||
"visitsCount": 1087
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pagination": {
|
|
||||||
"currentPage": 5,
|
|
||||||
"pagesCount": 5,
|
|
||||||
"itemsPerPage": 10,
|
|
||||||
"itemsInCurrentPage": 2,
|
|
||||||
"totalItems": 42
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
123
docs/swagger/paths/v2_tags_stats.json
Normal file
123
docs/swagger/paths/v2_tags_stats.json
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"get": {
|
||||||
|
"operationId": "tagsWithStats",
|
||||||
|
"tags": [
|
||||||
|
"Tags"
|
||||||
|
],
|
||||||
|
"summary": "Get tags with stats",
|
||||||
|
"description": "Returns the list of all tags used in any short URL, together with the amount of short URLs and visits for it",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "../parameters/version.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "page",
|
||||||
|
"in": "query",
|
||||||
|
"description": "The page to display. Defaults to 1",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "itemsPerPage",
|
||||||
|
"in": "query",
|
||||||
|
"description": "The amount of items to return on every page. Defaults to all the items",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "searchTerm",
|
||||||
|
"in": "query",
|
||||||
|
"description": "A query used to filter results by searching for it on the tag name.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "orderBy",
|
||||||
|
"in": "query",
|
||||||
|
"description": "To determine how to order the results.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"tag-ASC",
|
||||||
|
"tag-DESC"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The list of tags",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tags": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["data"],
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"description": "The tag stats will be returned only if the withStats param was provided with value 'true'",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "../definitions/TagInfo.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"$ref": "../definitions/Pagination.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"tags": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"tag": "games",
|
||||||
|
"shortUrlsCount": 10,
|
||||||
|
"visitsCount": 521
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "shlink",
|
||||||
|
"shortUrlsCount": 7,
|
||||||
|
"visitsCount": 1087
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pagination": {
|
||||||
|
"currentPage": 5,
|
||||||
|
"pagesCount": 5,
|
||||||
|
"itemsPerPage": 10,
|
||||||
|
"itemsInCurrentPage": 2,
|
||||||
|
"totalItems": 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "Unexpected error.",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "../definitions/Error.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,6 +82,9 @@
|
|||||||
"/rest/v{version}/tags": {
|
"/rest/v{version}/tags": {
|
||||||
"$ref": "paths/v1_tags.json"
|
"$ref": "paths/v1_tags.json"
|
||||||
},
|
},
|
||||||
|
"/rest/v{version}/tags/stats": {
|
||||||
|
"$ref": "paths/v2_tags_stats.json"
|
||||||
|
},
|
||||||
|
|
||||||
"/rest/v{version}/visits": {
|
"/rest/v{version}/visits": {
|
||||||
"$ref": "paths/v2_visits.json"
|
"$ref": "paths/v2_visits.json"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ return [
|
|||||||
Action\Visit\GlobalVisitsAction::class => ConfigAbstractFactory::class,
|
Action\Visit\GlobalVisitsAction::class => ConfigAbstractFactory::class,
|
||||||
Action\Visit\OrphanVisitsAction::class => ConfigAbstractFactory::class,
|
Action\Visit\OrphanVisitsAction::class => ConfigAbstractFactory::class,
|
||||||
Action\Tag\ListTagsAction::class => ConfigAbstractFactory::class,
|
Action\Tag\ListTagsAction::class => ConfigAbstractFactory::class,
|
||||||
|
Action\Tag\TagsStatsAction::class => ConfigAbstractFactory::class,
|
||||||
Action\Tag\DeleteTagsAction::class => ConfigAbstractFactory::class,
|
Action\Tag\DeleteTagsAction::class => ConfigAbstractFactory::class,
|
||||||
Action\Tag\UpdateTagAction::class => ConfigAbstractFactory::class,
|
Action\Tag\UpdateTagAction::class => ConfigAbstractFactory::class,
|
||||||
Action\Domain\ListDomainsAction::class => ConfigAbstractFactory::class,
|
Action\Domain\ListDomainsAction::class => ConfigAbstractFactory::class,
|
||||||
@@ -75,6 +76,7 @@ return [
|
|||||||
],
|
],
|
||||||
Action\ShortUrl\ListShortUrlsAction::class => [Service\ShortUrlService::class, ShortUrlDataTransformer::class],
|
Action\ShortUrl\ListShortUrlsAction::class => [Service\ShortUrlService::class, ShortUrlDataTransformer::class],
|
||||||
Action\Tag\ListTagsAction::class => [TagService::class],
|
Action\Tag\ListTagsAction::class => [TagService::class],
|
||||||
|
Action\Tag\TagsStatsAction::class => [TagService::class],
|
||||||
Action\Tag\DeleteTagsAction::class => [TagService::class],
|
Action\Tag\DeleteTagsAction::class => [TagService::class],
|
||||||
Action\Tag\UpdateTagAction::class => [TagService::class],
|
Action\Tag\UpdateTagAction::class => [TagService::class],
|
||||||
Action\Domain\ListDomainsAction::class => [DomainService::class, Options\NotFoundRedirectOptions::class],
|
Action\Domain\ListDomainsAction::class => [DomainService::class, Options\NotFoundRedirectOptions::class],
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ return [
|
|||||||
|
|
||||||
// Tags
|
// Tags
|
||||||
Action\Tag\ListTagsAction::getRouteDef(),
|
Action\Tag\ListTagsAction::getRouteDef(),
|
||||||
|
Action\Tag\TagsStatsAction::getRouteDef(),
|
||||||
Action\Tag\DeleteTagsAction::getRouteDef(),
|
Action\Tag\DeleteTagsAction::getRouteDef(),
|
||||||
Action\Tag\UpdateTagAction::getRouteDef(),
|
Action\Tag\UpdateTagAction::getRouteDef(),
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class ListTagsAction extends AbstractRestAction
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This part is deprecated. To get tags with stats, the /tags/stats endpoint should be used instead
|
||||||
$tagsInfo = $this->tagService->tagsInfo($params, $apiKey);
|
$tagsInfo = $this->tagService->tagsInfo($params, $apiKey);
|
||||||
$rawTags = $this->serializePaginator($tagsInfo, null, 'stats');
|
$rawTags = $this->serializePaginator($tagsInfo, null, 'stats');
|
||||||
$rawTags['data'] = map($tagsInfo, static fn (TagInfo $info) => $info->tag()->__toString());
|
$rawTags['data'] = map($tagsInfo, static fn (TagInfo $info) => $info->tag()->__toString());
|
||||||
|
|||||||
36
module/Rest/src/Action/Tag/TagsStatsAction.php
Normal file
36
module/Rest/src/Action/Tag/TagsStatsAction.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Rest\Action\Tag;
|
||||||
|
|
||||||
|
use Laminas\Diactoros\Response\JsonResponse;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
|
||||||
|
use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
|
||||||
|
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||||
|
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||||
|
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
|
||||||
|
|
||||||
|
class TagsStatsAction extends AbstractRestAction
|
||||||
|
{
|
||||||
|
use PagerfantaUtilsTrait;
|
||||||
|
|
||||||
|
protected const ROUTE_PATH = '/tags/stats';
|
||||||
|
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
|
||||||
|
|
||||||
|
public function __construct(private TagServiceInterface $tagService)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||||
|
{
|
||||||
|
$query = $request->getQueryParams();
|
||||||
|
$params = TagsParams::fromRawData($query);
|
||||||
|
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
|
||||||
|
$tagsInfo = $this->tagService->tagsInfo($params, $apiKey);
|
||||||
|
|
||||||
|
return new JsonResponse(['tags' => $this->serializePaginator($tagsInfo)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
module/Rest/test-api/Action/TagsStatsTest.php
Normal file
127
module/Rest/test-api/Action/TagsStatsTest.php
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioApiTest\Shlink\Rest\Action;
|
||||||
|
|
||||||
|
use GuzzleHttp\RequestOptions;
|
||||||
|
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
||||||
|
|
||||||
|
class TagsStatsTest extends ApiTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @dataProvider provideQueries
|
||||||
|
*/
|
||||||
|
public function expectedListOfTagsIsReturned(string $apiKey, array $query, array $expectedTags): void
|
||||||
|
{
|
||||||
|
$resp = $this->callApiWithKey(self::METHOD_GET, '/tags/stats', [RequestOptions::QUERY => $query], $apiKey);
|
||||||
|
$payload = $this->getJsonResponsePayload($resp);
|
||||||
|
|
||||||
|
self::assertEquals(['tags' => $expectedTags], $payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideQueries(): iterable
|
||||||
|
{
|
||||||
|
yield 'admin API key' => ['valid_api_key', [], [
|
||||||
|
'data' => [
|
||||||
|
[
|
||||||
|
'tag' => 'bar',
|
||||||
|
'shortUrlsCount' => 1,
|
||||||
|
'visitsCount' => 2,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'tag' => 'baz',
|
||||||
|
'shortUrlsCount' => 0,
|
||||||
|
'visitsCount' => 0,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'tag' => 'foo',
|
||||||
|
'shortUrlsCount' => 3,
|
||||||
|
'visitsCount' => 5,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'pagination' => [
|
||||||
|
'currentPage' => 1,
|
||||||
|
'pagesCount' => 1,
|
||||||
|
'itemsPerPage' => 3,
|
||||||
|
'itemsInCurrentPage' => 3,
|
||||||
|
'totalItems' => 3,
|
||||||
|
],
|
||||||
|
]];
|
||||||
|
yield 'admin API key with pagination' => ['valid_api_key', ['page' => 1, 'itemsPerPage' => 2], [
|
||||||
|
'data' => [
|
||||||
|
[
|
||||||
|
'tag' => 'bar',
|
||||||
|
'shortUrlsCount' => 1,
|
||||||
|
'visitsCount' => 2,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'tag' => 'baz',
|
||||||
|
'shortUrlsCount' => 0,
|
||||||
|
'visitsCount' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'pagination' => [
|
||||||
|
'currentPage' => 1,
|
||||||
|
'pagesCount' => 2,
|
||||||
|
'itemsPerPage' => 2,
|
||||||
|
'itemsInCurrentPage' => 2,
|
||||||
|
'totalItems' => 3,
|
||||||
|
],
|
||||||
|
]];
|
||||||
|
yield 'author API key' => ['author_api_key', [], [
|
||||||
|
'data' => [
|
||||||
|
[
|
||||||
|
'tag' => 'bar',
|
||||||
|
'shortUrlsCount' => 1,
|
||||||
|
'visitsCount' => 2,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'tag' => 'foo',
|
||||||
|
'shortUrlsCount' => 2,
|
||||||
|
'visitsCount' => 5,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'pagination' => [
|
||||||
|
'currentPage' => 1,
|
||||||
|
'pagesCount' => 1,
|
||||||
|
'itemsPerPage' => 2,
|
||||||
|
'itemsInCurrentPage' => 2,
|
||||||
|
'totalItems' => 2,
|
||||||
|
],
|
||||||
|
]];
|
||||||
|
yield 'author API key with pagination' => ['author_api_key', ['page' => 2, 'itemsPerPage' => 1], [
|
||||||
|
'data' => [
|
||||||
|
[
|
||||||
|
'tag' => 'foo',
|
||||||
|
'shortUrlsCount' => 2,
|
||||||
|
'visitsCount' => 5,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'pagination' => [
|
||||||
|
'currentPage' => 2,
|
||||||
|
'pagesCount' => 2,
|
||||||
|
'itemsPerPage' => 1,
|
||||||
|
'itemsInCurrentPage' => 1,
|
||||||
|
'totalItems' => 2,
|
||||||
|
],
|
||||||
|
]];
|
||||||
|
yield 'domain API key' => ['domain_api_key', [], [
|
||||||
|
'data' => [
|
||||||
|
[
|
||||||
|
'tag' => 'foo',
|
||||||
|
'shortUrlsCount' => 1,
|
||||||
|
'visitsCount' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'pagination' => [
|
||||||
|
'currentPage' => 1,
|
||||||
|
'pagesCount' => 1,
|
||||||
|
'itemsPerPage' => 1,
|
||||||
|
'itemsInCurrentPage' => 1,
|
||||||
|
'totalItems' => 1,
|
||||||
|
],
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user