Applied API role specs to short URLs list

This commit is contained in:
Alejandro Celaya 2021-01-03 13:05:21 +01:00
parent 6e1d6ab795
commit 940383646b
8 changed files with 57 additions and 22 deletions

View file

@ -7,16 +7,19 @@ namespace Shlinkio\Shlink\Core\Paginator\Adapter;
use Laminas\Paginator\Adapter\AdapterInterface; use Laminas\Paginator\Adapter\AdapterInterface;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class ShortUrlRepositoryAdapter implements AdapterInterface class ShortUrlRepositoryAdapter implements AdapterInterface
{ {
private ShortUrlRepositoryInterface $repository; private ShortUrlRepositoryInterface $repository;
private ShortUrlsParams $params; private ShortUrlsParams $params;
private ?ApiKey $apiKey;
public function __construct(ShortUrlRepositoryInterface $repository, ShortUrlsParams $params) public function __construct(ShortUrlRepositoryInterface $repository, ShortUrlsParams $params, ?ApiKey $apiKey)
{ {
$this->repository = $repository; $this->repository = $repository;
$this->params = $params; $this->params = $params;
$this->apiKey = $apiKey;
} }
public function getItems($offset, $itemCountPerPage): array // phpcs:ignore public function getItems($offset, $itemCountPerPage): array // phpcs:ignore
@ -28,6 +31,7 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
$this->params->tags(), $this->params->tags(),
$this->params->orderBy(), $this->params->orderBy(),
$this->params->dateRange(), $this->params->dateRange(),
$this->apiKey !== null ? $this->apiKey->spec() : null,
); );
} }
@ -37,6 +41,7 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
$this->params->searchTerm(), $this->params->searchTerm(),
$this->params->tags(), $this->params->tags(),
$this->params->dateRange(), $this->params->dateRange(),
$this->apiKey !== null ? $this->apiKey->spec() : null,
); );
} }
} }

View file

@ -4,9 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Repository; namespace Shlinkio\Shlink\Core\Repository;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Happyr\DoctrineSpecification\EntitySpecificationRepository;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType; use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
@ -19,7 +20,7 @@ use function array_key_exists;
use function count; use function count;
use function Functional\contains; use function Functional\contains;
class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface class ShortUrlRepository extends EntitySpecificationRepository implements ShortUrlRepositoryInterface
{ {
/** /**
* @param string[] $tags * @param string[] $tags
@ -31,9 +32,10 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
?string $searchTerm = null, ?string $searchTerm = null,
array $tags = [], array $tags = [],
?ShortUrlsOrdering $orderBy = null, ?ShortUrlsOrdering $orderBy = null,
?DateRange $dateRange = null ?DateRange $dateRange = null,
?Specification $spec = null
): array { ): array {
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange); $qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange, $spec);
$qb->select('DISTINCT s') $qb->select('DISTINCT s')
->setMaxResults($limit) ->setMaxResults($limit)
->setFirstResult($offset); ->setFirstResult($offset);
@ -75,9 +77,13 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
public function countList(?string $searchTerm = null, array $tags = [], ?DateRange $dateRange = null): int public function countList(
{ ?string $searchTerm = null,
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange); array $tags = [],
?DateRange $dateRange = null,
?Specification $spec = null
): int {
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange, $spec);
$qb->select('COUNT(DISTINCT s)'); $qb->select('COUNT(DISTINCT s)');
return (int) $qb->getQuery()->getSingleScalarResult(); return (int) $qb->getQuery()->getSingleScalarResult();
@ -86,7 +92,8 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
private function createListQueryBuilder( private function createListQueryBuilder(
?string $searchTerm = null, ?string $searchTerm = null,
array $tags = [], array $tags = [],
?DateRange $dateRange = null ?DateRange $dateRange = null,
?Specification $spec = null
): QueryBuilder { ): QueryBuilder {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(ShortUrl::class, 's') $qb->from(ShortUrl::class, 's')
@ -125,6 +132,10 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
->andWhere($qb->expr()->in('t.name', $tags)); ->andWhere($qb->expr()->in('t.name', $tags));
} }
if ($spec) {
$this->applySpecification($qb, $spec, 's');
}
return $qb; return $qb;
} }

View file

@ -5,13 +5,15 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Repository; namespace Shlinkio\Shlink\Core\Repository;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
use Happyr\DoctrineSpecification\EntitySpecificationRepositoryInterface;
use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering; use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl; use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
interface ShortUrlRepositoryInterface extends ObjectRepository interface ShortUrlRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
{ {
public function findList( public function findList(
?int $limit = null, ?int $limit = null,
@ -19,10 +21,16 @@ interface ShortUrlRepositoryInterface extends ObjectRepository
?string $searchTerm = null, ?string $searchTerm = null,
array $tags = [], array $tags = [],
?ShortUrlsOrdering $orderBy = null, ?ShortUrlsOrdering $orderBy = null,
?DateRange $dateRange = null ?DateRange $dateRange = null,
?Specification $spec = null
): array; ): array;
public function countList(?string $searchTerm = null, array $tags = [], ?DateRange $dateRange = null): int; public function countList(
?string $searchTerm = null,
array $tags = [],
?DateRange $dateRange = null,
?Specification $spec = null
): int;
public function findOneWithDomainFallback(string $shortCode, ?string $domain = null): ?ShortUrl; public function findOneWithDomainFallback(string $shortCode, ?string $domain = null): ?ShortUrl;

View file

@ -17,6 +17,7 @@ use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use Shlinkio\Shlink\Core\Util\TagManagerTrait; use Shlinkio\Shlink\Core\Util\TagManagerTrait;
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface; use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class ShortUrlService implements ShortUrlServiceInterface class ShortUrlService implements ShortUrlServiceInterface
{ {
@ -39,11 +40,11 @@ class ShortUrlService implements ShortUrlServiceInterface
/** /**
* @return ShortUrl[]|Paginator * @return ShortUrl[]|Paginator
*/ */
public function listShortUrls(ShortUrlsParams $params): Paginator public function listShortUrls(ShortUrlsParams $params, ?ApiKey $apiKey = null): Paginator
{ {
/** @var ShortUrlRepository $repo */ /** @var ShortUrlRepository $repo */
$repo = $this->em->getRepository(ShortUrl::class); $repo = $this->em->getRepository(ShortUrl::class);
$paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $params)); $paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $params, $apiKey));
$paginator->setItemCountPerPage($params->itemsPerPage()) $paginator->setItemCountPerPage($params->itemsPerPage())
->setCurrentPageNumber($params->page()); ->setCurrentPageNumber($params->page());

