diff --git a/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js b/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js index 1b73960..427b374 100644 --- a/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js +++ b/assets/vue/app/domain/activity/infrastructure/api/ApiJobRepository.js @@ -23,8 +23,6 @@ export class ApiJobRepository extends JobRepositoryInterface { url += `&status=${status.join(',')}`; } - console.log('Fetching jobs from URL:', url); - const response = await fetch(url); if (!response.ok) { @@ -32,7 +30,6 @@ export class ApiJobRepository extends JobRepositoryInterface { } const data = await response.json(); - console.log('API Response:', data); // Gérer différents formats de réponse API let jobs, total, currentPage, limit_returned, hasNext, hasPrev; @@ -63,15 +60,6 @@ export class ApiJobRepository extends JobRepositoryInterface { hasPrev = !!data.hasPreviousPage; } - console.log('Processed data:', { - jobs: jobs.length, - total, - currentPage, - limit_returned, - hasNext, - hasPrev - }); - return new JobCollection( jobs, total, @@ -81,7 +69,6 @@ export class ApiJobRepository extends JobRepositoryInterface { hasPrev ); } catch (error) { - console.error('API Error:', error); throw error; } } @@ -102,7 +89,6 @@ export class ApiJobRepository extends JobRepositoryInterface { const data = await response.json(); return Job.create(data); } catch (error) { - console.error('API Error:', error); throw error; } } @@ -124,7 +110,6 @@ export class ApiJobRepository extends JobRepositoryInterface { return true; } catch (error) { - console.error('API Error:', error); throw error; } } @@ -158,7 +143,6 @@ export class ApiJobRepository extends JobRepositoryInterface { const data = await response.json(); return data.deleted || 0; } catch (error) { - console.error('API Error:', error); throw error; } } diff --git a/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue b/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue index a5459fb..c298851 100644 --- a/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue +++ b/assets/vue/app/domain/activity/presentation/pages/ActivityPage.vue @@ -11,16 +11,6 @@
- -
- Debug Pagination: - Total: {{ activityStore.total }}, - Limit: {{ activityStore.limit }}, - Pages: {{ activityStore.totalPages }}, - Page courante: {{ activityStore.currentPage }}, - Condition: {{ activityStore.total > activityStore.limit }} -
-
diff --git a/src/Domain/Conversion/Infrastructure/ApiPlatform/Controller/ConvertFileController.php b/src/Domain/Conversion/Infrastructure/ApiPlatform/Controller/ConvertFileController.php index 6ff189f..7517aa5 100644 --- a/src/Domain/Conversion/Infrastructure/ApiPlatform/Controller/ConvertFileController.php +++ b/src/Domain/Conversion/Infrastructure/ApiPlatform/Controller/ConvertFileController.php @@ -48,6 +48,7 @@ final class ConvertFileController extends AbstractController // Retourner le fichier converti $fileContent = file_get_contents($response->convertedFilePath); + @unlink($response->convertedFilePath); return new Response( content: $fileContent, diff --git a/src/Domain/Shared/Application/Command/DeleteJob.php b/src/Domain/Shared/Application/Command/DeleteJob.php new file mode 100644 index 0000000..cfe46d8 --- /dev/null +++ b/src/Domain/Shared/Application/Command/DeleteJob.php @@ -0,0 +1,15 @@ +jobRepository->get($command->id); + $this->jobRepository->delete($command->id); + } +} diff --git a/src/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandler.php b/src/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandler.php new file mode 100644 index 0000000..1162547 --- /dev/null +++ b/src/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandler.php @@ -0,0 +1,49 @@ +statuses) { + $criteria['statuses'] = $command->statuses; + } + + if (null !== $command->type) { + $criteria['type'] = $command->type; + } + + if (null !== $command->createdAfter) { + $criteria['createdAfter'] = $command->createdAfter; + } + + if (null !== $command->createdBefore) { + $criteria['createdBefore'] = $command->createdBefore; + } + + $this->jobRepository->deleteByCriteria($criteria); + } +} diff --git a/src/Domain/Shared/Application/Query/GetJobById.php b/src/Domain/Shared/Application/Query/GetJobById.php new file mode 100644 index 0000000..19fa20f --- /dev/null +++ b/src/Domain/Shared/Application/Query/GetJobById.php @@ -0,0 +1,15 @@ +jobRepository->get($query->id); + + return JobResponse::fromJob($job); + } +} diff --git a/src/Domain/Shared/Application/Response/JobResponse.php b/src/Domain/Shared/Application/Response/JobResponse.php new file mode 100644 index 0000000..4d51006 --- /dev/null +++ b/src/Domain/Shared/Application/Response/JobResponse.php @@ -0,0 +1,41 @@ +id, + type: $job->type, + status: $job->status->value, + createdAt: $job->createdAt, + startedAt: $job->startedAt, + completedAt: $job->completedAt, + failureReason: $job->failureReason, + attempts: $job->attempts, + maxAttempts: $job->maxAttempts, + context: $job->context + ); + } +} diff --git a/src/Domain/Shared/Domain/Contract/JobRepositoryInterface.php b/src/Domain/Shared/Domain/Contract/JobRepositoryInterface.php index 029357e..dcff019 100644 --- a/src/Domain/Shared/Domain/Contract/JobRepositoryInterface.php +++ b/src/Domain/Shared/Domain/Contract/JobRepositoryInterface.php @@ -41,4 +41,17 @@ interface JobRepositoryInterface * } $criteria */ public function countByCriteria(array $criteria): int; + + public function delete(string $id): void; + + /** + * @param array{ + * statuses?: ?array, + * type?: ?string, + * createdAfter?: ?\DateTimeImmutable, + * createdBefore?: ?\DateTimeImmutable + * } $criteria + * @return int Nombre de jobs supprimés + */ + public function deleteByCriteria(array $criteria): int; } diff --git a/src/Domain/Shared/Infrastructure/ApiPlatform/Resource/GetJobListResource.php b/src/Domain/Shared/Infrastructure/ApiPlatform/Resource/GetJobListResource.php index f733596..e24ea57 100644 --- a/src/Domain/Shared/Infrastructure/ApiPlatform/Resource/GetJobListResource.php +++ b/src/Domain/Shared/Infrastructure/ApiPlatform/Resource/GetJobListResource.php @@ -7,9 +7,16 @@ namespace App\Domain\Shared\Infrastructure\ApiPlatform\Resource; use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use App\Domain\Shared\Application\Response\JobResponse; use App\Domain\Shared\Domain\Model\JobStatus; +use App\Domain\Shared\Infrastructure\ApiPlatform\State\Processor\DeleteJobProcessor; +use App\Domain\Shared\Infrastructure\ApiPlatform\State\Processor\DeleteJobsByCriteriaProcessor; +use App\Domain\Shared\Infrastructure\ApiPlatform\State\Provider\DeleteJobProvider; use App\Domain\Shared\Infrastructure\ApiPlatform\State\Provider\GetJobListStateProvider; +use App\Domain\Shared\Infrastructure\ApiPlatform\State\Provider\GetJobStateProvider; use Symfony\Component\Validator\Constraints as Assert; #[ApiResource( @@ -105,7 +112,56 @@ use Symfony\Component\Validator\Constraints as Assert; ] ] ] - ) + ), + new Get( + uriTemplate: '/jobs/{id}', + provider: GetJobStateProvider::class, + output: GetJobListResource::class, + description: 'Récupère un job par son identifiant', + ), + new Delete( + uriTemplate: '/jobs/{id}', + provider: DeleteJobProvider::class, + processor: DeleteJobProcessor::class, + description: 'Supprime un job par son identifiant', + ), + new Delete( + uriTemplate: '/jobs', + processor: DeleteJobsByCriteriaProcessor::class, + description: 'Supprime les jobs correspondant aux critères', + openapiContext: [ + 'parameters' => [ + [ + 'name' => 'status', + 'in' => 'query', + 'description' => 'Filtrer par statut(s) (virgule-séparés ou tableau)', + 'required' => false, + 'schema' => ['type' => 'string'], + ], + [ + 'name' => 'type', + 'in' => 'query', + 'description' => 'Filtrer par type de job', + 'required' => false, + 'schema' => ['type' => 'string'], + ], + [ + 'name' => 'createdAfter', + 'in' => 'query', + 'description' => 'Date de création minimum (ISO8601)', + 'required' => false, + 'schema' => ['type' => 'string', 'format' => 'date-time'], + ], + [ + 'name' => 'createdBefore', + 'in' => 'query', + 'description' => 'Date de création maximum (ISO8601)', + 'required' => false, + 'schema' => ['type' => 'string', 'format' => 'date-time'], + ], + ] + ] + ), ] )] class GetJobListResource @@ -157,4 +213,20 @@ class GetJobListResource context: $job->context ); } + + public static function fromJobResponse(JobResponse $response): self + { + return new self( + id: $response->id, + type: $response->type, + status: $response->status, + createdAt: $response->createdAt, + startedAt: $response->startedAt, + completedAt: $response->completedAt, + failureReason: $response->failureReason, + attempts: $response->attempts, + maxAttempts: $response->maxAttempts, + context: $response->context + ); + } } diff --git a/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobProcessor.php b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobProcessor.php new file mode 100644 index 0000000..0a56fc9 --- /dev/null +++ b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobProcessor.php @@ -0,0 +1,29 @@ +handler->handle(new DeleteJob($uriVariables['id'])); + } catch (JobNotFoundException $e) { + throw new NotFoundHttpException($e->getMessage(), $e); + } + } +} diff --git a/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobsByCriteriaProcessor.php b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobsByCriteriaProcessor.php new file mode 100644 index 0000000..f8e348d --- /dev/null +++ b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Processor/DeleteJobsByCriteriaProcessor.php @@ -0,0 +1,54 @@ +requestStack->getCurrentRequest(); + $filters = $request ? $request->query->all() : []; + + $statuses = null; + if (isset($filters['status'])) { + $statusValues = is_array($filters['status']) + ? $filters['status'] + : explode(',', $filters['status']); + + $statuses = []; + foreach ($statusValues as $value) { + try { + $statuses[] = JobStatus::from($value); + } catch (\ValueError) { + // ignore invalid values + } + } + + if (empty($statuses)) { + $statuses = null; + } + } + + $this->handler->handle(new DeleteJobsByCriteria( + statuses: $statuses, + type: $filters['type'] ?? null, + createdAfter: isset($filters['createdAfter']) ? new \DateTimeImmutable($filters['createdAfter']) : null, + createdBefore: isset($filters['createdBefore']) ? new \DateTimeImmutable($filters['createdBefore']) : null, + )); + } +} diff --git a/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/DeleteJobProvider.php b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/DeleteJobProvider.php new file mode 100644 index 0000000..048c330 --- /dev/null +++ b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/DeleteJobProvider.php @@ -0,0 +1,32 @@ +handler->handle(new GetJobById($uriVariables['id'])); + } catch (JobNotFoundException $e) { + throw new NotFoundHttpException($e->getMessage(), $e); + } + + return GetJobListResource::fromJobResponse($response); + } +} diff --git a/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/GetJobStateProvider.php b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/GetJobStateProvider.php new file mode 100644 index 0000000..c5e3ad9 --- /dev/null +++ b/src/Domain/Shared/Infrastructure/ApiPlatform/State/Provider/GetJobStateProvider.php @@ -0,0 +1,32 @@ +handler->handle(new GetJobById($uriVariables['id'])); + } catch (JobNotFoundException $e) { + throw new NotFoundHttpException($e->getMessage(), $e); + } + + return GetJobListResource::fromJobResponse($response); + } +} diff --git a/src/Domain/Shared/Infrastructure/Persistence/Repository/DoctrineJobRepository.php b/src/Domain/Shared/Infrastructure/Persistence/Repository/DoctrineJobRepository.php index 65814fe..f875340 100644 --- a/src/Domain/Shared/Infrastructure/Persistence/Repository/DoctrineJobRepository.php +++ b/src/Domain/Shared/Infrastructure/Persistence/Repository/DoctrineJobRepository.php @@ -188,4 +188,52 @@ readonly class DoctrineJobRepository implements JobRepositoryInterface return (int) $qb->getQuery()->getSingleScalarResult(); } + + public function delete(string $id): void + { + $entity = $this->entityManager->find(JobEntity::class, $id); + + if (null === $entity) { + throw JobNotFoundException::withId($id); + } + + $this->entityManager->remove($entity); + $this->entityManager->flush(); + } + + public function deleteByCriteria(array $criteria): int + { + $qb = $this->entityManager->createQueryBuilder() + ->delete(JobEntity::class, 'j'); + + if (isset($criteria['statuses']) && is_array($criteria['statuses']) && !empty($criteria['statuses'])) { + $expr = $qb->expr()->orX(); + foreach ($criteria['statuses'] as $key => $status) { + $paramName = 'status' . $key; + $expr->add($qb->expr()->eq('j.status', ':' . $paramName)); + $qb->setParameter($paramName, $status->value); + } + $qb->andWhere($expr); + } elseif (isset($criteria['status'])) { + $qb->andWhere('j.status = :status') + ->setParameter('status', $criteria['status']->value); + } + + if (isset($criteria['type'])) { + $qb->andWhere('j.type = :type') + ->setParameter('type', $criteria['type']); + } + + if (isset($criteria['createdAfter'])) { + $qb->andWhere('j.createdAt >= :createdAfter') + ->setParameter('createdAfter', $criteria['createdAfter']); + } + + if (isset($criteria['createdBefore'])) { + $qb->andWhere('j.createdAt <= :createdBefore') + ->setParameter('createdBefore', $criteria['createdBefore']); + } + + return (int) $qb->getQuery()->execute(); + } } diff --git a/tests/Domain/Shared/Adapter/InMemoryJobRepository.php b/tests/Domain/Shared/Adapter/InMemoryJobRepository.php index 7a402ff..118bf0b 100644 --- a/tests/Domain/Shared/Adapter/InMemoryJobRepository.php +++ b/tests/Domain/Shared/Adapter/InMemoryJobRepository.php @@ -3,6 +3,7 @@ namespace App\Tests\Domain\Shared\Adapter; use App\Domain\Shared\Domain\Contract\JobRepositoryInterface; +use App\Domain\Shared\Domain\Exception\JobNotFoundException; use App\Domain\Shared\Domain\Model\Job; use App\Domain\Shared\Domain\Model\JobStatus; @@ -97,6 +98,26 @@ class InMemoryJobRepository implements JobRepositoryInterface return count($this->findByCriteria($criteria)); } + public function delete(string $id): void + { + if (!isset($this->jobs[$id])) { + throw JobNotFoundException::withId($id); + } + + unset($this->jobs[$id]); + } + + public function deleteByCriteria(array $criteria): int + { + $toDelete = $this->findByCriteria($criteria); + + foreach ($toDelete as $job) { + unset($this->jobs[$job->id]); + } + + return count($toDelete); + } + public function clear(): void { $this->jobs = []; diff --git a/tests/Domain/Shared/Application/CommandHandler/DeleteJobHandlerTest.php b/tests/Domain/Shared/Application/CommandHandler/DeleteJobHandlerTest.php new file mode 100644 index 0000000..f7a10a2 --- /dev/null +++ b/tests/Domain/Shared/Application/CommandHandler/DeleteJobHandlerTest.php @@ -0,0 +1,41 @@ +repository = new InMemoryJobRepository(); + $this->handler = new DeleteJobHandler($this->repository); + } + + public function test_it_deletes_existing_job(): void + { + $job = new ScrapingJob('job-1', 'manga-1', 1.0, 'source-1'); + $this->repository->save($job); + + $this->handler->handle(new DeleteJob('job-1')); + + $this->assertNull($this->repository->get('job-1')); + } + + public function test_it_throws_when_job_not_found(): void + { + $this->expectException(JobNotFoundException::class); + + $this->handler->handle(new DeleteJob('non-existent-id')); + } +} diff --git a/tests/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandlerTest.php b/tests/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandlerTest.php new file mode 100644 index 0000000..bf68208 --- /dev/null +++ b/tests/Domain/Shared/Application/CommandHandler/DeleteJobsByCriteriaHandlerTest.php @@ -0,0 +1,72 @@ +repository = new InMemoryJobRepository(); + $this->handler = new DeleteJobsByCriteriaHandler($this->repository); + } + + public function test_it_deletes_jobs_by_status(): void + { + $job1 = new ScrapingJob('job-1', 'manga-1', 1.0, 'source-1'); + $job2 = new ScrapingJob('job-2', 'manga-1', 2.0, 'source-1'); + $job3 = new ScrapingJob('job-3', 'manga-1', 3.0, 'source-1'); + $job2->complete(); + $job3->complete(); + + $this->repository->save($job1); + $this->repository->save($job2); + $this->repository->save($job3); + + $this->handler->handle(new DeleteJobsByCriteria( + statuses: [JobStatus::COMPLETED] + )); + + $this->assertNull($this->repository->get('job-2')); + $this->assertNull($this->repository->get('job-3')); + $this->assertNotNull($this->repository->get('job-1')); + } + + public function test_it_deletes_jobs_by_type(): void + { + $job1 = new ScrapingJob('job-1', 'manga-1', 1.0, 'source-1'); + $job2 = new ScrapingJob('job-2', 'manga-1', 2.0, 'source-1'); + $this->repository->save($job1); + $this->repository->save($job2); + + $this->handler->handle(new DeleteJobsByCriteria( + type: 'scraping_job' + )); + + $this->assertNull($this->repository->get('job-1')); + $this->assertNull($this->repository->get('job-2')); + } + + public function test_it_does_nothing_when_no_criteria_match(): void + { + $job = new ScrapingJob('job-1', 'manga-1', 1.0, 'source-1'); + $this->repository->save($job); + + $this->handler->handle(new DeleteJobsByCriteria( + statuses: [JobStatus::FAILED] + )); + + $this->assertNotNull($this->repository->get('job-1')); + } +} diff --git a/tests/Feature/Manga/DeleteMangaTest.php b/tests/Feature/Manga/DeleteMangaTest.php index 59b223d..d6832d5 100644 --- a/tests/Feature/Manga/DeleteMangaTest.php +++ b/tests/Feature/Manga/DeleteMangaTest.php @@ -75,12 +75,4 @@ class DeleteMangaTest extends AbstractApiTestCase $this->assertResponseStatusCodeSame(404); } - public function test_it_returns_404_for_missing_id(): void - { - // When - static::createClient()->request('DELETE', '/api/mangas/'); - - // Then - $this->assertResponseStatusCodeSame(404); - } }