Merge pull request 'feat: activity page' (#5) from feature/activity-ddd-port into main
All checks were successful
Deploy / deploy (push) Successful in 2m48s
All checks were successful
Deploy / deploy (push) Successful in 2m48s
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -7,8 +7,12 @@ export class Job {
|
|||||||
payload = {},
|
payload = {},
|
||||||
result = null,
|
result = null,
|
||||||
error = null,
|
error = null,
|
||||||
|
failureReason = null,
|
||||||
createdAt = new Date().toISOString(),
|
createdAt = new Date().toISOString(),
|
||||||
updatedAt = new Date().toISOString()
|
updatedAt = new Date().toISOString(),
|
||||||
|
attempts = 0,
|
||||||
|
maxAttempts = 1,
|
||||||
|
context = {}
|
||||||
}) {
|
}) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
@@ -16,9 +20,12 @@ export class Job {
|
|||||||
this.progress = progress;
|
this.progress = progress;
|
||||||
this.payload = payload;
|
this.payload = payload;
|
||||||
this.result = result;
|
this.result = result;
|
||||||
this.error = error;
|
this.error = failureReason ?? error;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
|
this.attempts = attempts;
|
||||||
|
this.maxAttempts = maxAttempts;
|
||||||
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
static create(data) {
|
static create(data) {
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
|
|||||||
url += `&status=${status.join(',')}`;
|
url += `&status=${status.join(',')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Fetching jobs from URL:', url);
|
|
||||||
|
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -32,7 +30,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('API Response:', data);
|
|
||||||
|
|
||||||
// Gérer différents formats de réponse API
|
// Gérer différents formats de réponse API
|
||||||
let jobs, total, currentPage, limit_returned, hasNext, hasPrev;
|
let jobs, total, currentPage, limit_returned, hasNext, hasPrev;
|
||||||
@@ -63,15 +60,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
|
|||||||
hasPrev = !!data.hasPreviousPage;
|
hasPrev = !!data.hasPreviousPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Processed data:', {
|
|
||||||
jobs: jobs.length,
|
|
||||||
total,
|
|
||||||
currentPage,
|
|
||||||
limit_returned,
|
|
||||||
hasNext,
|
|
||||||
hasPrev
|
|
||||||
});
|
|
||||||
|
|
||||||
return new JobCollection(
|
return new JobCollection(
|
||||||
jobs,
|
jobs,
|
||||||
total,
|
total,
|
||||||
@@ -81,7 +69,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
|
|||||||
hasPrev
|
hasPrev
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('API Error:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,7 +89,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return Job.create(data);
|
return Job.create(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('API Error:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +110,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('API Error:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +143,6 @@ export class ApiJobRepository extends JobRepositoryInterface {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.deleted || 0;
|
return data.deleted || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('API Error:', error);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,12 @@
|
|||||||
<td class="py-4 px-4 text-center">
|
<td class="py-4 px-4 text-center">
|
||||||
<input type="checkbox" class="form-checkbox h-5 w-5 text-green-600" />
|
<input type="checkbox" class="form-checkbox h-5 w-5 text-green-600" />
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-4 font-medium">{{ job.type }}</td>
|
<td class="py-4 px-4 font-medium">
|
||||||
|
<div>{{ jobTypeLabel }}</div>
|
||||||
|
<div v-if="job.context?.mangaTitle" class="text-xs text-gray-500 mt-0.5">
|
||||||
|
{{ job.context.mangaTitle }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td class="py-4 px-4">
|
<td class="py-4 px-4">
|
||||||
<span
|
<span
|
||||||
class="px-2 py-1 text-xs rounded-full"
|
class="px-2 py-1 text-xs rounded-full"
|
||||||
@@ -27,6 +32,18 @@
|
|||||||
<div v-if="job.error" class="text-sm text-red-600">
|
<div v-if="job.error" class="text-sm text-red-600">
|
||||||
{{ job.error }}
|
{{ job.error }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="job.context?.mangaTitle || job.context?.chapterNumber !== undefined || job.context?.sourceId"
|
||||||
|
class="text-sm text-gray-700 space-y-0.5">
|
||||||
|
<div v-if="job.context.mangaTitle" class="font-medium">
|
||||||
|
{{ job.context.mangaTitle }}
|
||||||
|
</div>
|
||||||
|
<div v-if="job.context.chapterNumber !== undefined" class="text-gray-500">
|
||||||
|
Chapitre {{ job.context.chapterNumber }}
|
||||||
|
</div>
|
||||||
|
<div v-if="job.context.sourceId" class="text-xs text-gray-400">
|
||||||
|
Source : {{ job.context.sourceId }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else class="text-sm text-gray-600">
|
<div v-else class="text-sm text-gray-600">
|
||||||
{{ formatDate(job.createdAt) }}
|
{{ formatDate(job.createdAt) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -66,6 +83,11 @@
|
|||||||
En attente
|
En attente
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="job.maxAttempts > 1 || job.attempts > 0"
|
||||||
|
class="text-xs text-gray-400 mt-1 text-center">
|
||||||
|
{{ job.attempts }} / {{ job.maxAttempts }} tentative{{ job.maxAttempts > 1 ? 's' : '' }}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-4">
|
<td class="py-4 px-4">
|
||||||
<button
|
<button
|
||||||
@@ -79,24 +101,33 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { TrashIcon } from '@heroicons/vue/24/outline';
|
import { TrashIcon } from '@heroicons/vue/24/outline';
|
||||||
import { defineEmits, defineProps } from 'vue';
|
import { computed, defineEmits, defineProps } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
job: {
|
job: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(['delete']);
|
|
||||||
|
|
||||||
function formatDate(dateString) {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleString();
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function onDelete() {
|
const emit = defineEmits(['delete']);
|
||||||
emit('delete', props.job.id);
|
|
||||||
}
|
const JOB_TYPE_LABELS = {
|
||||||
|
scraping_job: 'Scraping',
|
||||||
|
conversion_job: 'Conversion',
|
||||||
|
};
|
||||||
|
|
||||||
|
const jobTypeLabel = computed(() =>
|
||||||
|
JOB_TYPE_LABELS[props.job.type] ?? props.job.type
|
||||||
|
);
|
||||||
|
|
||||||
|
function formatDate(dateString) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDelete() {
|
||||||
|
emit('delete', props.job.id);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,16 +11,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="container mx-auto p-2">
|
<div v-else class="container mx-auto p-2">
|
||||||
<!-- Debug pagination - À supprimer plus tard -->
|
|
||||||
<div class="bg-yellow-100 border border-yellow-400 text-yellow-700 px-4 py-3 rounded mb-4" v-if="true">
|
|
||||||
<strong>Debug Pagination:</strong>
|
|
||||||
Total: {{ activityStore.total }},
|
|
||||||
Limit: {{ activityStore.limit }},
|
|
||||||
Pages: {{ activityStore.totalPages }},
|
|
||||||
Page courante: {{ activityStore.currentPage }},
|
|
||||||
Condition: {{ activityStore.total > activityStore.limit }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full bg-white">
|
<table class="min-w-full bg-white">
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ final class ConvertFileController extends AbstractController
|
|||||||
|
|
||||||
// Retourner le fichier converti
|
// Retourner le fichier converti
|
||||||
$fileContent = file_get_contents($response->convertedFilePath);
|
$fileContent = file_get_contents($response->convertedFilePath);
|
||||||
|
@unlink($response->convertedFilePath);
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
content: $fileContent,
|
content: $fileContent,
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ readonly class ScrapeChapterHandler
|
|||||||
// Ajout de l'ID du chapitre et du slug dans le contexte du job
|
// Ajout de l'ID du chapitre et du slug dans le contexte du job
|
||||||
$job->context['chapterId'] = $command->chapterId;
|
$job->context['chapterId'] = $command->chapterId;
|
||||||
$job->context['slug'] = $slug;
|
$job->context['slug'] = $slug;
|
||||||
|
$job->context['mangaTitle'] = $manga->getTitle();
|
||||||
|
|
||||||
$job->start();
|
$job->start();
|
||||||
$this->jobRepository->save($job);
|
$this->jobRepository->save($job);
|
||||||
|
|||||||
15
src/Domain/Shared/Application/Command/DeleteJob.php
Normal file
15
src/Domain/Shared/Application/Command/DeleteJob.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Application\Command;
|
||||||
|
|
||||||
|
use App\Domain\Shared\Domain\Contract\CommandInterface;
|
||||||
|
|
||||||
|
readonly class DeleteJob implements CommandInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $id
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Application\Command;
|
||||||
|
|
||||||
|
use App\Domain\Shared\Domain\Contract\CommandInterface;
|
||||||
|
use App\Domain\Shared\Domain\Model\JobStatus;
|
||||||
|
|
||||||
|
readonly class DeleteJobsByCriteria implements CommandInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param JobStatus[]|null $statuses
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public ?array $statuses = null,
|
||||||
|
public ?string $type = null,
|
||||||
|
public ?\DateTimeImmutable $createdAfter = null,
|
||||||
|
public ?\DateTimeImmutable $createdBefore = null,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Application\CommandHandler;
|
||||||
|
|
||||||
|
use App\Domain\Shared\Application\Command\DeleteJob;
|
||||||
|
use App\Domain\Shared\Domain\Contract\CommandHandlerInterface;
|
||||||
|
use App\Domain\Shared\Domain\Contract\CommandInterface;
|
||||||
|
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
||||||
|
|
||||||
|
readonly class DeleteJobHandler implements CommandHandlerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private JobRepositoryInterface $jobRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(CommandInterface $command): void
|
||||||
|
{
|
||||||
|
if (!$command instanceof DeleteJob) {
|
||||||
|
throw new \InvalidArgumentException(sprintf(
|
||||||
|
'Command must be instance of %s, %s given',
|
||||||
|
DeleteJob::class,
|
||||||
|
get_class($command)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->jobRepository->get($command->id);
|
||||||
|
$this->jobRepository->delete($command->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Application\CommandHandler;
|
||||||
|
|
||||||
|
use App\Domain\Shared\Application\Command\DeleteJobsByCriteria;
|
||||||
|
use App\Domain\Shared\Domain\Contract\CommandHandlerInterface;
|
||||||
|
use App\Domain\Shared\Domain\Contract\CommandInterface;
|
||||||
|
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
||||||
|
|
||||||
|
readonly class DeleteJobsByCriteriaHandler implements CommandHandlerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private JobRepositoryInterface $jobRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(CommandInterface $command): void
|
||||||
|
{
|
||||||
|
if (!$command instanceof DeleteJobsByCriteria) {
|
||||||
|
throw new \InvalidArgumentException(sprintf(
|
||||||
|
'Command must be instance of %s, %s given',
|
||||||
|
DeleteJobsByCriteria::class,
|
||||||
|
get_class($command)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$criteria = [];
|
||||||
|
|
||||||
|
if (null !== $command->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/Domain/Shared/Application/Query/GetJobById.php
Normal file
15
src/Domain/Shared/Application/Query/GetJobById.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Application\Query;
|
||||||
|
|
||||||
|
use App\Domain\Shared\Domain\Contract\QueryInterface;
|
||||||
|
|
||||||
|
readonly class GetJobById implements QueryInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $id
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Application\QueryHandler;
|
||||||
|
|
||||||
|
use App\Domain\Shared\Application\Query\GetJobById;
|
||||||
|
use App\Domain\Shared\Application\Response\JobResponse;
|
||||||
|
use App\Domain\Shared\Domain\Contract\QueryHandlerInterface;
|
||||||
|
use App\Domain\Shared\Domain\Contract\QueryInterface;
|
||||||
|
use App\Domain\Shared\Domain\Contract\ResponseInterface;
|
||||||
|
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
||||||
|
|
||||||
|
readonly class GetJobByIdHandler implements QueryHandlerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private JobRepositoryInterface $jobRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(QueryInterface $query): ResponseInterface
|
||||||
|
{
|
||||||
|
if (!$query instanceof GetJobById) {
|
||||||
|
throw new \InvalidArgumentException(sprintf(
|
||||||
|
'Query must be instance of %s, %s given',
|
||||||
|
GetJobById::class,
|
||||||
|
get_class($query)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$job = $this->jobRepository->get($query->id);
|
||||||
|
|
||||||
|
return JobResponse::fromJob($job);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/Domain/Shared/Application/Response/JobResponse.php
Normal file
41
src/Domain/Shared/Application/Response/JobResponse.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Application\Response;
|
||||||
|
|
||||||
|
use App\Domain\Shared\Domain\Contract\ResponseInterface;
|
||||||
|
use App\Domain\Shared\Domain\Model\Job;
|
||||||
|
|
||||||
|
readonly class JobResponse implements ResponseInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $id,
|
||||||
|
public string $type,
|
||||||
|
public string $status,
|
||||||
|
public \DateTimeImmutable $createdAt,
|
||||||
|
public ?\DateTimeImmutable $startedAt,
|
||||||
|
public ?\DateTimeImmutable $completedAt,
|
||||||
|
public ?string $failureReason,
|
||||||
|
public int $attempts,
|
||||||
|
public int $maxAttempts,
|
||||||
|
public array $context
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromJob(Job $job): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
id: $job->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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,4 +41,17 @@ interface JobRepositoryInterface
|
|||||||
* } $criteria
|
* } $criteria
|
||||||
*/
|
*/
|
||||||
public function countByCriteria(array $criteria): int;
|
public function countByCriteria(array $criteria): int;
|
||||||
|
|
||||||
|
public function delete(string $id): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{
|
||||||
|
* statuses?: ?array<JobStatus>,
|
||||||
|
* type?: ?string,
|
||||||
|
* createdAfter?: ?\DateTimeImmutable,
|
||||||
|
* createdBefore?: ?\DateTimeImmutable
|
||||||
|
* } $criteria
|
||||||
|
* @return int Nombre de jobs supprimés
|
||||||
|
*/
|
||||||
|
public function deleteByCriteria(array $criteria): int;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ namespace App\Domain\Shared\Infrastructure\ApiPlatform\Resource;
|
|||||||
use ApiPlatform\Metadata\ApiFilter;
|
use ApiPlatform\Metadata\ApiFilter;
|
||||||
use ApiPlatform\Metadata\ApiProperty;
|
use ApiPlatform\Metadata\ApiProperty;
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use App\Domain\Shared\Application\Response\JobResponse;
|
||||||
use App\Domain\Shared\Domain\Model\JobStatus;
|
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\GetJobListStateProvider;
|
||||||
|
use App\Domain\Shared\Infrastructure\ApiPlatform\State\Provider\GetJobStateProvider;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
#[ApiResource(
|
#[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
|
class GetJobListResource
|
||||||
@@ -157,4 +213,20 @@ class GetJobListResource
|
|||||||
context: $job->context
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Infrastructure\ApiPlatform\State\Processor;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProcessorInterface;
|
||||||
|
use App\Domain\Shared\Application\Command\DeleteJob;
|
||||||
|
use App\Domain\Shared\Application\CommandHandler\DeleteJobHandler;
|
||||||
|
use App\Domain\Shared\Domain\Exception\JobNotFoundException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
readonly class DeleteJobProcessor implements ProcessorInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private DeleteJobHandler $handler
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->handler->handle(new DeleteJob($uriVariables['id']));
|
||||||
|
} catch (JobNotFoundException $e) {
|
||||||
|
throw new NotFoundHttpException($e->getMessage(), $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Infrastructure\ApiPlatform\State\Processor;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProcessorInterface;
|
||||||
|
use App\Domain\Shared\Application\Command\DeleteJobsByCriteria;
|
||||||
|
use App\Domain\Shared\Application\CommandHandler\DeleteJobsByCriteriaHandler;
|
||||||
|
use App\Domain\Shared\Domain\Model\JobStatus;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
|
readonly class DeleteJobsByCriteriaProcessor implements ProcessorInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private DeleteJobsByCriteriaHandler $handler,
|
||||||
|
private RequestStack $requestStack
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
|
||||||
|
{
|
||||||
|
$request = $this->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,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Infrastructure\ApiPlatform\State\Provider;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Domain\Shared\Application\Query\GetJobById;
|
||||||
|
use App\Domain\Shared\Application\QueryHandler\GetJobByIdHandler;
|
||||||
|
use App\Domain\Shared\Domain\Exception\JobNotFoundException;
|
||||||
|
use App\Domain\Shared\Infrastructure\ApiPlatform\Resource\GetJobListResource;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
readonly class DeleteJobProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private GetJobByIdHandler $handler
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): GetJobListResource
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$response = $this->handler->handle(new GetJobById($uriVariables['id']));
|
||||||
|
} catch (JobNotFoundException $e) {
|
||||||
|
throw new NotFoundHttpException($e->getMessage(), $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetJobListResource::fromJobResponse($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Infrastructure\ApiPlatform\State\Provider;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Domain\Shared\Application\Query\GetJobById;
|
||||||
|
use App\Domain\Shared\Application\QueryHandler\GetJobByIdHandler;
|
||||||
|
use App\Domain\Shared\Domain\Exception\JobNotFoundException;
|
||||||
|
use App\Domain\Shared\Infrastructure\ApiPlatform\Resource\GetJobListResource;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
|
readonly class GetJobStateProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private GetJobByIdHandler $handler
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): GetJobListResource
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$response = $this->handler->handle(new GetJobById($uriVariables['id']));
|
||||||
|
} catch (JobNotFoundException $e) {
|
||||||
|
throw new NotFoundHttpException($e->getMessage(), $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetJobListResource::fromJobResponse($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -188,4 +188,52 @@ readonly class DoctrineJobRepository implements JobRepositoryInterface
|
|||||||
|
|
||||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Tests\Domain\Shared\Adapter;
|
namespace App\Tests\Domain\Shared\Adapter;
|
||||||
|
|
||||||
use App\Domain\Shared\Domain\Contract\JobRepositoryInterface;
|
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\Job;
|
||||||
use App\Domain\Shared\Domain\Model\JobStatus;
|
use App\Domain\Shared\Domain\Model\JobStatus;
|
||||||
|
|
||||||
@@ -97,6 +98,26 @@ class InMemoryJobRepository implements JobRepositoryInterface
|
|||||||
return count($this->findByCriteria($criteria));
|
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
|
public function clear(): void
|
||||||
{
|
{
|
||||||
$this->jobs = [];
|
$this->jobs = [];
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Domain\Shared\Application\CommandHandler;
|
||||||
|
|
||||||
|
use App\Domain\Scraping\Domain\Model\ScrapingJob;
|
||||||
|
use App\Domain\Shared\Application\Command\DeleteJob;
|
||||||
|
use App\Domain\Shared\Application\CommandHandler\DeleteJobHandler;
|
||||||
|
use App\Domain\Shared\Domain\Exception\JobNotFoundException;
|
||||||
|
use App\Tests\Domain\Shared\Adapter\InMemoryJobRepository;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class DeleteJobHandlerTest extends TestCase
|
||||||
|
{
|
||||||
|
private InMemoryJobRepository $repository;
|
||||||
|
private DeleteJobHandler $handler;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Domain\Shared\Application\CommandHandler;
|
||||||
|
|
||||||
|
use App\Domain\Scraping\Domain\Model\ScrapingJob;
|
||||||
|
use App\Domain\Shared\Application\Command\DeleteJobsByCriteria;
|
||||||
|
use App\Domain\Shared\Application\CommandHandler\DeleteJobsByCriteriaHandler;
|
||||||
|
use App\Domain\Shared\Domain\Model\JobStatus;
|
||||||
|
use App\Tests\Domain\Shared\Adapter\InMemoryJobRepository;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class DeleteJobsByCriteriaHandlerTest extends TestCase
|
||||||
|
{
|
||||||
|
private InMemoryJobRepository $repository;
|
||||||
|
private DeleteJobsByCriteriaHandler $handler;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,12 +75,4 @@ class DeleteMangaTest extends AbstractApiTestCase
|
|||||||
$this->assertResponseStatusCodeSame(404);
|
$this->assertResponseStatusCodeSame(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_returns_404_for_missing_id(): void
|
|
||||||
{
|
|
||||||
// When
|
|
||||||
static::createClient()->request('DELETE', '/api/mangas/');
|
|
||||||
|
|
||||||
// Then
|
|
||||||
$this->assertResponseStatusCodeSame(404);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user