From b625a77783638fb984d508c25e7d2c872431786a Mon Sep 17 00:00:00 2001 From: Martin Chaine Date: Tue, 3 Jun 2025 09:46:55 +0200 Subject: [PATCH] add an API endpoint to expose EntryDeletion --- fixtures/EntryDeletionFixtures.php | 53 ++++++++++++ .../Api/EntryDeletionRestController.php | 80 +++++++++++++++++++ src/Repository/EntryDeletionRepository.php | 43 ++++++++++ src/Security/Voter/EntryDeletionVoter.php | 43 ++++++++++ .../Api/EntryDeletionRestControllerTest.php | 44 ++++++++++ 5 files changed, 263 insertions(+) create mode 100644 fixtures/EntryDeletionFixtures.php create mode 100644 src/Controller/Api/EntryDeletionRestController.php create mode 100644 src/Repository/EntryDeletionRepository.php create mode 100644 src/Security/Voter/EntryDeletionVoter.php create mode 100644 tests/Controller/Api/EntryDeletionRestControllerTest.php diff --git a/fixtures/EntryDeletionFixtures.php b/fixtures/EntryDeletionFixtures.php new file mode 100644 index 000000000..5547b8f29 --- /dev/null +++ b/fixtures/EntryDeletionFixtures.php @@ -0,0 +1,53 @@ +getReference('admin-user', User::class); + $bobUser = $this->getReference('bob-user', User::class); + + $deletions = [ + [ + 'user' => $adminUser, + 'entry_id' => 1004, + 'deleted_at' => new \DateTime('-4 day'), + ], + [ + 'user' => $adminUser, + 'entry_id' => 1001, + 'deleted_at' => new \DateTime('-1 day'), + ], + [ + 'user' => $bobUser, + 'entry_id' => 1003, + 'deleted_at' => new \DateTime('-3 days'), + ], + ]; + + foreach ($deletions as $deletionData) { + $deletion = new EntryDeletion($deletionData['user'], $deletionData['entry_id']); + $deletion->setDeletedAt($deletionData['deleted_at']); + + $manager->persist($deletion); + } + + $manager->flush(); + } + + public function getDependencies(): array + { + return [ + UserFixtures::class, + EntryFixtures::class, + ]; + } +} diff --git a/src/Controller/Api/EntryDeletionRestController.php b/src/Controller/Api/EntryDeletionRestController.php new file mode 100644 index 000000000..65f1593f6 --- /dev/null +++ b/src/Controller/Api/EntryDeletionRestController.php @@ -0,0 +1,80 @@ + 'json'])] + #[OA\Get( + summary: 'Retrieve all entry deletions for the current user.', + tags: ['EntryDeletions'], + parameters: [ + new OA\Parameter( + name: 'since', + in: 'query', + description: 'The timestamp (in seconds) since when you want entry deletions.', + required: false, + schema: new OA\Schema(type: 'integer') + ), + new WOA\OrderParameter(), + new WOA\PagerFanta\PageParameter(), + new WOA\PagerFanta\PerPageParameter(default: 100) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Returned when successful', + content: new WOA\PagerFanta\JsonContent(EntryDeletion::class) + ) + ] + )] + #[IsGranted('LIST_ENTRIES')] + public function getEntryDeletionsAction(Request $request, EntryDeletionRepository $entryDeletionRepository) + { + $this->validateAuthentication(); + $userId = $this->getUser()->getId(); + + $page = $request->query->get('page', 1); + $perPage = $request->query->get('perPage', 100); + $order = $request->query->get('order', 'desc'); + $since = $request->query->get('since'); + + if (!\in_array($order, ['asc', 'desc'], true)) { + $order = 'desc'; + } + + $pager = $entryDeletionRepository->findEntryDeletions($userId, $since, $order); + $pager->setMaxPerPage($perPage); + $pager->setCurrentPage($page); + + $pagerfantaFactory = new PagerfantaFactory('page', 'perPage'); + $paginatedCollection = $pagerfantaFactory->createRepresentation( + $pager, + new HateoasRoute( + 'api_get_entry_deletions', + [ + 'page' => $page, + 'perPage' => $perPage, + 'order' => $order, + 'since' => $since, + ], + true + ), + ); + + return $this->sendResponse($paginatedCollection); + } +} diff --git a/src/Repository/EntryDeletionRepository.php b/src/Repository/EntryDeletionRepository.php new file mode 100644 index 000000000..613de69d6 --- /dev/null +++ b/src/Repository/EntryDeletionRepository.php @@ -0,0 +1,43 @@ +createQueryBuilder('de') + ->where('de.user = :userId')->setParameter('userId', $userId) + ->orderBy('de.deletedAt', $order); + + if ($since > 0) { + $qb->andWhere('de.deletedAt >= :since') + ->setParameter('since', new \DateTime(date('Y-m-d H:i:s', $since))); + } + + $pagerAdapter = new DoctrineORMAdapter($qb, true, false); + $pager = new Pagerfanta($pagerAdapter); + + return $pager; + } +} diff --git a/src/Security/Voter/EntryDeletionVoter.php b/src/Security/Voter/EntryDeletionVoter.php new file mode 100644 index 000000000..becd90209 --- /dev/null +++ b/src/Security/Voter/EntryDeletionVoter.php @@ -0,0 +1,43 @@ +getUser(); + + if (!$user instanceof User) { + return false; + } + + return match ($attribute) { + self::VIEW, self::LIST => $user === $subject->getUser(), + default => false, + }; + } +} diff --git a/tests/Controller/Api/EntryDeletionRestControllerTest.php b/tests/Controller/Api/EntryDeletionRestControllerTest.php new file mode 100644 index 000000000..941711c3a --- /dev/null +++ b/tests/Controller/Api/EntryDeletionRestControllerTest.php @@ -0,0 +1,44 @@ +client->request('GET', '/api/entry-deletions'); + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + // check that only the items for the current user are returned + $this->assertEquals(2, \count($content['_embedded']['items'])); + + // validate the deletion schema on the first item + $deletionData = $content['_embedded']['items'][0]; + $this->assertArrayHasKey('id', $deletionData); + $this->assertArrayHasKey('entry_id', $deletionData); + $this->assertArrayHasKey('deleted_at', $deletionData); + $this->assertArrayNotHasKey('user_id', $deletionData); + } + + public function testGetEntryDeletionsSince() + { + // Test date range filter - get deletions from last 2 days + $since = (new \DateTime('-2 days'))->getTimestamp(); + $this->client->request('GET', "/api/entry-deletions?since={$since}"); + $this->assertSame(200, $this->client->getResponse()->getStatusCode()); + + $content = json_decode($this->client->getResponse()->getContent(), true); + $this->assertGreaterThanOrEqual(1, \count($content['_embedded']['items'])); + } +}