Allow short URLs list to be filtered by domain authority

This commit is contained in:
Alejandro Celaya 2024-10-28 09:27:33 +01:00
parent 525a306ec6
commit bb270396b6
9 changed files with 83 additions and 25 deletions

View File

@ -125,6 +125,15 @@
"false" "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": [ "security": [

View File

@ -74,7 +74,7 @@ class ListShortUrlsCommandTest extends TestCase
} }
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with( $this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
ShortUrlsParams::emptyInstance(), ShortUrlsParams::empty(),
)->willReturn(new Paginator(new ArrayAdapter($data))); )->willReturn(new Paginator(new ArrayAdapter($data)));
$this->commandTester->setInputs(['n']); $this->commandTester->setInputs(['n']);
@ -110,7 +110,7 @@ class ListShortUrlsCommandTest extends TestCase
ApiKey $apiKey, ApiKey $apiKey,
): void { ): void {
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with( $this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
ShortUrlsParams::emptyInstance(), ShortUrlsParams::empty(),
)->willReturn(new Paginator(new ArrayAdapter([ )->willReturn(new Paginator(new ArrayAdapter([
ShortUrlWithVisitsSummary::fromShortUrl( ShortUrlWithVisitsSummary::fromShortUrl(
ShortUrl::create(ShortUrlCreation::fromRawData([ ShortUrl::create(ShortUrlCreation::fromRawData([

View File

@ -19,17 +19,18 @@ final class ShortUrlsParams
private function __construct( private function __construct(
public readonly int $page, public readonly int $page,
public readonly int $itemsPerPage, public readonly int $itemsPerPage,
public readonly ?string $searchTerm, public readonly string|null $searchTerm,
public readonly array $tags, public readonly array $tags,
public readonly Ordering $orderBy, public readonly Ordering $orderBy,
public readonly ?DateRange $dateRange, public readonly ?DateRange $dateRange,
public readonly bool $excludeMaxVisitsReached, public readonly bool $excludeMaxVisitsReached,
public readonly bool $excludePastValidUntil, public readonly bool $excludePastValidUntil,
public readonly TagsMode $tagsMode = TagsMode::ANY, 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([]); return self::fromRawData([]);
} }
@ -59,6 +60,7 @@ final class ShortUrlsParams
excludeMaxVisitsReached: $inputFilter->getValue(ShortUrlsParamsInputFilter::EXCLUDE_MAX_VISITS_REACHED), excludeMaxVisitsReached: $inputFilter->getValue(ShortUrlsParamsInputFilter::EXCLUDE_MAX_VISITS_REACHED),
excludePastValidUntil: $inputFilter->getValue(ShortUrlsParamsInputFilter::EXCLUDE_PAST_VALID_UNTIL), excludePastValidUntil: $inputFilter->getValue(ShortUrlsParamsInputFilter::EXCLUDE_PAST_VALID_UNTIL),
tagsMode: self::resolveTagsMode($inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS_MODE)), tagsMode: self::resolveTagsMode($inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS_MODE)),
domain: $inputFilter->getValue(ShortUrlsParamsInputFilter::DOMAIN),
); );
} }

View File

@ -26,6 +26,7 @@ class ShortUrlsParamsInputFilter extends InputFilter
public const ORDER_BY = 'orderBy'; public const ORDER_BY = 'orderBy';
public const EXCLUDE_MAX_VISITS_REACHED = 'excludeMaxVisitsReached'; public const EXCLUDE_MAX_VISITS_REACHED = 'excludeMaxVisitsReached';
public const EXCLUDE_PAST_VALID_UNTIL = 'excludePastValidUntil'; public const EXCLUDE_PAST_VALID_UNTIL = 'excludePastValidUntil';
public const DOMAIN = 'domain';
public function __construct(array $data) 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_MAX_VISITS_REACHED));
$this->add(InputFactory::boolean(self::EXCLUDE_PAST_VALID_UNTIL)); $this->add(InputFactory::boolean(self::EXCLUDE_PAST_VALID_UNTIL));
$this->add(InputFactory::basic(self::DOMAIN));
} }
} }

View File

@ -44,6 +44,7 @@ class ShortUrlsCountFiltering
$params->excludePastValidUntil, $params->excludePastValidUntil,
$apiKey, $apiKey,
$defaultDomain, $defaultDomain,
$params->domain,
); );
} }
} }

View File

@ -23,7 +23,9 @@ class ShortUrlsListFiltering extends ShortUrlsCountFiltering
bool $excludeMaxVisitsReached = false, bool $excludeMaxVisitsReached = false,
bool $excludePastValidUntil = false, bool $excludePastValidUntil = false,
?ApiKey $apiKey = null, ?ApiKey $apiKey = null,
// Used only to determine if search term includes default domain
?string $defaultDomain = null, ?string $defaultDomain = null,
?string $domain = null,
) { ) {
parent::__construct( parent::__construct(
$searchTerm, $searchTerm,
@ -34,6 +36,7 @@ class ShortUrlsListFiltering extends ShortUrlsCountFiltering
$excludePastValidUntil, $excludePastValidUntil,
$apiKey, $apiKey,
$defaultDomain, $defaultDomain,
$domain,
); );
} }
@ -56,6 +59,7 @@ class ShortUrlsListFiltering extends ShortUrlsCountFiltering
$params->excludePastValidUntil, $params->excludePastValidUntil,
$apiKey, $apiKey,
$defaultDomain, $defaultDomain,
$params->domain,
); );
} }
} }

