1
0
Fork 0
mirror of https://github.com/shlinkio/shlink.git synced 2025-03-28 12:32:01 +03:00

Added ordering support for tags list when not requesting stats

This commit is contained in:
Alejandro Celaya 2022-01-09 13:31:08 +01:00
parent ff75b3cd1f
commit 1b51a1aedd
7 changed files with 73 additions and 27 deletions

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Tag\Model; namespace Shlinkio\Shlink\Core\Tag\Model;
use Shlinkio\Shlink\Core\Model\Ordering;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
final class TagsListFiltering final class TagsListFiltering
@ -12,10 +13,16 @@ final class TagsListFiltering
private ?int $limit = null, private ?int $limit = null,
private ?int $offset = null, private ?int $offset = null,
private ?string $searchTerm = null, private ?string $searchTerm = null,
private ?Ordering $orderBy = null,
private ?ApiKey $apiKey = null, private ?ApiKey $apiKey = null,
) { ) {
} }
public static function fromRangeAndParams(int $limit, int $offset, TagsParams $params, ?ApiKey $apiKey): self
{
return new self($limit, $offset, $params->searchTerm(), $params->orderBy(), $apiKey);
}
public function limit(): ?int public function limit(): ?int
{ {
return $this->limit; return $this->limit;
@ -31,6 +38,11 @@ final class TagsListFiltering
return $this->searchTerm; return $this->searchTerm;
} }
public function orderBy(): ?Ordering
{
return $this->orderBy;
}
public function apiKey(): ?ApiKey public function apiKey(): ?ApiKey
{ {
return $this->apiKey; return $this->apiKey;

View file

@ -5,11 +5,19 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Tag\Model; namespace Shlinkio\Shlink\Core\Tag\Model;
use Shlinkio\Shlink\Core\Model\AbstractInfinitePaginableListParams; use Shlinkio\Shlink\Core\Model\AbstractInfinitePaginableListParams;
use Shlinkio\Shlink\Core\Model\Ordering;
use function Shlinkio\Shlink\Common\parseOrderBy;
final class TagsParams extends AbstractInfinitePaginableListParams final class TagsParams extends AbstractInfinitePaginableListParams
{ {
private function __construct(private ?string $searchTerm, ?int $page, ?int $itemsPerPage) private function __construct(
{ private ?string $searchTerm,
private Ordering $orderBy,
private bool $withStats,
?int $page,
?int $itemsPerPage,
) {
parent::__construct($page, $itemsPerPage); parent::__construct($page, $itemsPerPage);
} }
@ -17,6 +25,8 @@ final class TagsParams extends AbstractInfinitePaginableListParams
{ {
return new self( return new self(
$query['searchTerm'] ?? null, $query['searchTerm'] ?? null,
Ordering::fromTuple(isset($query['orderBy']) ? parseOrderBy($query['orderBy']) : [null, null]),
($query['withStats'] ?? null) === 'true',
isset($query['page']) ? (int) $query['page'] : null, isset($query['page']) ? (int) $query['page'] : null,
isset($query['itemsPerPage']) ? (int) $query['itemsPerPage'] : null, isset($query['itemsPerPage']) ? (int) $query['itemsPerPage'] : null,
); );
@ -26,4 +36,14 @@ final class TagsParams extends AbstractInfinitePaginableListParams
{ {
return $this->searchTerm; return $this->searchTerm;
} }
public function orderBy(): Ordering
{
return $this->orderBy;
}
public function withStats(): bool
{
return $this->withStats;
}
} }

View file

@ -11,7 +11,7 @@ class TagsInfoPaginatorAdapter extends AbstractTagsPaginatorAdapter
public function getSlice(int $offset, int $length): iterable public function getSlice(int $offset, int $length): iterable
{ {
return $this->repo->findTagsWithInfo( return $this->repo->findTagsWithInfo(
new TagsListFiltering($length, $offset, $this->params->searchTerm(), $this->apiKey), TagsListFiltering::fromRangeAndParams($length, $offset, $this->params, $this->apiKey),
); );
} }
} }

View file

@ -13,7 +13,10 @@ class TagsPaginatorAdapter extends AbstractTagsPaginatorAdapter
{ {
$conditions = [ $conditions = [
new WithApiKeySpecsEnsuringJoin($this->apiKey), new WithApiKeySpecsEnsuringJoin($this->apiKey),
Spec::orderBy('name'), Spec::orderBy(
'name', // Ordering by other fields makes no sense here
$this->params->orderBy()->orderDirection(),
),
Spec::limit($length), Spec::limit($length),
Spec::offset($offset), Spec::offset($offset),
]; ];

View file

@ -10,6 +10,8 @@ use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
use Shlinkio\Shlink\Core\Tag\Paginator\Adapter\TagsPaginatorAdapter; use Shlinkio\Shlink\Core\Tag\Paginator\Adapter\TagsPaginatorAdapter;
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase; use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
use function Functional\map;
class TagsPaginatorAdapterTest extends DatabaseTestCase class TagsPaginatorAdapterTest extends DatabaseTestCase
{ {
private TagRepository $repo; private TagRepository $repo;
@ -25,9 +27,10 @@ class TagsPaginatorAdapterTest extends DatabaseTestCase
*/ */
public function expectedListOfTagsIsReturned( public function expectedListOfTagsIsReturned(
?string $searchTerm, ?string $searchTerm,
?string $orderBy,
int $offset, int $offset,
int $length, int $length,
int $expectedSliceSize, array $expectedTags,
int $expectedTotalCount, int $expectedTotalCount,
): void { ): void {
$names = ['foo', 'bar', 'baz', 'another']; $names = ['foo', 'bar', 'baz', 'another'];
@ -36,22 +39,31 @@ class TagsPaginatorAdapterTest extends DatabaseTestCase
} }
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();
$adapter = new TagsPaginatorAdapter($this->repo, TagsParams::fromRawData(['searchTerm' => $searchTerm]), null); $adapter = new TagsPaginatorAdapter($this->repo, TagsParams::fromRawData([
'searchTerm' => $searchTerm,
'orderBy' => $orderBy,
]), null);
self::assertCount($expectedSliceSize, $adapter->getSlice($offset, $length)); $tagNames = map($adapter->getSlice($offset, $length), static fn (Tag $tag) => $tag->__toString());
self::assertEquals($expectedTags, $tagNames);
self::assertEquals($expectedTotalCount, $adapter->getNbResults()); self::assertEquals($expectedTotalCount, $adapter->getNbResults());
} }
public function provideFilters(): iterable public function provideFilters(): iterable
{ {
yield [null, 0, 10, 4, 4]; yield [null, null, 0, 10, ['another', 'bar', 'baz', 'foo'], 4];
yield [null, 2, 10, 2, 4]; yield [null, null, 2, 10, ['baz', 'foo'], 4];
yield [null, 1, 3, 3, 4]; yield [null, null, 1, 3, ['bar', 'baz', 'foo'], 4];
yield [null, 3, 3, 1, 4]; yield [null, null, 3, 3, ['foo'], 4];
yield [null, 0, 2, 2, 4]; yield [null, null, 0, 2, ['another', 'bar'], 4];
yield ['ba', 0, 10, 2, 2]; yield ['ba', null, 0, 10, ['bar', 'baz'], 2];
yield ['ba', 0, 1, 1, 2]; yield ['ba', null, 0, 1, ['bar'], 2];
yield ['foo', 0, 10, 1, 1]; yield ['foo', null, 0, 10, ['foo'], 1];
yield ['a', 0, 10, 3, 3]; yield ['a', null, 0, 10, ['another', 'bar', 'baz'], 3];
yield [null, 'name-DESC', 0, 10, ['foo', 'baz', 'bar', 'another'], 4];
yield [null, 'name-ASC', 0, 10, ['another', 'bar', 'baz', 'foo'], 4];
yield [null, 'name-DESC', 0, 2, ['foo', 'baz'], 4];
yield ['ba', 'name-DESC', 0, 1, ['baz'], 2];
} }
} }

View file

@ -83,26 +83,26 @@ class TagServiceTest extends TestCase
{ {
yield 'no API key, no filter' => [ yield 'no API key, no filter' => [
null, null,
TagsParams::fromRawData([]), $params = TagsParams::fromRawData([]),
new TagsListFiltering(2, 0, null, null), TagsListFiltering::fromRangeAndParams(2, 0, $params, null),
1, 1,
]; ];
yield 'admin API key, no filter' => [ yield 'admin API key, no filter' => [
$apiKey = ApiKey::create(), $apiKey = ApiKey::create(),
TagsParams::fromRawData([]), $params = TagsParams::fromRawData([]),
new TagsListFiltering(2, 0, null, $apiKey), TagsListFiltering::fromRangeAndParams(2, 0, $params, $apiKey),
1, 1,
]; ];
yield 'no API key, search term' => [ yield 'no API key, search term' => [
null, null,
TagsParams::fromRawData(['searchTerm' => $searchTerm = 'foobar']), $params = TagsParams::fromRawData(['searchTerm' => 'foobar']),
new TagsListFiltering(2, 0, $searchTerm, null), TagsListFiltering::fromRangeAndParams(2, 0, $params, null),
1, 1,
]; ];
yield 'admin API key, limits' => [ yield 'admin API key, limits' => [
$apiKey = ApiKey::create(), $apiKey = ApiKey::create(),
TagsParams::fromRawData(['page' => 1, 'itemsPerPage' => 1]), $params = TagsParams::fromRawData(['page' => 1, 'itemsPerPage' => 1]),
new TagsListFiltering(1, 0, null, $apiKey), TagsListFiltering::fromRangeAndParams(1, 0, $params, $apiKey),
0, 0,
]; ];
} }

View file

@ -30,11 +30,10 @@ class ListTagsAction extends AbstractRestAction
public function handle(ServerRequestInterface $request): ResponseInterface public function handle(ServerRequestInterface $request): ResponseInterface
{ {
$query = $request->getQueryParams(); $query = $request->getQueryParams();
$withStats = ($query['withStats'] ?? null) === 'true';
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
$params = TagsParams::fromRawData($query); $params = TagsParams::fromRawData($query);
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
if (! $withStats) { if (! $params->withStats()) {
return new JsonResponse([ return new JsonResponse([
'tags' => $this->serializePaginator($this->tagService->listTags($params, $apiKey)), 'tags' => $this->serializePaginator($this->tagService->listTags($params, $apiKey)),
]); ]);