diff --git a/module/Core/src/Visit/Paginator/Adapter/NonOrphanVisitsPaginatorAdapter.php b/module/Core/src/Visit/Paginator/Adapter/NonOrphanVisitsPaginatorAdapter.php new file mode 100644 index 00000000..ba5b6663 --- /dev/null +++ b/module/Core/src/Visit/Paginator/Adapter/NonOrphanVisitsPaginatorAdapter.php @@ -0,0 +1,42 @@ +repo->countNonOrphanVisits(new VisitsCountFiltering( + $this->params->getDateRange(), + $this->params->excludeBots(), + $this->apiKey, + )); + } + + public function getSlice(int $offset, int $length): iterable + { + return $this->repo->findNonOrphanVisits(new VisitsListFiltering( + $this->params->getDateRange(), + $this->params->excludeBots(), + $this->apiKey, + $length, + $offset, + )); + } +} diff --git a/module/Core/src/Visit/VisitsStatsHelper.php b/module/Core/src/Visit/VisitsStatsHelper.php index 25f5c82e..914a9c5b 100644 --- a/module/Core/src/Visit/VisitsStatsHelper.php +++ b/module/Core/src/Visit/VisitsStatsHelper.php @@ -19,6 +19,7 @@ use Shlinkio\Shlink\Core\Repository\TagRepository; use Shlinkio\Shlink\Core\Repository\VisitRepository; use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface; use Shlinkio\Shlink\Core\Visit\Model\VisitsStats; +use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\NonOrphanVisitsPaginatorAdapter; use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\OrphanVisitsPaginatorAdapter; use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\ShortUrlVisitsPaginatorAdapter; use Shlinkio\Shlink\Core\Visit\Paginator\Adapter\TagVisitsPaginatorAdapter; @@ -95,6 +96,14 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface return $this->createPaginator(new OrphanVisitsPaginatorAdapter($repo, $params), $params); } + public function nonOrphanVisits(VisitsParams $params, ?ApiKey $apiKey = null): Paginator + { + /** @var VisitRepositoryInterface $repo */ + $repo = $this->em->getRepository(Visit::class); + + return $this->createPaginator(new NonOrphanVisitsPaginatorAdapter($repo, $params, $apiKey), $params); + } + private function createPaginator(AdapterInterface $adapter, VisitsParams $params): Paginator { $paginator = new Paginator($adapter); diff --git a/module/Core/src/Visit/VisitsStatsHelperInterface.php b/module/Core/src/Visit/VisitsStatsHelperInterface.php index 5e15be4f..3616b531 100644 --- a/module/Core/src/Visit/VisitsStatsHelperInterface.php +++ b/module/Core/src/Visit/VisitsStatsHelperInterface.php @@ -37,4 +37,9 @@ interface VisitsStatsHelperInterface * @return Visit[]|Paginator */ public function orphanVisits(VisitsParams $params): Paginator; + + /** + * @return Visit[]|Paginator + */ + public function nonOrphanVisits(VisitsParams $params, ?ApiKey $apiKey = null): Paginator; } diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index e7d99a85..5f0d5c05 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -34,6 +34,7 @@ return [ Action\Visit\TagVisitsAction::class => ConfigAbstractFactory::class, Action\Visit\GlobalVisitsAction::class => ConfigAbstractFactory::class, Action\Visit\OrphanVisitsAction::class => ConfigAbstractFactory::class, + Action\Visit\NonOrphanVisitsAction::class => ConfigAbstractFactory::class, Action\Tag\ListTagsAction::class => ConfigAbstractFactory::class, Action\Tag\TagsStatsAction::class => ConfigAbstractFactory::class, Action\Tag\DeleteTagsAction::class => ConfigAbstractFactory::class, @@ -74,6 +75,7 @@ return [ Visit\VisitsStatsHelper::class, Visit\Transformer\OrphanVisitDataTransformer::class, ], + Action\Visit\NonOrphanVisitsAction::class => [Visit\VisitsStatsHelper::class], Action\ShortUrl\ListShortUrlsAction::class => [Service\ShortUrlService::class, ShortUrlDataTransformer::class], Action\Tag\ListTagsAction::class => [TagService::class], Action\Tag\TagsStatsAction::class => [TagService::class], diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index 49d9f107..16f83149 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -34,6 +34,7 @@ return [ Action\Visit\TagVisitsAction::getRouteDef(), Action\Visit\GlobalVisitsAction::getRouteDef(), Action\Visit\OrphanVisitsAction::getRouteDef(), + Action\Visit\NonOrphanVisitsAction::getRouteDef(), // Tags Action\Tag\ListTagsAction::getRouteDef(), diff --git a/module/Rest/src/Action/Visit/NonOrphanVisitsAction.php b/module/Rest/src/Action/Visit/NonOrphanVisitsAction.php new file mode 100644 index 00000000..7d77a5b1 --- /dev/null +++ b/module/Rest/src/Action/Visit/NonOrphanVisitsAction.php @@ -0,0 +1,37 @@ +getQueryParams()); + $apiKey = AuthenticationMiddleware::apiKeyFromRequest($request); + $visits = $this->visitsHelper->nonOrphanVisits($params, $apiKey); + + return new JsonResponse([ + 'visits' => $this->serializePaginator($visits), + ]); + } +} diff --git a/module/Rest/test-api/Action/NonOrphanVisitsTest.php b/module/Rest/test-api/Action/NonOrphanVisitsTest.php new file mode 100644 index 00000000..c53e29cc --- /dev/null +++ b/module/Rest/test-api/Action/NonOrphanVisitsTest.php @@ -0,0 +1,36 @@ +callApiWithKey(self::METHOD_GET, '/visits/non-orphan', [RequestOptions::QUERY => $query]); + $payload = $this->getJsonResponsePayload($resp); + + self::assertEquals($totalItems, $payload['visits']['pagination']['totalItems'] ?? Paginator::ALL_ITEMS); + self::assertCount($returnedItems, $payload['visits']['data'] ?? []); + } + + public function provideQueries(): iterable + { + yield 'all data' => [[], 7, 7]; + yield 'middle page' => [['page' => 2, 'itemsPerPage' => 3], 7, 3]; + yield 'last page' => [['page' => 3, 'itemsPerPage' => 3], 7, 1]; + yield 'bots excluded' => [['excludeBots' => 'true'], 6, 6]; + yield 'bots excluded and pagination' => [['excludeBots' => 'true', 'page' => 1, 'itemsPerPage' => 4], 6, 4]; + yield 'date filter' => [['startDate' => Chronos::now()->addDay()->toAtomString()], 0, 0]; + } +} diff --git a/module/Rest/test/Action/Visit/NonOrphanVisitsActionTest.php b/module/Rest/test/Action/Visit/NonOrphanVisitsActionTest.php new file mode 100644 index 00000000..5b3487f0 --- /dev/null +++ b/module/Rest/test/Action/Visit/NonOrphanVisitsActionTest.php @@ -0,0 +1,49 @@ +visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class); + $this->action = new NonOrphanVisitsAction($this->visitsHelper->reveal()); + } + + /** @test */ + public function requestIsHandled(): void + { + $apiKey = ApiKey::create(); + $getVisits = $this->visitsHelper->nonOrphanVisits(Argument::type(VisitsParams::class), $apiKey)->willReturn( + new Paginator(new ArrayAdapter([])), + ); + + /** @var JsonResponse $response */ + $response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute(ApiKey::class, $apiKey)); + $payload = $response->getPayload(); + + self::assertEquals(200, $response->getStatusCode()); + self::assertArrayHasKey('visits', $payload); + $getVisits->shouldHaveBeenCalledOnce(); + } +} diff --git a/module/Rest/test/Action/Visit/TagVisitsActionTest.php b/module/Rest/test/Action/Visit/TagVisitsActionTest.php index be3ce914..33907d09 100644 --- a/module/Rest/test/Action/Visit/TagVisitsActionTest.php +++ b/module/Rest/test/Action/Visit/TagVisitsActionTest.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Rest\Action\Visit; -use Laminas\Diactoros\ServerRequest; +use Laminas\Diactoros\ServerRequestFactory; use Pagerfanta\Adapter\ArrayAdapter; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -30,7 +30,7 @@ class TagVisitsActionTest extends TestCase } /** @test */ - public function providingCorrectShortCodeReturnsVisits(): void + public function providingCorrectTagReturnsVisits(): void { $tag = 'foo'; $apiKey = ApiKey::create(); @@ -39,7 +39,7 @@ class TagVisitsActionTest extends TestCase ); $response = $this->action->handle( - (new ServerRequest())->withAttribute('tag', $tag)->withAttribute(ApiKey::class, $apiKey), + ServerRequestFactory::fromGlobals()->withAttribute('tag', $tag)->withAttribute(ApiKey::class, $apiKey), ); self::assertEquals(200, $response->getStatusCode());