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:
parent
ff75b3cd1f
commit
1b51a1aedd
7 changed files with 73 additions and 27 deletions
module
Core
src/Tag
Model
Paginator/Adapter
test-db/Tag/Paginator/Adapter
test/Tag
Rest/src/Action/Tag
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
];
|
];
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)),
|
||||||
]);
|
]);
|
||||||
|
|
Loading…
Add table
Reference in a new issue