Added logic to actually filter short URLs by any tag or all tags

This commit is contained in:
Alejandro Celaya 2022-01-04 14:23:21 +01:00
parent 665a3dbcbf
commit d8484e777f
4 changed files with 102 additions and 10 deletions

View File

@ -64,6 +64,12 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
InputOption::VALUE_REQUIRED,
'A comma-separated list of tags to filter results.',
)
->addOption(
'including-all-tags',
'i',
InputOption::VALUE_NONE,
'If tags is provided, returns only short URLs having ALL tags.',
)
->addOption(
'order-by',
'o',
@ -115,6 +121,9 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
$page = (int) $input->getOption('page');
$searchTerm = $input->getOption('search-term');
$tags = $input->getOption('tags');
$tagsMode = $input->getOption('including-all-tags') === true
? ShortUrlsParams::TAGS_MODE_ALL
: ShortUrlsParams::TAGS_MODE_ANY;
$tags = ! empty($tags) ? explode(',', $tags) : [];
$all = $input->getOption('all');
$startDate = $this->getStartDateOption($input, $output);
@ -125,6 +134,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
$data = [
ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm,
ShortUrlsParamsInputFilter::TAGS => $tags,
ShortUrlsParamsInputFilter::TAGS_MODE => $tagsMode,
ShortUrlsOrdering::ORDER_BY => $orderBy,
ShortUrlsParamsInputFilter::START_DATE => $startDate?->toAtomString(),
ShortUrlsParamsInputFilter::END_DATE => $endDate?->toAtomString(),

View File

@ -184,6 +184,7 @@ class ListShortUrlsCommandTest extends TestCase
?int $page,
?string $searchTerm,
array $tags,
string $tagsMode,
?string $startDate = null,
?string $endDate = null,
): void {
@ -191,6 +192,7 @@ class ListShortUrlsCommandTest extends TestCase
'page' => $page,
'searchTerm' => $searchTerm,
'tags' => $tags,
'tagsMode' => $tagsMode,
'startDate' => $startDate !== null ? Chronos::parse($startDate)->toAtomString() : null,
'endDate' => $endDate !== null ? Chronos::parse($endDate)->toAtomString() : null,
]))->willReturn(new Paginator(new ArrayAdapter([])));
@ -203,20 +205,23 @@ class ListShortUrlsCommandTest extends TestCase
public function provideArgs(): iterable
{
yield [[], 1, null, []];
yield [['--page' => $page = 3], $page, null, []];
yield [['--search-term' => $searchTerm = 'search this'], 1, $searchTerm, []];
yield [[], 1, null, [], ShortUrlsParams::TAGS_MODE_ANY];
yield [['--page' => $page = 3], $page, null, [], ShortUrlsParams::TAGS_MODE_ANY];
yield [['--including-all-tags' => true], 1, null, [], ShortUrlsParams::TAGS_MODE_ALL];
yield [['--search-term' => $searchTerm = 'search this'], 1, $searchTerm, [], ShortUrlsParams::TAGS_MODE_ANY];
yield [
['--page' => $page = 3, '--search-term' => $searchTerm = 'search this', '--tags' => $tags = 'foo,bar'],
$page,
$searchTerm,
explode(',', $tags),
ShortUrlsParams::TAGS_MODE_ANY,
];
yield [
['--start-date' => $startDate = '2019-01-01'],
1,
null,
[],
ShortUrlsParams::TAGS_MODE_ANY,
$startDate,
];
yield [
@ -224,6 +229,7 @@ class ListShortUrlsCommandTest extends TestCase
1,
null,
[],
ShortUrlsParams::TAGS_MODE_ANY,
null,
$endDate,
];
@ -232,6 +238,7 @@ class ListShortUrlsCommandTest extends TestCase
1,
null,
[],
ShortUrlsParams::TAGS_MODE_ANY,
$startDate,
$endDate,
];
@ -269,6 +276,7 @@ class ListShortUrlsCommandTest extends TestCase
'page' => 1,
'searchTerm' => null,
'tags' => [],
'tagsMode' => ShortUrlsParams::TAGS_MODE_ANY,
'startDate' => null,
'endDate' => null,
'orderBy' => null,

View File

