mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-20 01:09:56 +03:00
Merge pull request #1345 from acelaya-forks/feature/extended-tags-ordering
Feature/extended tags ordering
This commit is contained in:
commit
1f90af3aec
17 changed files with 157 additions and 140 deletions
|
@ -74,7 +74,7 @@
|
||||||
"phpunit/phpunit": "^9.5",
|
"phpunit/phpunit": "^9.5",
|
||||||
"roave/security-advisories": "dev-master",
|
"roave/security-advisories": "dev-master",
|
||||||
"shlinkio/php-coding-standard": "~2.2.0",
|
"shlinkio/php-coding-standard": "~2.2.0",
|
||||||
"shlinkio/shlink-test-utils": "^2.5",
|
"shlinkio/shlink-test-utils": "^3.0",
|
||||||
"symfony/var-dumper": "^6.0",
|
"symfony/var-dumper": "^6.0",
|
||||||
"veewee/composer-run-parallel": "^1.1"
|
"veewee/composer-run-parallel": "^1.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -100,7 +100,7 @@ services:
|
||||||
|
|
||||||
shlink_db_maria:
|
shlink_db_maria:
|
||||||
container_name: shlink_db_maria
|
container_name: shlink_db_maria
|
||||||
image: mariadb:10.5
|
image: mariadb:10.7
|
||||||
ports:
|
ports:
|
||||||
- "3308:3306"
|
- "3308:3306"
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
@ -45,13 +45,17 @@
|
||||||
{
|
{
|
||||||
"name": "orderBy",
|
"name": "orderBy",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"description": "To determine how to order the results.",
|
"description": "To determine how to order the results.<br /><br />**Important!** Ordering by `shortUrlsCount` or `visitsCount` has a [known performance issue](https://github.com/shlinkio/shlink/issues/1346) which makes loading a subset of the list take as much as loading the whole list.<br />If you plan to order by any of these fields, it's worth loading the whole list with no pagination.",
|
||||||
"required": false,
|
"required": false,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"tag-ASC",
|
"tag-ASC",
|
||||||
"tag-DESC"
|
"tag-DESC",
|
||||||
|
"shortUrlsCount-ASC",
|
||||||
|
"shortUrlsCount-DESC",
|
||||||
|
"visitsCount-ASC",
|
||||||
|
"visitsCount-DESC"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,8 +46,7 @@ class ListTagsCommand extends Command
|
||||||
|
|
||||||
return map(
|
return map(
|
||||||
$tags,
|
$tags,
|
||||||
static fn (TagInfo $tagInfo) =>
|
static fn (TagInfo $tagInfo) => [$tagInfo->tag(), $tagInfo->shortUrlsCount(), $tagInfo->visitsCount()],
|
||||||
[$tagInfo->tag()->__toString(), $tagInfo->shortUrlsCount(), $tagInfo->visitsCount()],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
|
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
|
@ -45,8 +44,8 @@ class ListTagsCommandTest extends TestCase
|
||||||
public function listOfTagsIsPrinted(): void
|
public function listOfTagsIsPrinted(): void
|
||||||
{
|
{
|
||||||
$tagsInfo = $this->tagService->tagsInfo(Argument::any())->willReturn(new Paginator(new ArrayAdapter([
|
$tagsInfo = $this->tagService->tagsInfo(Argument::any())->willReturn(new Paginator(new ArrayAdapter([
|
||||||
new TagInfo(new Tag('foo'), 10, 2),
|
new TagInfo('foo', 10, 2),
|
||||||
new TagInfo(new Tag('bar'), 7, 32),
|
new TagInfo('bar', 7, 32),
|
||||||
])));
|
])));
|
||||||
|
|
||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
|
|
|
@ -12,6 +12,9 @@ final class Ordering
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{string|null, string|null} $props
|
||||||
|
*/
|
||||||
public static function fromTuple(array $props): self
|
public static function fromTuple(array $props): self
|
||||||
{
|
{
|
||||||
[$field, $dir] = $props;
|
[$field, $dir] = $props;
|
||||||
|
|
|
@ -16,6 +16,7 @@ use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithInlinedApiKeySpecsEnsuringJoin;
|
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithInlinedApiKeySpecsEnsuringJoin;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
|
use function Functional\contains;
|
||||||
use function Functional\map;
|
use function Functional\map;
|
||||||
|
|
||||||
use const PHP_INT_MAX;
|
use const PHP_INT_MAX;
|
||||||
|
@ -40,12 +41,19 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
||||||
*/
|
*/
|
||||||
public function findTagsWithInfo(?TagsListFiltering $filtering = null): array
|
public function findTagsWithInfo(?TagsListFiltering $filtering = null): array
|
||||||
{
|
{
|
||||||
|
$orderField = $filtering?->orderBy()?->orderField();
|
||||||
|
$orderDir = $filtering?->orderBy()?->orderDirection();
|
||||||
|
$orderMainQuery = contains(['shortUrlsCount', 'visitsCount'], $orderField);
|
||||||
|
|
||||||
$conn = $this->getEntityManager()->getConnection();
|
$conn = $this->getEntityManager()->getConnection();
|
||||||
$subQb = $this->createQueryBuilder('t');
|
$subQb = $this->createQueryBuilder('t');
|
||||||
$subQb->select('t.id', 't.name')
|
$subQb->select('t.id', 't.name');
|
||||||
->orderBy('t.name', $filtering?->orderBy()?->orderDirection() ?? 'ASC') // TODO Make filed dynamic
|
|
||||||
->setMaxResults($filtering?->limit() ?? PHP_INT_MAX)
|
if (! $orderMainQuery) {
|
||||||
->setFirstResult($filtering?->offset() ?? 0);
|
$subQb->orderBy('t.name', $orderDir ?? 'ASC')
|
||||||
|
->setMaxResults($filtering?->limit() ?? PHP_INT_MAX)
|
||||||
|
->setFirstResult($filtering?->offset() ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
$searchTerm = $filtering?->searchTerm();
|
$searchTerm = $filtering?->searchTerm();
|
||||||
if ($searchTerm !== null) {
|
if ($searchTerm !== null) {
|
||||||
|
@ -53,7 +61,7 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
||||||
}
|
}
|
||||||
|
|
||||||
$apiKey = $filtering?->apiKey();
|
$apiKey = $filtering?->apiKey();
|
||||||
$this->applySpecification($subQb, new WithInlinedApiKeySpecsEnsuringJoin($apiKey, 'shortUrls'), 't');
|
$this->applySpecification($subQb, new WithInlinedApiKeySpecsEnsuringJoin($apiKey), 't');
|
||||||
|
|
||||||
$subQuery = $subQb->getQuery();
|
$subQuery = $subQb->getQuery();
|
||||||
$subQuerySql = $subQuery->getSQL();
|
$subQuerySql = $subQuery->getSQL();
|
||||||
|
@ -73,11 +81,10 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
||||||
->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'))
|
||||||
->leftJoin('st', 'short_urls', 's', $nativeQb->expr()->eq('s.id', 'st.short_url_id'))
|
->leftJoin('st', 'short_urls', 's', $nativeQb->expr()->eq('s.id', 'st.short_url_id'))
|
||||||
->leftJoin('st', 'visits', 'v', $nativeQb->expr()->eq('s.id', 'v.short_url_id'))
|
->leftJoin('st', 'visits', 'v', $nativeQb->expr()->eq('s.id', 'v.short_url_id'))
|
||||||
->groupBy('t.id_0', 't.name_1')
|
->groupBy('t.id_0', 't.name_1');
|
||||||
->orderBy('t.name_1', $filtering?->orderBy()?->orderDirection() ?? 'ASC'); // TODO Make field dynamic
|
|
||||||
|
|
||||||
// Apply API key role conditions to the native query too, as they will affect the amounts on the aggregates
|
// Apply API key role conditions to the native query too, as they will affect the amounts on the aggregates
|
||||||
$apiKey?->mapRoles(fn (string $roleName, array $meta) => match ($roleName) {
|
$apiKey?->mapRoles(static fn (string $roleName, array $meta) => match ($roleName) {
|
||||||
Role::DOMAIN_SPECIFIC => $nativeQb->andWhere(
|
Role::DOMAIN_SPECIFIC => $nativeQb->andWhere(
|
||||||
$nativeQb->expr()->eq('s.domain_id', $conn->quote(Role::domainIdFromMeta($meta))),
|
$nativeQb->expr()->eq('s.domain_id', $conn->quote(Role::domainIdFromMeta($meta))),
|
||||||
),
|
),
|
||||||
|
@ -87,14 +94,27 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
||||||
default => $nativeQb,
|
default => $nativeQb,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ($orderMainQuery) {
|
||||||
|
$nativeQb
|
||||||
|
->orderBy(
|
||||||
|
$orderField === 'shortUrlsCount' ? 'short_urls_count' : 'visits_count',
|
||||||
|
$orderDir ?? 'ASC',
|
||||||
|
)
|
||||||
|
->setMaxResults($filtering?->limit() ?? PHP_INT_MAX)
|
||||||
|
->setFirstResult($filtering?->offset() ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ordering by tag name, as a fallback in case of same amount, or as default ordering
|
||||||
|
$nativeQb->addOrderBy('t.name_1', $orderMainQuery || $orderDir === null ? 'ASC' : $orderDir);
|
||||||
|
|
||||||
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
|
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
|
||||||
$rsm->addRootEntityFromClassMetadata(Tag::class, 't');
|
$rsm->addScalarResult('name', 'tag');
|
||||||
$rsm->addScalarResult('short_urls_count', 'shortUrlsCount');
|
$rsm->addScalarResult('short_urls_count', 'shortUrlsCount');
|
||||||
$rsm->addScalarResult('visits_count', 'visitsCount');
|
$rsm->addScalarResult('visits_count', 'visitsCount');
|
||||||
|
|
||||||
return map(
|
return map(
|
||||||
$this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm)->getResult(),
|
$this->getEntityManager()->createNativeQuery($nativeQb->getSQL(), $rsm)->getResult(),
|
||||||
static fn (array $row) => new TagInfo($row[0], (int) $row['shortUrlsCount'], (int) $row['visitsCount']),
|
static fn (array $row) => new TagInfo($row['tag'], (int) $row['shortUrlsCount'], (int) $row['visitsCount']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,15 +5,14 @@ declare(strict_types=1);
|
||||||
namespace Shlinkio\Shlink\Core\Tag\Model;
|
namespace Shlinkio\Shlink\Core\Tag\Model;
|
||||||
|
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
|
||||||
|
|
||||||
final class TagInfo implements JsonSerializable
|
final class TagInfo implements JsonSerializable
|
||||||
{
|
{
|
||||||
public function __construct(private Tag $tag, private int $shortUrlsCount, private int $visitsCount)
|
public function __construct(private string $tag, private int $shortUrlsCount, private int $visitsCount)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function tag(): Tag
|
public function tag(): string
|
||||||
{
|
{
|
||||||
return $this->tag;
|
return $this->tag;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ class DomainRepositoryTest extends DatabaseTestCase
|
||||||
{
|
{
|
||||||
private DomainRepository $repo;
|
private DomainRepository $repo;
|
||||||
|
|
||||||
protected function beforeEach(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->repo = $this->getEntityManager()->getRepository(Domain::class);
|
$this->repo = $this->getEntityManager()->getRepository(Domain::class);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||||
private ShortUrlRepository $repo;
|
private ShortUrlRepository $repo;
|
||||||
private PersistenceShortUrlRelationResolver $relationResolver;
|
private PersistenceShortUrlRelationResolver $relationResolver;
|
||||||
|
|
||||||
public function beforeEach(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->repo = $this->getEntityManager()->getRepository(ShortUrl::class);
|
$this->repo = $this->getEntityManager()->getRepository(ShortUrl::class);
|
||||||
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
||||||
|
|
|
@ -13,7 +13,6 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
use Shlinkio\Shlink\Core\Repository\TagRepository;
|
use Shlinkio\Shlink\Core\Repository\TagRepository;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
|
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagsListFiltering;
|
use Shlinkio\Shlink\Core\Tag\Model\TagsListFiltering;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
||||||
|
@ -21,13 +20,14 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||||
|
|
||||||
use function array_chunk;
|
use function array_chunk;
|
||||||
|
use function count;
|
||||||
|
|
||||||
class TagRepositoryTest extends DatabaseTestCase
|
class TagRepositoryTest extends DatabaseTestCase
|
||||||
{
|
{
|
||||||
private TagRepository $repo;
|
private TagRepository $repo;
|
||||||
private PersistenceShortUrlRelationResolver $relationResolver;
|
private PersistenceShortUrlRelationResolver $relationResolver;
|
||||||
|
|
||||||
protected function beforeEach(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->repo = $this->getEntityManager()->getRepository(Tag::class);
|
$this->repo = $this->getEntityManager()->getRepository(Tag::class);
|
||||||
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
||||||
|
@ -57,7 +57,7 @@ class TagRepositoryTest extends DatabaseTestCase
|
||||||
* @test
|
* @test
|
||||||
* @dataProvider provideFilters
|
* @dataProvider provideFilters
|
||||||
*/
|
*/
|
||||||
public function properTagsInfoIsReturned(?TagsListFiltering $filtering, callable $asserts): void
|
public function properTagsInfoIsReturned(?TagsListFiltering $filtering, array $expectedList): void
|
||||||
{
|
{
|
||||||
$names = ['foo', 'bar', 'baz', 'another'];
|
$names = ['foo', 'bar', 'baz', 'another'];
|
||||||
foreach ($names as $name) {
|
foreach ($names as $name) {
|
||||||
|
@ -86,126 +86,120 @@ class TagRepositoryTest extends DatabaseTestCase
|
||||||
$shortUrl2 = ShortUrl::fromMeta($metaWithTags($secondUrlTags, null), $this->relationResolver);
|
$shortUrl2 = ShortUrl::fromMeta($metaWithTags($secondUrlTags, null), $this->relationResolver);
|
||||||
$this->getEntityManager()->persist($shortUrl2);
|
$this->getEntityManager()->persist($shortUrl2);
|
||||||
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance()));
|
$this->getEntityManager()->persist(Visit::forValidShortUrl($shortUrl2, Visitor::emptyInstance()));
|
||||||
|
|
||||||
|
// One of the tags has two extra short URLs, but with no visits
|
||||||
|
$this->getEntityManager()->persist(
|
||||||
|
ShortUrl::fromMeta($metaWithTags(['bar'], null), $this->relationResolver),
|
||||||
|
);
|
||||||
|
$this->getEntityManager()->persist(
|
||||||
|
ShortUrl::fromMeta($metaWithTags(['bar'], $apiKey), $this->relationResolver),
|
||||||
|
);
|
||||||
|
|
||||||
$this->getEntityManager()->flush();
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
$result = $this->repo->findTagsWithInfo($filtering);
|
$result = $this->repo->findTagsWithInfo($filtering);
|
||||||
|
|
||||||
$asserts($result, $names);
|
self::assertCount(count($expectedList), $result);
|
||||||
|
foreach ($expectedList as $index => [$tag, $shortUrlsCount, $visitsCount]) {
|
||||||
|
self::assertEquals($shortUrlsCount, $result[$index]->shortUrlsCount());
|
||||||
|
self::assertEquals($visitsCount, $result[$index]->visitsCount());
|
||||||
|
self::assertEquals($tag, $result[$index]->tag());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideFilters(): iterable
|
public function provideFilters(): iterable
|
||||||
{
|
{
|
||||||
$defaultAsserts = static function (array $result, array $tagNames): void {
|
$defaultList = [
|
||||||
/** @var TagInfo[] $result */
|
['another', 0, 0],
|
||||||
self::assertCount(4, $result);
|
['bar', 3, 3],
|
||||||
self::assertEquals(0, $result[0]->shortUrlsCount());
|
['baz', 1, 3],
|
||||||
self::assertEquals(0, $result[0]->visitsCount());
|
['foo', 2, 4],
|
||||||
self::assertEquals($tagNames[3], $result[0]->tag()->__toString());
|
|
||||||
|
|
||||||
self::assertEquals(1, $result[1]->shortUrlsCount());
|
|
||||||
self::assertEquals(3, $result[1]->visitsCount());
|
|
||||||
self::assertEquals($tagNames[1], $result[1]->tag()->__toString());
|
|
||||||
|
|
||||||
self::assertEquals(1, $result[2]->shortUrlsCount());
|
|
||||||
self::assertEquals(3, $result[2]->visitsCount());
|
|
||||||
self::assertEquals($tagNames[2], $result[2]->tag()->__toString());
|
|
||||||
|
|
||||||
self::assertEquals(2, $result[3]->shortUrlsCount());
|
|
||||||
self::assertEquals(4, $result[3]->visitsCount());
|
|
||||||
self::assertEquals($tagNames[0], $result[3]->tag()->__toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
yield 'no filter' => [null, $defaultAsserts];
|
|
||||||
yield 'empty filter' => [new TagsListFiltering(), $defaultAsserts];
|
|
||||||
yield 'limit' => [new TagsListFiltering(2), static function (array $result, array $tagNames): void {
|
|
||||||
/** @var TagInfo[] $result */
|
|
||||||
self::assertCount(2, $result);
|
|
||||||
self::assertEquals(0, $result[0]->shortUrlsCount());
|
|
||||||
self::assertEquals(0, $result[0]->visitsCount());
|
|
||||||
self::assertEquals($tagNames[3], $result[0]->tag()->__toString());
|
|
||||||
|
|
||||||
self::assertEquals(1, $result[1]->shortUrlsCount());
|
|
||||||
self::assertEquals(3, $result[1]->visitsCount());
|
|
||||||
self::assertEquals($tagNames[1], $result[1]->tag()->__toString());
|
|
||||||
}];
|
|
||||||
yield 'offset' => [new TagsListFiltering(null, 3), static function (array $result, array $tagNames): void {
|
|
||||||
/** @var TagInfo[] $result */
|
|
||||||
self::assertCount(1, $result);
|
|
||||||
self::assertEquals(2, $result[0]->shortUrlsCount());
|
|
||||||
self::assertEquals(4, $result[0]->visitsCount());
|
|
||||||
self::assertEquals($tagNames[0], $result[0]->tag()->__toString());
|
|
||||||
}];
|
|
||||||
yield 'limit and offset' => [
|
|
||||||
new TagsListFiltering(2, 1),
|
|
||||||
static function (array $result, array $tagNames): void {
|
|
||||||
/** @var TagInfo[] $result */
|
|
||||||
self::assertCount(2, $result);
|
|
||||||
self::assertEquals(1, $result[0]->shortUrlsCount());
|
|
||||||
self::assertEquals(3, $result[0]->visitsCount());
|
|
||||||
self::assertEquals($tagNames[1], $result[0]->tag()->__toString());
|
|
||||||
|
|
||||||
self::assertEquals(1, $result[1]->shortUrlsCount());
|
|
||||||
self::assertEquals(3, $result[1]->visitsCount());
|
|
||||||
self::assertEquals($tagNames[2], $result[1]->tag()->__toString());
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
yield 'search term' => [
|
|
||||||
new TagsListFiltering(null, null, 'ba'),
|
|
||||||
static function (array $result, array $tagNames): void {
|
|
||||||
/** @var TagInfo[] $result */
|
|
||||||
self::assertCount(2, $result);
|
|
||||||
self::assertEquals(1, $result[0]->shortUrlsCount());
|
|
||||||
self::assertEquals(3, $result[0]->visitsCount());
|
|
||||||
self::assertEquals($tagNames[1], $result[0]->tag()->__toString());
|
|
||||||
|
|
||||||
self::assertEquals(1, $result[1]->shortUrlsCount());
|
yield 'no filter' => [null, $defaultList];
|
||||||
self::assertEquals(3, $result[1]->visitsCount());
|
yield 'empty filter' => [new TagsListFiltering(), $defaultList];
|
||||||
self::assertEquals($tagNames[2], $result[1]->tag()->__toString());
|
yield 'limit' => [new TagsListFiltering(2), [
|
||||||
},
|
['another', 0, 0],
|
||||||
];
|
['bar', 3, 3],
|
||||||
|
]];
|
||||||
|
yield 'offset' => [new TagsListFiltering(null, 3), [
|
||||||
|
['foo', 2, 4],
|
||||||
|
]];
|
||||||
|
yield 'limit and offset' => [new TagsListFiltering(2, 1), [
|
||||||
|
['bar', 3, 3],
|
||||||
|
['baz', 1, 3],
|
||||||
|
]];
|
||||||
|
yield 'search term' => [new TagsListFiltering(null, null, 'ba'), [
|
||||||
|
['bar', 3, 3],
|
||||||
|
['baz', 1, 3],
|
||||||
|
]];
|
||||||
yield 'ASC ordering' => [
|
yield 'ASC ordering' => [
|
||||||
new TagsListFiltering(null, null, null, Ordering::fromTuple(['tag', 'ASC'])),
|
new TagsListFiltering(null, null, null, Ordering::fromTuple(['tag', 'ASC'])),
|
||||||
$defaultAsserts,
|
$defaultList,
|
||||||
];
|
];
|
||||||
yield 'DESC ordering' => [
|
yield 'DESC ordering' => [new TagsListFiltering(null, null, null, Ordering::fromTuple(['tag', 'DESC'])), [
|
||||||
new TagsListFiltering(null, null, null, Ordering::fromTuple(['tag', 'DESC'])),
|
['foo', 2, 4],
|
||||||
static function (array $result, array $tagNames): void {
|
['baz', 1, 3],
|
||||||
/** @var TagInfo[] $result */
|
['bar', 3, 3],
|
||||||
self::assertCount(4, $result);
|
['another', 0, 0],
|
||||||
self::assertEquals(0, $result[3]->shortUrlsCount());
|
]];
|
||||||
self::assertEquals(0, $result[3]->visitsCount());
|
yield 'short URLs count ASC ordering' => [
|
||||||
self::assertEquals($tagNames[3], $result[3]->tag()->__toString());
|
new TagsListFiltering(null, null, null, Ordering::fromTuple(['shortUrlsCount', 'ASC'])),
|
||||||
|
[
|
||||||
self::assertEquals(1, $result[2]->shortUrlsCount());
|
['another', 0, 0],
|
||||||
self::assertEquals(3, $result[2]->visitsCount());
|
['baz', 1, 3],
|
||||||
self::assertEquals($tagNames[1], $result[2]->tag()->__toString());
|
['foo', 2, 4],
|
||||||
|
['bar', 3, 3],
|
||||||
self::assertEquals(1, $result[1]->shortUrlsCount());
|
],
|
||||||
self::assertEquals(3, $result[1]->visitsCount());
|
];
|
||||||
self::assertEquals($tagNames[2], $result[1]->tag()->__toString());
|
yield 'short URLs count DESC ordering' => [
|
||||||
|
new TagsListFiltering(null, null, null, Ordering::fromTuple(['shortUrlsCount', 'DESC'])),
|
||||||
self::assertEquals(2, $result[0]->shortUrlsCount());
|
[
|
||||||
self::assertEquals(4, $result[0]->visitsCount());
|
['bar', 3, 3],
|
||||||
self::assertEquals($tagNames[0], $result[0]->tag()->__toString());
|
['foo', 2, 4],
|
||||||
},
|
['baz', 1, 3],
|
||||||
|
['another', 0, 0],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
yield 'visits count ASC ordering' => [
|
||||||
|
new TagsListFiltering(null, null, null, Ordering::fromTuple(['visitsCount', 'ASC'])),
|
||||||
|
[
|
||||||
|
['another', 0, 0],
|
||||||
|
['bar', 3, 3],
|
||||||
|
['baz', 1, 3],
|
||||||
|
['foo', 2, 4],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
yield 'visits count DESC ordering' => [
|
||||||
|
new TagsListFiltering(null, null, null, Ordering::fromTuple(['visitsCount', 'DESC'])),
|
||||||
|
[
|
||||||
|
['foo', 2, 4],
|
||||||
|
['bar', 3, 3],
|
||||||
|
['baz', 1, 3],
|
||||||
|
['another', 0, 0],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
yield 'visits count DESC ordering and limit' => [
|
||||||
|
new TagsListFiltering(2, null, null, Ordering::fromTuple(['visitsCount', 'DESC'])),
|
||||||
|
[
|
||||||
|
['foo', 2, 4],
|
||||||
|
['bar', 3, 3],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
yield 'api key' => [new TagsListFiltering(null, null, null, null, ApiKey::fromMeta(
|
yield 'api key' => [new TagsListFiltering(null, null, null, null, ApiKey::fromMeta(
|
||||||
ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()),
|
ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()),
|
||||||
)), static function (array $result, array $tagNames): void {
|
)), [
|
||||||
/** @var TagInfo[] $result */
|
['bar', 2, 3],
|
||||||
self::assertCount(3, $result);
|
['baz', 1, 3],
|
||||||
self::assertEquals(1, $result[0]->shortUrlsCount());
|
['foo', 1, 3],
|
||||||
self::assertEquals(3, $result[0]->visitsCount());
|
]];
|
||||||
self::assertEquals($tagNames[1], $result[0]->tag()->__toString());
|
yield 'combined' => [new TagsListFiltering(1, null, null, Ordering::fromTuple(
|
||||||
|
['shortUrls', 'DESC'],
|
||||||
self::assertEquals(1, $result[1]->shortUrlsCount());
|
), ApiKey::fromMeta(
|
||||||
self::assertEquals(3, $result[1]->visitsCount());
|
ApiKeyMeta::withRoles(RoleDefinition::forAuthoredShortUrls()),
|
||||||
self::assertEquals($tagNames[2], $result[1]->tag()->__toString());
|
)), [
|
||||||
|
['foo', 1, 3],
|
||||||
self::assertEquals(1, $result[2]->shortUrlsCount());
|
]];
|
||||||
self::assertEquals(3, $result[2]->visitsCount());
|
|
||||||
self::assertEquals($tagNames[0], $result[2]->tag()->__toString());
|
|
||||||
}];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
|
|
|
@ -38,7 +38,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||||
private VisitRepository $repo;
|
private VisitRepository $repo;
|
||||||
private PersistenceShortUrlRelationResolver $relationResolver;
|
private PersistenceShortUrlRelationResolver $relationResolver;
|
||||||
|
|
||||||
protected function beforeEach(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->repo = $this->getEntityManager()->getRepository(Visit::class);
|
$this->repo = $this->getEntityManager()->getRepository(Visit::class);
|
||||||
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
$this->relationResolver = new PersistenceShortUrlRelationResolver($this->getEntityManager());
|
||||||
|
|
|
@ -16,7 +16,7 @@ class TagsPaginatorAdapterTest extends DatabaseTestCase
|
||||||
{
|
{
|
||||||
private TagRepository $repo;
|
private TagRepository $repo;
|
||||||
|
|
||||||
protected function beforeEach(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->repo = $this->getEntityManager()->getRepository(Tag::class);
|
$this->repo = $this->getEntityManager()->getRepository(Tag::class);
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ class TagServiceTest extends TestCase
|
||||||
TagsListFiltering $expectedFiltering,
|
TagsListFiltering $expectedFiltering,
|
||||||
int $countCalls,
|
int $countCalls,
|
||||||
): void {
|
): void {
|
||||||
$expected = [new TagInfo(new Tag('foo'), 1, 1), new TagInfo(new Tag('bar'), 3, 10)];
|
$expected = [new TagInfo('foo', 1, 1), new TagInfo('bar', 3, 10)];
|
||||||
|
|
||||||
$find = $this->repo->findTagsWithInfo($expectedFiltering)->willReturn($expected);
|
$find = $this->repo->findTagsWithInfo($expectedFiltering)->willReturn($expected);
|
||||||
$count = $this->repo->matchSingleScalarResult(Argument::cetera())->willReturn(2);
|
$count = $this->repo->matchSingleScalarResult(Argument::cetera())->willReturn(2);
|
||||||
|
|
|
@ -41,7 +41,7 @@ class ListTagsAction extends AbstractRestAction
|
||||||
// This part is deprecated. To get tags with stats, the /tags/stats endpoint should be used instead
|
// 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());
|
||||||
|
|
||||||
return new JsonResponse(['tags' => $rawTags]);
|
return new JsonResponse(['tags' => $rawTags]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,8 +76,8 @@ class ListTagsActionTest extends TestCase
|
||||||
public function returnsStatsWhenRequested(): void
|
public function returnsStatsWhenRequested(): void
|
||||||
{
|
{
|
||||||
$stats = [
|
$stats = [
|
||||||
new TagInfo(new Tag('foo'), 1, 1),
|
new TagInfo('foo', 1, 1),
|
||||||
new TagInfo(new Tag('bar'), 3, 10),
|
new TagInfo('bar', 3, 10),
|
||||||
];
|
];
|
||||||
$itemsCount = count($stats);
|
$itemsCount = count($stats);
|
||||||
$tagsInfo = $this->tagService->tagsInfo(Argument::any(), Argument::type(ApiKey::class))->willReturn(
|
$tagsInfo = $this->tagService->tagsInfo(Argument::any(), Argument::type(ApiKey::class))->willReturn(
|
||||||
|
|
|
@ -13,7 +13,6 @@ use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
|
||||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||||
use Shlinkio\Shlink\Rest\Action\Tag\TagsStatsAction;
|
use Shlinkio\Shlink\Rest\Action\Tag\TagsStatsAction;
|
||||||
|
@ -38,8 +37,8 @@ class TagsStatsActionTest extends TestCase
|
||||||
public function returnsTagsStatsWhenRequested(): void
|
public function returnsTagsStatsWhenRequested(): void
|
||||||
{
|
{
|
||||||
$stats = [
|
$stats = [
|
||||||
new TagInfo(new Tag('foo'), 1, 1),
|
new TagInfo('foo', 1, 1),
|
||||||
new TagInfo(new Tag('bar'), 3, 10),
|
new TagInfo('bar', 3, 10),
|
||||||
];
|
];
|
||||||
$itemsCount = count($stats);
|
$itemsCount = count($stats);
|
||||||
$tagsInfo = $this->tagService->tagsInfo(Argument::any(), Argument::type(ApiKey::class))->willReturn(
|
$tagsInfo = $this->tagService->tagsInfo(Argument::any(), Argument::type(ApiKey::class))->willReturn(
|
||||||
|
|
Loading…
Add table
Reference in a new issue