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"
]
}
},
{
"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": [

View File

@ -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([

View File

@ -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),
);
}

View File

@ -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));
}
}

View File

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

View File

@ -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,
);
}
}

View File

@ -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) {

View File

@ -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,
)));
}
}

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('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());