Fixed ordering in tags supporting more fields

This commit is contained in:
Alejandro Celaya 2023-01-02 19:49:54 +01:00
parent 961178fd82
commit ce9ec0d738
4 changed files with 45 additions and 23 deletions

View File

@ -9,22 +9,26 @@ use function Shlinkio\Shlink\Core\camelCaseToSnakeCase;
enum OrderableField: string enum OrderableField: string
{ {
case TAG = 'tag'; case TAG = 'tag';
// case SHORT_URLS = 'shortUrls'; case SHORT_URLS_COUNT = 'shortUrlsCount';
// case VISITS = 'visits'; case VISITS = 'visits';
// case NON_BOT_VISITS = 'nonBotVisits'; case NON_BOT_VISITS = 'nonBotVisits';
/** @deprecated Use VISITS instead */ /** @deprecated Use VISITS instead */
case VISITS_COUNT = 'visitsCount'; case VISITS_COUNT = 'visitsCount';
/** @deprecated Use SHORT_URLS instead */
case SHORT_URLS_COUNT = 'shortUrlsCount';
public static function isAggregateField(string $field): bool public static function isAggregateField(string $field): bool
{ {
return $field === self::SHORT_URLS_COUNT->value || $field === self::VISITS_COUNT->value; $parsed = self::tryFrom($field);
return $parsed !== null && $parsed !== self::TAG;
} }
public static function toSnakeCaseValidField(?string $field): string public static function toSnakeCaseValidField(?string $field): string
{ {
return camelCaseToSnakeCase($field === self::SHORT_URLS_COUNT->value ? $field : self::VISITS_COUNT->value); $parsed = self::tryFrom($field);
$normalized = match ($parsed) {
self::VISITS_COUNT, null => self::VISITS,
default => $parsed,
};
return camelCaseToSnakeCase($normalized->value);
} }
} }

View File

@ -25,8 +25,8 @@ final class TagInfo implements JsonSerializable
return new self( return new self(
$data['tag'], $data['tag'],
(int) $data['shortUrlsCount'], (int) $data['shortUrlsCount'],
(int) $data['visitsCount'], (int) $data['visits'],
isset($data['nonBotVisitsCount']) ? (int) $data['nonBotVisitsCount'] : null, isset($data['nonBotVisits']) ? (int) $data['nonBotVisits'] : null,
); );
} }

View File

@ -72,8 +72,8 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
't.id_0 AS id', 't.id_0 AS id',
't.name_1 AS name', 't.name_1 AS name',
'COUNT(DISTINCT s.id) AS short_urls_count', 'COUNT(DISTINCT s.id) AS short_urls_count',
'COUNT(DISTINCT v.id) AS visits_count', // Native queries require snake_case for cross-db compatibility 'COUNT(DISTINCT v.id) AS visits', // Native queries require snake_case for cross-db compatibility
'COUNT(DISTINCT v2.id) AS non_bot_visits_count', 'COUNT(DISTINCT v2.id) AS non_bot_visits',
) )
->from('(' . $subQb->getQuery()->getSQL() . ')', 't') // @phpstan-ignore-line ->from('(' . $subQb->getQuery()->getSQL() . ')', 't') // @phpstan-ignore-line
->leftJoin('t', 'short_urls_in_tags', 'st', $nativeQb->expr()->eq('t.id_0', 'st.tag_id')) ->leftJoin('t', 'short_urls_in_tags', 'st', $nativeQb->expr()->eq('t.id_0', 'st.tag_id'))
@ -108,8 +108,8 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
$rsm = new ResultSetMappingBuilder($this->getEntityManager()); $rsm = new ResultSetMappingBuilder($this->getEntityManager());
$rsm->addScalarResult('name', 'tag'); $rsm->addScalarResult('name', 'tag');
$rsm->addScalarResult('short_urls_count', 'shortUrlsCount'); $rsm->addScalarResult('short_urls_count', 'shortUrlsCount');
$rsm->addScalarResult('visits_count', 'visitsCount'); $rsm->addScalarResult('visits', 'visits');
$rsm->addScalarResult('non_bot_visits_count', 'nonBotVisitsCount'); $rsm->addScalarResult('non_bot_visits', 'nonBotVisits');
return map( return map(
$this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm)->getResult(), $this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm)->getResult(),

View File

