Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion back/fixtures/Scenario.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ App\Entity\Scenario:
id: 016b1d91-a8df-6a09-90c2-56d8e6ef1fb6
name: Scenario 1
steps: ['@http_step_1', '@http_step_2', '@loop_step_1', '@loop_step_2', '@sleep_step_1', '@sleep_step_2', '@sqs_step_1', '@sqs_step_2']
webhooks: ['@webhook_1']

scenario_2 (extends scenario):
id: 016b1d91-a8df-6a09-90c2-56d8e6ef1fc7
name: Scenario 2
steps: []
steps: []
webhooks: []
8 changes: 8 additions & 0 deletions back/fixtures/Webhook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
App\Entity\Webhook:
webhook (template):
scenario: '@scenario_1'

webhook_1 (extends webhook) :
id: 1ee35bf0-d4ab-6220-996d-4143beb0724e
eventType: <(App\Entity\Enum\WebhookEventType::ON_FAILURE)>
url: https://url.domain.net/api
39 changes: 39 additions & 0 deletions back/migrations/Version20250212151143.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250212151143 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE webhook (id UUID NOT NULL, scenario_id UUID NOT NULL, event_type VARCHAR(255) NOT NULL, url VARCHAR(1000) NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_8A741756E04E49DF ON webhook (scenario_id)');
$this->addSql('COMMENT ON COLUMN webhook.id IS \'(DC2Type:uuid)\'');
$this->addSql('COMMENT ON COLUMN webhook.scenario_id IS \'(DC2Type:uuid)\'');
$this->addSql('ALTER TABLE webhook ADD CONSTRAINT FK_8A741756E04E49DF FOREIGN KEY (scenario_id) REFERENCES scenario (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE log ALTER created_at TYPE TIMESTAMP(6) WITHOUT TIME ZONE');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE webhook DROP CONSTRAINT FK_8A741756E04E49DF');
$this->addSql('DROP TABLE webhook');
$this->addSql('ALTER TABLE log ALTER created_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
}
}
30 changes: 30 additions & 0 deletions back/src/Entity/Enum/WebhookEventType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace App\Entity\Enum;

enum WebhookEventType: string
{
case ON_FAILURE = 'onFailure';
case ON_SUCCESS = 'onSuccess';

/**
* Return specific data for each event type.
*
* @return array<mixed>
*/
public function getWebhookData(): array
{
return match ($this) {
WebhookEventType::ON_FAILURE => [
'activityTitle' => '🚨 Error while executing the scenario',
'summary' => 'Scenario Execution Failure',
'themeColor' => 'FF0000',
],
WebhookEventType::ON_SUCCESS => [
'activityTitle' => '✅ Scenario executed successfully',
'summary' => 'Scenario Execution Success',
'themeColor' => '00FF00',
],
};
}
}
42 changes: 41 additions & 1 deletion back/src/Entity/Scenario.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#[ORM\Entity]
#[ApiResource(
normalizationContext: [
'groups' => ['scenario:read', 'step:read', 'httpstep:read', 'loopstep:read', 'sleepstep:read', 'sqsstep:read', 'metrics:read'],
'groups' => ['scenario:read', 'step:read', 'httpstep:read', 'loopstep:read', 'sleepstep:read', 'sqsstep:read', 'metrics:read', 'webhook:read'],
],
denormalizationContext: ['groups' => ['scenario:write']],
)]
Expand All @@ -38,6 +38,13 @@ class Scenario
#[ORM\OrderBy(['stepNumber' => 'ASC'])]
private Collection $steps;

/**
* @var Collection<int, Webhook>
*/
#[ORM\OneToMany(mappedBy: 'scenario', targetEntity: Webhook::class, cascade: ['persist', 'remove'], fetch: 'EAGER')]
#[Groups(['webhook:read'])]
private Collection $webhooks;

/**
* @var array<string, string>
*/
Expand All @@ -57,6 +64,7 @@ public function __construct()
{
$this->id = Uuid::v6();
$this->steps = new ArrayCollection();
$this->webhooks = new ArrayCollection();
}

public function getId(): Uuid
Expand Down Expand Up @@ -115,6 +123,38 @@ public function removeStep(AbstractStep $step): self
return $this;
}

public function hasWebhooks(): bool
{
return \count($this->webhooks) > 0;
}

/**
* @return Collection<int, Webhook>
*/
public function getWebhooks(): Collection
{
return $this->webhooks;
}

public function addWebhook(Webhook $webhook): self
{
if (!$this->webhooks->contains($webhook)) {
$this->webhooks[] = $webhook;
$webhook->setScenario($this);
}

return $this;
}

public function removeWebhook(Webhook $webhook): self
{
if ($this->webhooks->removeElement($webhook) && $webhook->getScenario() === $this) {
$webhook->setScenario(null);
}

return $this;
}