View File

@ -9,6 +9,7 @@ use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository; use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType; 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\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\OrderableField; use Shlinkio\Shlink\Core\ShortUrl\Model\OrderableField;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlWithVisitsSummary; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlWithVisitsSummary;
@ -118,8 +119,8 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
$qb->expr()->like('d.authority', ':searchPattern'), $qb->expr()->like('d.authority', ':searchPattern'),
]; ];
// Include default domain in search if provided // Include default domain in search if included, and a domain was not explicitly provided
if ($filtering->searchIncludesDefaultDomain) { if ($filtering->searchIncludesDefaultDomain && $filtering->domain === null) {
$conditions[] = $qb->expr()->isNull('s.domain'); $conditions[] = $qb->expr()->isNull('s.domain');
} }
@ -142,6 +143,12 @@ class ShortUrlListRepository extends EntitySpecificationRepository implements Sh
} }
if ($filtering->domain !== null) { 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) { if ($filtering->excludeMaxVisitsReached) {

View File

@ -9,6 +9,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\Test;
use ReflectionObject; use ReflectionObject;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
use Shlinkio\Shlink\Core\Model\Ordering; use Shlinkio\Shlink\Core\Model\Ordering;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Model\OrderableField; use Shlinkio\Shlink\Core\ShortUrl\Model\OrderableField;
@ -261,16 +262,23 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();
$buildFiltering = static fn (string $searchTerm) => new ShortUrlsListFiltering( $buildFiltering = static fn (string $searchTerm = '', string|null $domain = null) => new ShortUrlsListFiltering(
searchTerm: $searchTerm, searchTerm: $searchTerm,
defaultDomain: 'deFaulT-domain.com', defaultDomain: 'deFaulT-domain.com',
domain: $domain,
); );
self::assertCount(2, $this->repo->findList($buildFiltering('default-dom'))); self::assertCount(2, $this->repo->findList($buildFiltering(searchTerm: 'default-dom')));
self::assertCount(2, $this->repo->findList($buildFiltering('DOM'))); self::assertCount(2, $this->repo->findList($buildFiltering(searchTerm: 'DOM')));
self::assertCount(1, $this->repo->findList($buildFiltering('another'))); self::assertCount(1, $this->repo->findList($buildFiltering(searchTerm: 'another')));
self::assertCount(3, $this->repo->findList($buildFiltering('foo'))); self::assertCount(3, $this->repo->findList($buildFiltering(searchTerm: 'foo')));
self::assertCount(0, $this->repo->findList($buildFiltering('no results'))); 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] #[Test]
@ -303,18 +311,42 @@ class ShortUrlListRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();
$filtering = static fn (bool $excludeMaxVisitsReached, bool $excludePastValidUntil) => $filtering = static fn (bool $excludeMaxVisitsReached, bool $excludePastValidUntil) =>
new ShortUrlsListFiltering( new ShortUrlsListFiltering(
excludeMaxVisitsReached: $excludeMaxVisitsReached, excludeMaxVisitsReached: $excludeMaxVisitsReached,
excludePastValidUntil: $excludePastValidUntil, excludePastValidUntil: $excludePastValidUntil,
); );
self::assertCount(4, $this->repo->findList($filtering(false, false))); self::assertCount(4, $this->repo->findList($filtering(
self::assertEquals(4, $this->repo->countList($filtering(false, false))); excludeMaxVisitsReached: false,
self::assertCount(3, $this->repo->findList($filtering(true, false))); excludePastValidUntil: false,
self::assertEquals(3, $this->repo->countList($filtering(true, false))); )));
self::assertCount(3, $this->repo->findList($filtering(false, true))); self::assertEquals(4, $this->repo->countList($filtering(
self::assertEquals(3, $this->repo->countList($filtering(false, true))); excludeMaxVisitsReached: false,
self::assertCount(2, $this->repo->findList($filtering(true, true))); excludePastValidUntil: false,
self::assertEquals(2, $this->repo->countList($filtering(true, true))); )));
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,
)));
} }
} }

View File

@ -42,7 +42,7 @@ class ShortUrlListServiceTest extends TestCase
$this->repo->expects($this->once())->method('findList')->willReturn($list); $this->repo->expects($this->once())->method('findList')->willReturn($list);
$this->repo->expects($this->once())->method('countList')->willReturn(count($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);
self::assertCount(4, $paginator->getCurrentPageResults()); self::assertCount(4, $paginator->getCurrentPageResults());