@ -10,6 +10,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver; use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
use Shlinkio\Shlink\Core\Tag\Entity\Tag; use Shlinkio\Shlink\Core\Tag\Entity\Tag;
use Shlinkio\Shlink\Core\Tag\Model\OrderableField;
use Shlinkio\Shlink\Core\Tag\Model\TagsListFiltering; use Shlinkio\Shlink\Core\Tag\Model\TagsListFiltering;
use Shlinkio\Shlink\Core\Tag\Repository\TagRepository; use Shlinkio\Shlink\Core\Tag\Repository\TagRepository;
use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\Visit;
@ -135,17 +136,21 @@ class TagRepositoryTest extends DatabaseTestCase
['baz', 1, 3, 2], ['baz', 1, 3, 2],
]]; ]];
yield 'ASC ordering' => [ yield 'ASC ordering' => [
new TagsListFiltering(null, null, null, Ordering::fromTuple(['tag', 'ASC'])), new TagsListFiltering(null, null, null, Ordering::fromTuple([OrderableField::TAG->value, 'ASC'])),
$defaultList, $defaultList,
]; ];
yield 'DESC ordering' => [new TagsListFiltering(null, null, null, Ordering::fromTuple(['tag', 'DESC'])), [ yield 'DESC ordering' => [new TagsListFiltering(null, null, null, Ordering::fromTuple(
[OrderableField::TAG->value, 'DESC'],
)), [
['foo', 2, 4, 3], ['foo', 2, 4, 3],
['baz', 1, 3, 2], ['baz', 1, 3, 2],
['bar', 3, 3, 2], ['bar', 3, 3, 2],
['another', 0, 0, 0], ['another', 0, 0, 0],
]]; ]];
yield 'short URLs count ASC ordering' => [ yield 'short URLs count ASC ordering' => [
new TagsListFiltering(null, null, null, Ordering::fromTuple(['shortUrlsCount', 'ASC'])), new TagsListFiltering(null, null, null, Ordering::fromTuple(
[OrderableField::SHORT_URLS_COUNT->value, 'ASC'],
)),
[ [
['another', 0, 0, 0], ['another', 0, 0, 0],
['baz', 1, 3, 2], ['baz', 1, 3, 2],
@ -154,7 +159,9 @@ class TagRepositoryTest extends DatabaseTestCase
], ],
]; ];
yield 'short URLs count DESC ordering' => [ yield 'short URLs count DESC ordering' => [
new TagsListFiltering(null, null, null, Ordering::fromTuple(['shortUrlsCount', 'DESC'])), new TagsListFiltering(null, null, null, Ordering::fromTuple(
[OrderableField::SHORT_URLS_COUNT->value, 'DESC'],
)),
[ [
['bar', 3, 3, 2], ['bar', 3, 3, 2],
['foo', 2, 4, 3], ['foo', 2, 4, 3],
@ -163,7 +170,18 @@ class TagRepositoryTest extends DatabaseTestCase
], ],
]; ];
yield 'visits count ASC ordering' => [ yield 'visits count ASC ordering' => [
new TagsListFiltering(null, null, null, Ordering::fromTuple(['visitsCount', 'ASC'])), new TagsListFiltering(null, null, null, Ordering::fromTuple([OrderableField::VISITS->value, 'ASC'])),
[
['another', 0, 0, 0],
['bar', 3, 3, 2],
['baz', 1, 3, 2],
['foo', 2, 4, 3],
],
];
yield 'non-bot visits count ASC ordering' => [
new TagsListFiltering(null, null, null, Ordering::fromTuple(
[OrderableField::NON_BOT_VISITS->value, 'ASC'],
)),
[ [
['another', 0, 0, 0], ['another', 0, 0, 0],
['bar', 3, 3, 2], ['bar', 3, 3, 2],
@ -172,7 +190,7 @@ class TagRepositoryTest extends DatabaseTestCase
], ],
]; ];
yield 'visits count DESC ordering' => [ yield 'visits count DESC ordering' => [
new TagsListFiltering(null, null, null, Ordering::fromTuple(['visitsCount', 'DESC'])), new TagsListFiltering(null, null, null, Ordering::fromTuple([OrderableField::VISITS->value, 'DESC'])),
[ [
['foo', 2, 4, 3], ['foo', 2, 4, 3],
['bar', 3, 3, 2], ['bar', 3, 3, 2],
@ -181,7 +199,7 @@ class TagRepositoryTest extends DatabaseTestCase
], ],
]; ];
yield 'visits count DESC ordering and limit' => [ yield 'visits count DESC ordering and limit' => [
new TagsListFiltering(2, null, null, Ordering::fromTuple(['visitsCount', 'DESC'])), new TagsListFiltering(2, null, null, Ordering::fromTuple([OrderableField::VISITS_COUNT->value, 'DESC'])),
[ [
['foo', 2, 4, 3], ['foo', 2, 4, 3],
['bar', 3, 3, 2], ['bar', 3, 3, 2],
@ -195,11 +213,11 @@ class TagRepositoryTest extends DatabaseTestCase
['foo', 1, 3, 2], ['foo', 1, 3, 2],
]]; ]];
yield 'combined' => [new TagsListFiltering(1, null, null, Ordering::fromTuple( yield 'combined' => [new TagsListFiltering(1, null, null, Ordering::fromTuple(
['shortUrls', 'DESC'], [OrderableField::SHORT_URLS_COUNT->value, 'DESC'],
), ApiKey::fromMeta( ), ApiKey::fromMeta(
ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()), ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()),
)), [ )), [
['foo', 1, 3, 2], ['bar', 2, 3, 2],
]]; ]];
} }