View file

@ -11,13 +11,14 @@ use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlEdit; use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
interface ShortUrlServiceInterface interface ShortUrlServiceInterface
{ {
/** /**
* @return ShortUrl[]|Paginator * @return ShortUrl[]|Paginator
*/ */
public function listShortUrls(ShortUrlsParams $params): Paginator; public function listShortUrls(ShortUrlsParams $params, ?ApiKey $apiKey = null): Paginator;
/** /**
* @param string[] $tags * @param string[] $tags

View file

@ -11,6 +11,7 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter; use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class ShortUrlRepositoryAdapterTest extends TestCase class ShortUrlRepositoryAdapterTest extends TestCase
{ {
@ -41,11 +42,11 @@ class ShortUrlRepositoryAdapterTest extends TestCase
'endDate' => $endDate, 'endDate' => $endDate,
'orderBy' => $orderBy, 'orderBy' => $orderBy,
]); ]);
$adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), $params); $adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), $params, null);
$orderBy = $params->orderBy(); $orderBy = $params->orderBy();
$dateRange = $params->dateRange(); $dateRange = $params->dateRange();
$this->repo->findList(10, 5, $searchTerm, $tags, $orderBy, $dateRange)->shouldBeCalledOnce(); $this->repo->findList(10, 5, $searchTerm, $tags, $orderBy, $dateRange, null)->shouldBeCalledOnce();
$adapter->getItems(5, 10); $adapter->getItems(5, 10);
} }
@ -65,10 +66,11 @@ class ShortUrlRepositoryAdapterTest extends TestCase
'startDate' => $startDate, 'startDate' => $startDate,
'endDate' => $endDate, 'endDate' => $endDate,
]); ]);
$adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), $params); $apiKey = new ApiKey();
$adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), $params, $apiKey);
$dateRange = $params->dateRange(); $dateRange = $params->dateRange();
$this->repo->countList($searchTerm, $tags, $dateRange)->shouldBeCalledOnce(); $this->repo->countList($searchTerm, $tags, $dateRange, $apiKey->spec())->shouldBeCalledOnce();
$adapter->count(); $adapter->count();
} }

View file

@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
class ListShortUrlsAction extends AbstractRestAction class ListShortUrlsAction extends AbstractRestAction
{ {
@ -31,7 +32,10 @@ class ListShortUrlsAction extends AbstractRestAction
public function handle(Request $request): Response public function handle(Request $request): Response
{ {
$shortUrls = $this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData($request->getQueryParams())); $shortUrls = $this->shortUrlService->listShortUrls(
ShortUrlsParams::fromRawData($request->getQueryParams()),
AuthenticationMiddleware::apiKeyFromRequest($request),
);
return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls, new ShortUrlDataTransformer( return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls, new ShortUrlDataTransformer(
$this->domainConfig, $this->domainConfig,
))]); ))]);

View file

@ -15,6 +15,7 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Service\ShortUrlService; use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Rest\Action\ShortUrl\ListShortUrlsAction; use Shlinkio\Shlink\Rest\Action\ShortUrl\ListShortUrlsAction;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class ListShortUrlsActionTest extends TestCase class ListShortUrlsActionTest extends TestCase
{ {
@ -46,6 +47,8 @@ class ListShortUrlsActionTest extends TestCase
?string $startDate = null, ?string $startDate = null,
?string $endDate = null ?string $endDate = null
): void { ): void {
$apiKey = new ApiKey();
$request = (new ServerRequest())->withQueryParams($query)->withAttribute(ApiKey::class, $apiKey);
$listShortUrls = $this->service->listShortUrls(ShortUrlsParams::fromRawData([ $listShortUrls = $this->service->listShortUrls(ShortUrlsParams::fromRawData([
'page' => $expectedPage, 'page' => $expectedPage,
'searchTerm' => $expectedSearchTerm, 'searchTerm' => $expectedSearchTerm,
@ -53,10 +56,10 @@ class ListShortUrlsActionTest extends TestCase
'orderBy' => $expectedOrderBy, 'orderBy' => $expectedOrderBy,
'startDate' => $startDate, 'startDate' => $startDate,
'endDate' => $endDate, 'endDate' => $endDate,
]))->willReturn(new Paginator(new ArrayAdapter())); ]), $apiKey)->willReturn(new Paginator(new ArrayAdapter()));
/** @var JsonResponse $response */ /** @var JsonResponse $response */
$response = $this->action->handle((new ServerRequest())->withQueryParams($query)); $response = $this->action->handle($request);
$payload = $response->getPayload(); $payload = $response->getPayload();
self::assertArrayHasKey('shortUrls', $payload); self::assertArrayHasKey('shortUrls', $payload);