From bb270396b6f7a0617c49bea762f4c13aa05ae8fc Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Mon, 28 Oct 2024 09:27:33 +0100 Subject: [PATCH] Allow short URLs list to be filtered by domain authority --- docs/swagger/paths/v1_short-urls.json | 9 +++ .../ShortUrl/ListShortUrlsCommandTest.php | 4 +- .../src/ShortUrl/Model/ShortUrlsParams.php | 6 +- .../Validation/ShortUrlsParamsInputFilter.php | 3 + .../Persistence/ShortUrlsCountFiltering.php | 1 + .../Persistence/ShortUrlsListFiltering.php | 4 ++ .../Repository/ShortUrlListRepository.php | 11 ++- .../Repository/ShortUrlListRepositoryTest.php | 68 ++++++++++++++----- .../test/ShortUrl/ShortUrlListServiceTest.php | 2 +- 9 files changed, 83 insertions(+), 25 deletions(-) diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index 89bdaaf4..6ca05c2e 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -125,6 +125,15 @@ "false" ] } + }, + { + "name": "domain", + "in": "query", + "description": "Get short URLs for this particular domain only. Use **DEFAULT** keyword for default domain.", + "required": false, + "schema": { + "type": "string" + } } ], "security": [ diff --git a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php index 176800ab..c1a3ab23 100644 --- a/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/ListShortUrlsCommandTest.php @@ -74,7 +74,7 @@ class ListShortUrlsCommandTest extends TestCase } $this->shortUrlService->expects($this->once())->method('listShortUrls')->with( - ShortUrlsParams::emptyInstance(), + ShortUrlsParams::empty(), )->willReturn(new Paginator(new ArrayAdapter($data))); $this->commandTester->setInputs(['n']); @@ -110,7 +110,7 @@ class ListShortUrlsCommandTest extends TestCase ApiKey $apiKey, ): void { $this->shortUrlService->expects($this->once())->method('listShortUrls')->with( - ShortUrlsParams::emptyInstance(), + ShortUrlsParams::empty(), )->willReturn(new Paginator(new ArrayAdapter([ ShortUrlWithVisitsSummary::fromShortUrl( ShortUrl::create(ShortUrlCreation::fromRawData([ diff --git a/module/Core/src/ShortUrl/Model/ShortUrlsParams.php b/module/Core/src/ShortUrl/Model/ShortUrlsParams.php index 88e20aa7..e625087e 100644 --- a/module/Core/src/ShortUrl/Model/ShortUrlsParams.php +++ b/module/Core/src/ShortUrl/Model/ShortUrlsParams.php @@ -19,17 +19,18 @@ final class ShortUrlsParams private function __construct( public readonly int $page, public readonly int $itemsPerPage, - public readonly ?string $searchTerm, + public readonly string|null $searchTerm, public readonly array $tags, public readonly Ordering $orderBy, public readonly ?DateRange $dateRange, public readonly bool $excludeMaxVisitsReached, public readonly bool $excludePastValidUntil, public readonly TagsMode $tagsMode = TagsMode::ANY, + public readonly string|null $domain = null, ) { } - public static function emptyInstance(): self + public static function empty(): self { return self::fromRawData([]); } @@ -59,6 +60,7 @@ final class ShortUrlsParams excludeMaxVisitsReached: $inputFilter->getValue(ShortUrlsParamsInputFilter::EXCLUDE_MAX_VISITS_REACHED), excludePastValidUntil: $inputFilter->getValue(ShortUrlsParamsInputFilter::EXCLUDE_PAST_VALID_UNTIL), tagsMode: self::resolveTagsMode($inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS_MODE)), + domain: $inputFilter->getValue(ShortUrlsParamsInputFilter::DOMAIN), ); } diff --git a/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php b/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php index 0a0d45ed..600ebc33 100644 --- a/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php +++ b/module/Core/src/ShortUrl/Model/Validation/ShortUrlsParamsInputFilter.php @@ -26,6 +26,7 @@ class ShortUrlsParamsInputFilter extends InputFilter public const ORDER_BY = 'orderBy'; public const EXCLUDE_MAX_VISITS_REACHED = 'excludeMaxVisitsReached'; public const EXCLUDE_PAST_VALID_UNTIL = 'excludePastValidUntil'; + public const DOMAIN = 'domain'; public function __construct(array $data) { @@ -56,5 +57,7 @@ class ShortUrlsParamsInputFilter extends InputFilter $this->add(InputFactory::boolean(self::EXCLUDE_MAX_VISITS_REACHED)); $this->add(InputFactory::boolean(self::EXCLUDE_PAST_VALID_UNTIL)); + + $this->add(InputFactory::basic(self::DOMAIN)); } } diff --git a/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php b/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php index 15b9d47f..b27fe7c5 100644 --- a/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php +++ b/module/Core/src/ShortUrl/Persistence/ShortUrlsCountFiltering.php @@ -44,6 +44,7 @@ class ShortUrlsCountFiltering $params->excludePastValidUntil, $apiKey, $defaultDomain, + $params->domain, ); } } diff --git a/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php b/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php index 589947dd..b3946ab1 100644 --- a/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php +++ b/module/Core/src/ShortUrl/Persistence/ShortUrlsListFiltering.php @@ -23,7 +23,9 @@ class ShortUrlsListFiltering extends ShortUrlsCountFiltering bool $excludeMaxVisitsReached = false, bool $excludePastValidUntil = false, ?ApiKey $apiKey = null, + // Used only to determine if search term includes default domain ?string $defaultDomain = null, + ?string $domain = null, ) { parent::__construct( $searchTerm, @@ -34,6 +36,7 @@ class ShortUrlsListFiltering extends ShortUrlsCountFiltering $excludePastValidUntil, $apiKey, $defaultDomain, + $domain, ); } @@ -56,6 +59,7 @@ class ShortUrlsListFiltering extends ShortUrlsCountFiltering $params->excludePastValidUntil, $apiKey, $defaultDomain, + $params->domain, ); } } diff --git a/module/Core/src/ShortUrl/Repository/ShortUrlListRepository.php b/module/Core/src/ShortUrl/Repository/ShortUrlListRepository.php index 70e9dbff..6749a03f 100644 --- a/module/Core/src/ShortUrl/Repository/ShortUrlListRepository.php +++ b/module/Core/src/ShortUrl/Repository/ShortUrlListRepository.php @@ -9,6 +9,7 @@ use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository; use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType; +use Shlinkio\Shlink\Core\Domain\Entity\Domain; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Model\OrderableField; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlWithVisitsSummary; @@ -118,8 +119,8 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh $qb->expr()->like('d.authority', ':searchPattern'), ]; - // Include default domain in search if provided - if ($filtering->searchIncludesDefaultDomain) { + // Include default domain in search if included, and a domain was not explicitly provided + if ($filtering->searchIncludesDefaultDomain && $filtering->domain === null) { $conditions[] = $qb->expr()->isNull('s.domain'); } @@ -142,6 +143,12 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh } if ($filtering->domain !== null) { + if ($filtering->domain === Domain::DEFAULT_AUTHORITY) { + $qb->andWhere($qb->expr()->isNull('s.domain')); + } else { + $qb->andWhere($qb->expr()->eq('d.authority', ':domain')) + ->setParameter('domain', $filtering->domain); + } } if ($filtering->excludeMaxVisitsReached) { diff --git a/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php b/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php index 95924956..995f7218 100644 --- a/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php +++ b/module/Core/test-db/ShortUrl/Repository/ShortUrlListRepositoryTest.php @@ -9,6 +9,7 @@ use Doctrine\Common\Collections\ArrayCollection; use PHPUnit\Framework\Attributes\Test; use ReflectionObject; use Shlinkio\Shlink\Common\Util\DateRange; +use Shlinkio\Shlink\Core\Domain\Entity\Domain; use Shlinkio\Shlink\Core\Model\Ordering; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Model\OrderableField; @@ -261,16 +262,23 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase $this->getEntityManager()->flush(); - $buildFiltering = static fn (string $searchTerm) => new ShortUrlsListFiltering( + $buildFiltering = static fn (string $searchTerm = '', string|null $domain = null) => new ShortUrlsListFiltering( searchTerm: $searchTerm, defaultDomain: 'deFaulT-domain.com', + domain: $domain, ); - self::assertCount(2, $this->repo->findList($buildFiltering('default-dom'))); - self::assertCount(2, $this->repo->findList($buildFiltering('DOM'))); - self::assertCount(1, $this->repo->findList($buildFiltering('another'))); - self::assertCount(3, $this->repo->findList($buildFiltering('foo'))); - self::assertCount(0, $this->repo->findList($buildFiltering('no results'))); + self::assertCount(2, $this->repo->findList($buildFiltering(searchTerm: 'default-dom'))); + self::assertCount(2, $this->repo->findList($buildFiltering(searchTerm: 'DOM'))); + self::assertCount(1, $this->repo->findList($buildFiltering(searchTerm: 'another'))); + self::assertCount(3, $this->repo->findList($buildFiltering(searchTerm: 'foo'))); + self::assertCount(0, $this->repo->findList($buildFiltering(searchTerm: 'no results'))); + self::assertCount(1, $this->repo->findList($buildFiltering(domain: 'another.com'))); + self::assertCount(0, $this->repo->findList($buildFiltering( + searchTerm: 'default-domain.com', + domain: 'another.com', + ))); + self::assertCount(2, $this->repo->findList($buildFiltering(domain: Domain::DEFAULT_AUTHORITY))); } #[Test] @@ -303,18 +311,42 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase $this->getEntityManager()->flush(); $filtering = static fn (bool $excludeMaxVisitsReached, bool $excludePastValidUntil) => - new ShortUrlsListFiltering( - excludeMaxVisitsReached: $excludeMaxVisitsReached, - excludePastValidUntil: $excludePastValidUntil, - ); + new ShortUrlsListFiltering( + excludeMaxVisitsReached: $excludeMaxVisitsReached, + excludePastValidUntil: $excludePastValidUntil, + ); - self::assertCount(4, $this->repo->findList($filtering(false, false))); - self::assertEquals(4, $this->repo->countList($filtering(false, false))); - self::assertCount(3, $this->repo->findList($filtering(true, false))); - self::assertEquals(3, $this->repo->countList($filtering(true, false))); - self::assertCount(3, $this->repo->findList($filtering(false, true))); - self::assertEquals(3, $this->repo->countList($filtering(false, true))); - self::assertCount(2, $this->repo->findList($filtering(true, true))); - self::assertEquals(2, $this->repo->countList($filtering(true, true))); + self::assertCount(4, $this->repo->findList($filtering( + excludeMaxVisitsReached: false, + excludePastValidUntil: false, + ))); + self::assertEquals(4, $this->repo->countList($filtering( + excludeMaxVisitsReached: false, + excludePastValidUntil: false, + ))); + self::assertCount(3, $this->repo->findList($filtering( + excludeMaxVisitsReached: true, + excludePastValidUntil: false, + ))); + self::assertEquals(3, $this->repo->countList($filtering( + excludeMaxVisitsReached: true, + excludePastValidUntil: false, + ))); + self::assertCount(3, $this->repo->findList($filtering( + excludeMaxVisitsReached: false, + excludePastValidUntil: true, + ))); + self::assertEquals(3, $this->repo->countList($filtering( + excludeMaxVisitsReached: false, + excludePastValidUntil: true, + ))); + self::assertCount(2, $this->repo->findList($filtering( + excludeMaxVisitsReached: true, + excludePastValidUntil: true, + ))); + self::assertEquals(2, $this->repo->countList($filtering( + excludeMaxVisitsReached: true, + excludePastValidUntil: true, + ))); } } diff --git a/module/Core/test/ShortUrl/ShortUrlListServiceTest.php b/module/Core/test/ShortUrl/ShortUrlListServiceTest.php index d8663761..2ae5c584 100644 --- a/module/Core/test/ShortUrl/ShortUrlListServiceTest.php +++ b/module/Core/test/ShortUrl/ShortUrlListServiceTest.php @@ -42,7 +42,7 @@ class ShortUrlListServiceTest extends TestCase $this->repo->expects($this->once())->method('findList')->willReturn($list); $this->repo->expects($this->once())->method('countList')->willReturn(count($list)); - $paginator = $this->service->listShortUrls(ShortUrlsParams::emptyInstance(), $apiKey); + $paginator = $this->service->listShortUrls(ShortUrlsParams::empty(), $apiKey); self::assertCount(4, $paginator); self::assertCount(4, $paginator->getCurrentPageResults());