/**
* @return array<string, string>
*/
Expand Down
115 changes: 115 additions & 0 deletions back/src/Entity/Webhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use App\Entity\Enum\WebhookEventType;
use App\State\WebhookProvider;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity]
#[ApiResource(
paginationItemsPerPage: 10,
paginationClientItemsPerPage: true,
operations: [
new GetCollection(
uriTemplate: '/webhooks/scenarios/{idScenario}',
uriVariables: [
'idScenario',
],
provider: WebhookProvider::class,
),
],
normalizationContext: [
'groups' => ['webhook:read'],
],
)]
#[ApiResource(
paginationItemsPerPage: 10,
paginationClientItemsPerPage: true,
normalizationContext: [
'groups' => ['webhook:read'],
],
)]
class Webhook
{
#[ORM\Id]
#[ORM\Column(type: 'uuid', unique: true)]
#[Assert\NotBlank]
#[Groups(['webhook:read'])]
private Uuid $id;

#[Assert\NotBlank]
#[ORM\ManyToOne(targetEntity: Scenario::class, inversedBy: 'webhooks')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private ?Scenario $scenario;

#[ORM\Column(type: 'string', enumType: WebhookEventType::class)]
#[Assert\NotBlank]
#[Assert\Choice(callback: [WebhookEventType::class, 'cases'])]
#[Groups(['webhook:read'])]
private WebhookEventType $eventType;

#[ORM\Column(type: Types::STRING, length: 1000, nullable: false)]
#[Groups(['webhook:read'])]
#[Assert\NotBlank()]
private string $url;

public function __construct()
{
$this->id = Uuid::v6();
}

public function getId(): Uuid
{
return $this->id;
}

public function setId(string|Uuid $uuid): self
{
$this->id = \is_string($uuid) ? new Uuid($uuid) : $uuid;

return $this;
}

public function getScenario(): ?Scenario
{
return $this->scenario;
}

public function setScenario(?Scenario $scenario): self
{
$this->scenario = $scenario;

return $this;
}

public function getEventType(): WebhookEventType
{
return $this->eventType;
}

public function setEventType(WebhookEventType $eventType): self
{
$this->eventType = $eventType;

return $this;
}

public function getUrl(): ?string
{
return $this->url;
}

public function setUrl(string $url): self
{
$this->url = $url;

return $this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@

namespace App\Messenger\Handler\DataRetention\Logs;

final class DeleteLogsMessage {}
final class DeleteLogsMessage
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function __invoke(DeleteLogsMessage $message): void

$qb->delete('App\Entity\Log', 'l')
->where('l.createdAt < :dateLimit')
->setParameter('dateLimit', new \DateTime('-7 days')) # TODO: data retention settings on UI
->setParameter('dateLimit', new \DateTime('-7 days')) // TODO: data retention settings on UI
->getQuery()
->execute();
}
Expand Down
37 changes: 15 additions & 22 deletions back/src/Messenger/Handler/Http/HttpMessageHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Uid\Uuid;
use Symfony\Contracts\HttpClient\HttpClientInterface;

#[AsMessageHandler]
Expand Down Expand Up @@ -65,37 +64,31 @@ public function __invoke(HttpMessage $message): void
}

$this->setStepName($step->getName());
$this->logBeginStep($idScenario, $idExecution, $step);
$this->beginStep($idScenario, $idExecution, $step);

$response = $this->handleRequest($step, $context);
$stepInContext = $this->updateContext($context, $step, $response);

$nextStepMessage = $this->getNextStep($context, $step->getStepNumber() + 1);
$this->handleMessage($context, $nextStepMessage);
} catch (\Exception $e) {
$nextStepMessage = null;
if (null !== $step) {
$nextStepMessage = $this->getNextStepBasedOnFailure($context, $step->getStepNumber());
}
$this->handleError($context, $step ?? null, $e);
} finally {
$this->logEndStep($context, $idScenario, $idExecution, $step ?? null, $stepInContext ?? null, $this->getError() ?? null);
$this->setError(null);
}
}
$stepInContext ??= null;
$step ??= null;
$nextStepMessage ??= null;

if (null !== $stepInContext) {
$stepInContext = $this->replaceDynamicVariablesInStep($context, $stepInContext);
} elseif (null !== $step) {
$step = $this->replaceDynamicVariablesInStep($context, $step);
}

private function logEndStep(Context $context, Uuid $idScenario, Uuid $idExecution, ?AbstractStep $step, ?ContextHttpStep $stepInContext, ?string $error): void
{
if (null !== $stepInContext) {
$stepInContext = $this->replaceDynamicVariablesInStep($context, $stepInContext);
} elseif (null !== $step) {
$step = $this->replaceDynamicVariablesInStep($context, $step);
$this->endStep($context, $idScenario, $idExecution, $step, $stepInContext, $this->getError() ?? null, $nextStepMessage);
}

$this->log(
$idScenario,
$idExecution,
Logs::END_STEP->getLog(['name' => $this->getStepName(), 'handler' => $this->handlerName]),
isset($step) ? $step->getId() : Uuid::v6(),
$stepInContext ?? ($step ?? null),
$error
);
}

private function replaceDynamicVariablesInStep(Context $context, AbstractStep $step): AbstractStep
Expand Down
Loading