@ -16,6 +16,7 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
use function array_column;
@ -130,8 +131,10 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
// Filter by tags if provided
if (! empty($tags)) {
$qb->join('s.tags', 't')
->andWhere($qb->expr()->in('t.name', $tags));
$tagsMode = $tagsMode ?? ShortUrlsParams::TAGS_MODE_ANY;
$tagsMode === ShortUrlsParams::TAGS_MODE_ANY
? $qb->join('s.tags', 't')->andWhere($qb->expr()->in('t.name', $tags))
: $this->joinAllTags($qb, $tags);
}
$this->applySpecification($qb, $spec, 's');
@ -261,11 +264,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
return $qb->getQuery()->getOneOrNullResult();
}
foreach ($tags as $index => $tag) {
$alias = 't_' . $index;
$qb->join('s.tags', $alias, Join::WITH, $alias . '.name = :tag' . $index)
->setParameter('tag' . $index, $tag);
}
$this->joinAllTags($qb, $tags);
// If tags where provided, we need an extra join to see the amount of tags that every short URL has, so that we
// can discard those that also have more tags, making sure only those fully matching are included.
@ -277,6 +276,15 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
return $qb->getQuery()->getOneOrNullResult();
}
private function joinAllTags(QueryBuilder $qb, array $tags): void
{
foreach ($tags as $index => $tag) {
$alias = 't_' . $index;
$qb->join('s.tags', $alias, Join::WITH, $alias . '.name = :tag' . $index)
->setParameter('tag' . $index, $tag);
}
}
public function findOneByImportedUrl(ImportedShlinkUrl $url): ?ShortUrl
{
$qb = $this->createQueryBuilder('s');

View File

@ -14,6 +14,7 @@ use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
@ -174,6 +175,71 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
self::assertEquals('z', $result[3]->getLongUrl());
}
/** @test */
public function findListReturnsOnlyThoseWithMatchingTags(): void
{
$shortUrl1 = ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'longUrl' => 'foo1',
'tags' => ['foo', 'bar'],
]), $this->relationResolver);
$this->getEntityManager()->persist($shortUrl1);
$shortUrl2 = ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'longUrl' => 'foo2',
'tags' => ['foo', 'baz'],
]), $this->relationResolver);
$this->getEntityManager()->persist($shortUrl2);
$shortUrl3 = ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'longUrl' => 'foo3',
'tags' => ['foo'],
]), $this->relationResolver);
$this->getEntityManager()->persist($shortUrl3);
$shortUrl4 = ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'longUrl' => 'foo4',
'tags' => ['bar', 'baz'],
]), $this->relationResolver);
$this->getEntityManager()->persist($shortUrl4);
$shortUrl5 = ShortUrl::fromMeta(ShortUrlMeta::fromRawData([
'longUrl' => 'foo5',
'tags' => ['bar', 'baz'],
]), $this->relationResolver);
$this->getEntityManager()->persist($shortUrl5);
$this->getEntityManager()->flush();
self::assertCount(5, $this->repo->findList(null, null, null, ['foo', 'bar']));
self::assertCount(5, $this->repo->findList(null, null, null, ['foo', 'bar'], ShortUrlsParams::TAGS_MODE_ANY));
self::assertCount(1, $this->repo->findList(null, null, null, ['foo', 'bar'], ShortUrlsParams::TAGS_MODE_ALL));
self::assertEquals(5, $this->repo->countList(null, ['foo', 'bar']));
self::assertEquals(5, $this->repo->countList(null, ['foo', 'bar'], ShortUrlsParams::TAGS_MODE_ANY));
self::assertEquals(1, $this->repo->countList(null, ['foo', 'bar'], ShortUrlsParams::TAGS_MODE_ALL));
self::assertCount(4, $this->repo->findList(null, null, null, ['bar', 'baz']));
self::assertCount(4, $this->repo->findList(null, null, null, ['bar', 'baz'], ShortUrlsParams::TAGS_MODE_ANY));
self::assertCount(2, $this->repo->findList(null, null, null, ['bar', 'baz'], ShortUrlsParams::TAGS_MODE_ALL));
self::assertEquals(4, $this->repo->countList(null, ['bar', 'baz']));
self::assertEquals(4, $this->repo->countList(null, ['bar', 'baz'], ShortUrlsParams::TAGS_MODE_ANY));
self::assertEquals(2, $this->repo->countList(null, ['bar', 'baz'], ShortUrlsParams::TAGS_MODE_ALL));
self::assertCount(5, $this->repo->findList(null, null, null, ['foo', 'bar', 'baz']));
self::assertCount(5, $this->repo->findList(
null,
null,
null,
['foo', 'bar', 'baz'],
ShortUrlsParams::TAGS_MODE_ANY,
));
self::assertCount(0, $this->repo->findList(
null,
null,
null,
['foo', 'bar', 'baz'],
ShortUrlsParams::TAGS_MODE_ALL,
));
self::assertEquals(5, $this->repo->countList(null, ['foo', 'bar', 'baz']));
self::assertEquals(5, $this->repo->countList(null, ['foo', 'bar', 'baz'], ShortUrlsParams::TAGS_MODE_ANY));
self::assertEquals(0, $this->repo->countList(null, ['foo', 'bar', 'baz'], ShortUrlsParams::TAGS_MODE_ALL));
}
/** @test */
public function shortCodeIsInUseLooksForShortUrlInProperSetOfTables(): void
{