Applied API role specs to global visits

This commit is contained in:
Alejandro Celaya 2021-01-04 11:27:55 +01:00
parent 8aa6bdb934
commit 68c601a5a8
9 changed files with 34 additions and 12 deletions

View File

@ -56,7 +56,7 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
{ {
$result = (int) $this->matchSingleScalarResult(Spec::andX( $result = (int) $this->matchSingleScalarResult(Spec::andX(
new CountTagsWithName($tag), new CountTagsWithName($tag),
new WithApiKeySpecsEnsuringJoin($apiKey), new WithApiKeySpecsEnsuringJoin($apiKey, 'shortUrls'),
)); ));
return $result > 0; return $result > 0;

View File

@ -7,11 +7,14 @@ namespace Shlinkio\Shlink\Core\Repository;
use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Happyr\DoctrineSpecification\EntitySpecificationRepository; use Happyr\DoctrineSpecification\EntitySpecificationRepository;
use Happyr\DoctrineSpecification\Spec;
use Happyr\DoctrineSpecification\Specification\Specification; 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\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Entity\VisitLocation; use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Rest\ApiKey\Spec\WithApiKeySpecsEnsuringJoin;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use const PHP_INT_MAX; use const PHP_INT_MAX;
@ -205,4 +208,11 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
return $query->getResult(); return $query->getResult();
} }
public function countVisits(?ApiKey $apiKey = null): int
{
return (int) $this->matchSingleScalarResult(
Spec::countOf(new WithApiKeySpecsEnsuringJoin($apiKey, 'shortUrl')),
);
}
} }

View File

@ -9,6 +9,7 @@ use Happyr\DoctrineSpecification\EntitySpecificationRepositoryInterface;
use Happyr\DoctrineSpecification\Specification\Specification; use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
interface VisitRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface interface VisitRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
{ {
@ -60,4 +61,6 @@ interface VisitRepositoryInterface extends ObjectRepository, EntitySpecification
): array; ): array;
public function countVisitsByTag(string $tag, ?DateRange $dateRange = null, ?Specification $spec = null): int; public function countVisitsByTag(string $tag, ?DateRange $dateRange = null, ?Specification $spec = null): int;
public function countVisits(?ApiKey $apiKey = null): int;
} }

View File

@ -8,6 +8,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Repository\VisitRepository; use Shlinkio\Shlink\Core\Repository\VisitRepository;
use Shlinkio\Shlink\Core\Visit\Model\VisitsStats; use Shlinkio\Shlink\Core\Visit\Model\VisitsStats;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsStatsHelper implements VisitsStatsHelperInterface class VisitsStatsHelper implements VisitsStatsHelperInterface
{ {
@ -18,15 +19,15 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
$this->em = $em; $this->em = $em;
} }
public function getVisitsStats(): VisitsStats public function getVisitsStats(?ApiKey $apiKey = null): VisitsStats
{ {
return new VisitsStats($this->getVisitsCount()); return new VisitsStats($this->getVisitsCount($apiKey));
} }
private function getVisitsCount(): int private function getVisitsCount(?ApiKey $apiKey): int
{ {
/** @var VisitRepository $visitsRepo */ /** @var VisitRepository $visitsRepo */
$visitsRepo = $this->em->getRepository(Visit::class); $visitsRepo = $this->em->getRepository(Visit::class);
return $visitsRepo->count([]); return $visitsRepo->countVisits($apiKey);
} }
} }

View File

@ -5,8 +5,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit; namespace Shlinkio\Shlink\Core\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsStats; use Shlinkio\Shlink\Core\Visit\Model\VisitsStats;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
interface VisitsStatsHelperInterface interface VisitsStatsHelperInterface
{ {
public function getVisitsStats(): VisitsStats; public function getVisitsStats(?ApiKey $apiKey = null): VisitsStats;
} }

View File

@ -36,7 +36,7 @@ class VisitsStatsHelperTest extends TestCase
public function returnsExpectedVisitsStats(int $expectedCount): void public function returnsExpectedVisitsStats(int $expectedCount): void
{ {
$repo = $this->prophesize(VisitRepository::class); $repo = $this->prophesize(VisitRepository::class);
$count = $repo->count([])->willReturn($expectedCount); $count = $repo->countVisits(null)->willReturn($expectedCount);
$getRepo = $this->em->getRepository(Visit::class)->willReturn($repo->reveal()); $getRepo = $this->em->getRepository(Visit::class)->willReturn($repo->reveal());
$stats = $this->helper->getVisitsStats(); $stats = $this->helper->getVisitsStats();

View File

@ -9,6 +9,7 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware;
class GlobalVisitsAction extends AbstractRestAction class GlobalVisitsAction extends AbstractRestAction
{ {
@ -24,8 +25,10 @@ class GlobalVisitsAction extends AbstractRestAction
public function handle(ServerRequestInterface $request): ResponseInterface public function handle(ServerRequestInterface $request): ResponseInterface
{ {
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
return new JsonResponse([ return new JsonResponse([
'visits' => $this->statsHelper->getVisitsStats(), 'visits' => $this->statsHelper->getVisitsStats($apiKey),
]); ]);
} }
} }

View File

@ -12,17 +12,19 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
class WithApiKeySpecsEnsuringJoin extends BaseSpecification class WithApiKeySpecsEnsuringJoin extends BaseSpecification
{ {
private ?ApiKey $apiKey; private ?ApiKey $apiKey;
private string $fieldToJoin;
public function __construct(?ApiKey $apiKey) public function __construct(?ApiKey $apiKey, string $fieldToJoin)
{ {
parent::__construct(); parent::__construct();
$this->apiKey = $apiKey; $this->apiKey = $apiKey;
$this->fieldToJoin = $fieldToJoin;
} }
protected function getSpec(): Specification protected function getSpec(): Specification
{ {
return $this->apiKey === null || $this->apiKey->isAdmin() ? Spec::andX() : Spec::andX( return $this->apiKey === null || $this->apiKey->isAdmin() ? Spec::andX() : Spec::andX(
Spec::join('shortUrls', 's'), Spec::join($this->fieldToJoin, 's'),
$this->apiKey->spec(), $this->apiKey->spec(),
); );
} }

View File

@ -12,6 +12,7 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Visit\Model\VisitsStats; use Shlinkio\Shlink\Core\Visit\Model\VisitsStats;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface; use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Shlinkio\Shlink\Rest\Action\Visit\GlobalVisitsAction; use Shlinkio\Shlink\Rest\Action\Visit\GlobalVisitsAction;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class GlobalVisitsActionTest extends TestCase class GlobalVisitsActionTest extends TestCase
{ {
@ -29,11 +30,12 @@ class GlobalVisitsActionTest extends TestCase
/** @test */ /** @test */
public function statsAreReturnedFromHelper(): void public function statsAreReturnedFromHelper(): void
{ {
$apiKey = new ApiKey();
$stats = new VisitsStats(5); $stats = new VisitsStats(5);
$getStats = $this->helper->getVisitsStats()->willReturn($stats); $getStats = $this->helper->getVisitsStats($apiKey)->willReturn($stats);
/** @var JsonResponse $resp */ /** @var JsonResponse $resp */
$resp = $this->action->handle(ServerRequestFactory::fromGlobals()); $resp = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute(ApiKey::class, $apiKey));
$payload = $resp->getPayload(); $payload = $resp->getPayload();
self::assertEquals($payload, ['visits' => $stats]); self::assertEquals($payload, ['visits' => $stats]);