diff --git a/app/Actions/Automation/Automation/CreateAutomation.php b/app/Actions/Automation/Automation/CreateAutomation.php index 59db041a..41d29fb0 100644 --- a/app/Actions/Automation/Automation/CreateAutomation.php +++ b/app/Actions/Automation/Automation/CreateAutomation.php @@ -4,6 +4,7 @@ namespace App\Actions\Automation\Automation; +use App\Enums\Automation\ScheduleField; use App\Enums\Automation\Status; use App\Enums\Automation\Trigger\Type as TriggerType; use App\Models\Automation; @@ -41,7 +42,7 @@ private function defaultTriggerNode(): array 'data' => [ 'trigger_type' => TriggerType::Schedule->value, 'cron' => '0 9 * * *', - 'schedule_field' => 'days', + 'schedule_field' => ScheduleField::Days->value, 'schedule_days_interval' => 1, 'schedule_hour' => 9, 'schedule_minute' => 0, diff --git a/app/Actions/Automation/Node/RunConditionNode.php b/app/Actions/Automation/Node/RunConditionNode.php index 2bbce980..7e3ea79d 100644 --- a/app/Actions/Automation/Node/RunConditionNode.php +++ b/app/Actions/Automation/Node/RunConditionNode.php @@ -20,8 +20,8 @@ public function __construct(private ExpressionResolver $resolver) {} public function __invoke(AutomationRun $run, array $config): NodeRunResult { $context = $run->resolverContext(); - $field = $this->resolver->resolve(data_get($config, 'field', ''), $context); - $operator = Operator::from(data_get($config, 'operator', 'equals')); + $field = $this->resolver->resolve((string) data_get($config, 'field', ''), $context); + $operator = Operator::from(data_get($config, 'operator', Operator::Equals->value)); $value = $this->resolver->resolve((string) data_get($config, 'value', ''), $context); $matched = match ($operator) { diff --git a/app/Actions/Automation/Node/RunDelayNode.php b/app/Actions/Automation/Node/RunDelayNode.php index c61f2b94..2ae4f8cc 100644 --- a/app/Actions/Automation/Node/RunDelayNode.php +++ b/app/Actions/Automation/Node/RunDelayNode.php @@ -5,6 +5,7 @@ namespace App\Actions\Automation\Node; use App\DataTransferObjects\Automation\NodeRunResult; +use App\Enums\Automation\DelayUnit; use App\Models\AutomationRun; use InvalidArgumentException; @@ -12,13 +13,13 @@ class RunDelayNode { public function __invoke(AutomationRun $run, array $config): NodeRunResult { - $duration = (int) ($config['duration'] ?? 0); - $unit = $config['unit'] ?? 'minutes'; + $duration = (int) data_get($config, 'duration', 0); + $unit = data_get($config, 'unit', DelayUnit::Minutes->value); - $until = match ($unit) { - 'minutes' => now()->addMinutes($duration), - 'hours' => now()->addHours($duration), - 'days' => now()->addDays($duration), + $until = match (DelayUnit::tryFrom((string) $unit)) { + DelayUnit::Minutes => now()->addMinutes($duration), + DelayUnit::Hours => now()->addHours($duration), + DelayUnit::Days => now()->addDays($duration), default => throw new InvalidArgumentException("Unknown delay unit: {$unit}"), }; diff --git a/app/Actions/Automation/Node/RunFetchRssNode.php b/app/Actions/Automation/Node/RunFetchRssNode.php index 45f7038b..cc0b02b3 100644 --- a/app/Actions/Automation/Node/RunFetchRssNode.php +++ b/app/Actions/Automation/Node/RunFetchRssNode.php @@ -34,6 +34,8 @@ class RunFetchRssNode { private const ITEM_HANDLE = 'default'; + private const NO_ITEMS_HANDLE = 'no_items'; + public function __construct( private ExpressionResolver $resolver, private SafeHttpFetcher $safeHttp, @@ -90,7 +92,7 @@ public function __invoke(AutomationRun $run, array $config): NodeRunResult } if ($newItems === []) { - return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: 'no_items'); + return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: self::NO_ITEMS_HANDLE); } $first = array_shift($newItems); diff --git a/app/Actions/Automation/Node/RunGenerateNode.php b/app/Actions/Automation/Node/RunGenerateNode.php index a108a0be..05cf59cb 100644 --- a/app/Actions/Automation/Node/RunGenerateNode.php +++ b/app/Actions/Automation/Node/RunGenerateNode.php @@ -29,7 +29,7 @@ public function __construct( public function __invoke(AutomationRun $run, array $config): NodeRunResult { $context = $run->resolverContext(); - $prompt = $this->resolver->resolve(data_get($config, 'prompt_template', ''), $context); + $prompt = $this->resolver->resolve((string) data_get($config, 'prompt_template', ''), $context); $accountsConfig = $this->resolveAccountsConfig($config); ['format' => $format, 'slide_count' => $slideCount] = $this->deriveFormat($accountsConfig, $config); diff --git a/app/Actions/Automation/Node/RunHttpRequestNode.php b/app/Actions/Automation/Node/RunHttpRequestNode.php index 76b6f958..0036cdc7 100644 --- a/app/Actions/Automation/Node/RunHttpRequestNode.php +++ b/app/Actions/Automation/Node/RunHttpRequestNode.php @@ -6,6 +6,8 @@ use App\Actions\Automation\Run\AdvanceAutomationRun; use App\DataTransferObjects\Automation\NodeRunResult; +use App\Enums\Automation\AuthType; +use App\Enums\Automation\HttpMethod; use App\Enums\Automation\Run\Status as RunStatus; use App\Models\AutomationNodeState; use App\Models\AutomationRun; @@ -47,6 +49,8 @@ class RunHttpRequestNode { private const ITEM_HANDLE = 'default'; + private const NO_ITEMS_HANDLE = 'no_items'; + /** * Upper bound on the per-node seen-key history. FIFO-evicts the oldest keys * once exceeded — matching n8n's capped "history size" for its dedup store. @@ -62,7 +66,7 @@ public function __construct( public function __invoke(AutomationRun $run, array $config): NodeRunResult { $url = (string) data_get($config, 'url', ''); - $method = strtoupper((string) data_get($config, 'method', 'GET')); + $method = strtoupper((string) data_get($config, 'method', HttpMethod::Get->value)); $nodeId = (string) $run->current_node_id; $context = $run->resolverContext(); @@ -85,12 +89,12 @@ public function __invoke(AutomationRun $run, array $config): NodeRunResult $jsonBody = $this->buildJsonBody($method, $config, $context); try { - $response = match ($method) { - 'GET' => $request->get($resolvedUrl), - 'DELETE' => $request->delete($resolvedUrl), - 'POST' => $request->post($resolvedUrl, $jsonBody), - 'PUT' => $request->put($resolvedUrl, $jsonBody), - 'PATCH' => $request->patch($resolvedUrl, $jsonBody), + $response = match (HttpMethod::tryFrom($method)) { + HttpMethod::Get => $request->get($resolvedUrl), + HttpMethod::Delete => $request->delete($resolvedUrl), + HttpMethod::Post => $request->post($resolvedUrl, $jsonBody), + HttpMethod::Put => $request->put($resolvedUrl, $jsonBody), + HttpMethod::Patch => $request->patch($resolvedUrl, $jsonBody), default => null, }; } catch (Throwable $e) { @@ -188,7 +192,7 @@ private function processItems(AutomationRun $run, string $nodeId, array $config, // flow, without spawning siblings, advancing watermarks or recording keys. if ($run->is_manual || $run->is_dry_run) { if ($items === []) { - return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: 'no_items'); + return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: self::NO_ITEMS_HANDLE); } return NodeRunResult::completed([ @@ -206,7 +210,7 @@ private function processItems(AutomationRun $run, string $nodeId, array $config, }; if ($newItems === []) { - return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: 'no_items'); + return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: self::NO_ITEMS_HANDLE); } $first = array_shift($newItems); @@ -333,13 +337,13 @@ private function buildRequest(array $config, array $context): PendingRequest $headers[$k] = $this->resolver->resolve((string) $v, $context); } - $authType = data_get($config, 'auth_type', 'none'); - if ($authType === 'bearer') { + $authType = AuthType::tryFrom((string) data_get($config, 'auth_type', AuthType::None->value)); + if ($authType === AuthType::Bearer) { $token = $this->decrypt((string) data_get($config, 'auth_token', '')); if ($token !== '') { $request = $request->withToken($this->resolver->resolve($token, $context)); } - } elseif ($authType === 'basic') { + } elseif ($authType === AuthType::Basic) { $user = (string) data_get($config, 'auth_username', ''); $pass = $this->decrypt((string) data_get($config, 'auth_password', '')); if ($user !== '' || $pass !== '') { @@ -348,7 +352,7 @@ private function buildRequest(array $config, array $context): PendingRequest $this->resolver->resolve($pass, $context), ); } - } elseif ($authType === 'api_key') { + } elseif ($authType === AuthType::ApiKey) { $headerName = (string) data_get($config, 'auth_header_name', 'X-API-Key'); $token = $this->decrypt((string) data_get($config, 'auth_token', '')); if ($token !== '') { @@ -370,7 +374,7 @@ private function buildRequest(array $config, array $context): PendingRequest */ private function buildJsonBody(string $method, array $config, array $context): array { - if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + if (! in_array(HttpMethod::tryFrom($method), HttpMethod::withBody(), true)) { return []; } diff --git a/app/Actions/Automation/Node/RunWebhookNode.php b/app/Actions/Automation/Node/RunWebhookNode.php index a2b38000..ce7475c6 100644 --- a/app/Actions/Automation/Node/RunWebhookNode.php +++ b/app/Actions/Automation/Node/RunWebhookNode.php @@ -5,6 +5,7 @@ namespace App\Actions\Automation\Node; use App\DataTransferObjects\Automation\NodeRunResult; +use App\Enums\Automation\HttpMethod; use App\Models\AutomationRun; use App\Services\Automation\ExpressionResolver; use App\Services\Brand\SafeHttpFetcher; @@ -22,8 +23,14 @@ public function __construct( public function __invoke(AutomationRun $run, array $config): NodeRunResult { $context = $run->resolverContext(); - $url = $this->resolver->resolve($config['url'] ?? '', $context); - $method = strtoupper($config['method'] ?? 'POST'); + $url = $this->resolver->resolve((string) data_get($config, 'url', ''), $context); + $method = strtoupper((string) data_get($config, 'method', HttpMethod::Post->value)); + + if ($url === '') { + return NodeRunResult::failed(__('automations.errors.webhook_missing_url'), [ + 'reason' => 'missing_url', + ]); + } try { $this->safeHttp->guardAgainstSsrf($url); diff --git a/app/Actions/Automation/Run/AdvanceAutomationRun.php b/app/Actions/Automation/Run/AdvanceAutomationRun.php index cfb5cf7b..1e76581f 100644 --- a/app/Actions/Automation/Run/AdvanceAutomationRun.php +++ b/app/Actions/Automation/Run/AdvanceAutomationRun.php @@ -42,7 +42,7 @@ public function __invoke(AutomationRun $run, string $fromNodeId, string $handle public function targetsFor(Automation $automation, string $fromNodeId, string $handle = 'default'): array { return collect($automation->connections ?? []) - ->filter(fn ($c) => ($c['source'] ?? null) === $fromNodeId && ($c['source_handle'] ?? 'default') === $handle) + ->filter(fn ($c) => data_get($c, 'source') === $fromNodeId && data_get($c, 'source_handle', 'default') === $handle) ->pluck('target') ->filter() ->values() diff --git a/app/Enums/Automation/AuthType.php b/app/Enums/Automation/AuthType.php new file mode 100644 index 00000000..b631dcda --- /dev/null +++ b/app/Enums/Automation/AuthType.php @@ -0,0 +1,17 @@ + + */ + public static function withBody(): array + { + return [self::Post, self::Put, self::Patch]; + } +} diff --git a/app/Enums/Automation/ScheduleField.php b/app/Enums/Automation/ScheduleField.php new file mode 100644 index 00000000..df151faf --- /dev/null +++ b/app/Enums/Automation/ScheduleField.php @@ -0,0 +1,18 @@ +value => [ 'trigger_type' => ['required', Rule::in(array_column(TriggerType::cases(), 'value'))], 'cron' => ['required_if:nodes.'.$i.'.data.trigger_type,'.TriggerType::Schedule->value, 'string'], - 'schedule_field' => ['sometimes', Rule::in(['minutes', 'hours', 'days', 'weeks', 'months'])], + 'schedule_field' => ['sometimes', Rule::in(array_column(ScheduleField::cases(), 'value'))], 'schedule_minutes_interval' => ['sometimes', 'integer', 'min:1', 'max:59'], 'schedule_hours_interval' => ['sometimes', 'integer', 'min:1', 'max:23'], 'schedule_days_interval' => ['sometimes', 'integer', 'min:1', 'max:31'], @@ -137,8 +141,8 @@ private function dataRulesForNodeType(?string $type, int $i): array ], NodeType::HttpRequest->value => [ 'url' => ['required', 'url'], - 'method' => ['required', Rule::in(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])], - 'auth_type' => ['required', Rule::in(['none', 'bearer', 'basic', 'api_key'])], + 'method' => ['required', Rule::in(array_column(HttpMethod::cases(), 'value'))], + 'auth_type' => ['required', Rule::in(array_column(AuthType::cases(), 'value'))], 'auth_token' => ['nullable', 'string'], 'auth_username' => ['nullable', 'string'], 'auth_password' => ['nullable', 'string'], @@ -159,7 +163,7 @@ private function dataRulesForNodeType(?string $type, int $i): array ], NodeType::Delay->value => [ 'duration' => ['required', 'integer', 'min:1'], - 'unit' => ['required', Rule::in(['minutes', 'hours', 'days'])], + 'unit' => ['required', Rule::in(array_column(DelayUnit::cases(), 'value'))], ], NodeType::Condition->value => [ 'field' => ['required', 'string'], @@ -172,7 +176,7 @@ private function dataRulesForNodeType(?string $type, int $i): array ], NodeType::Webhook->value => [ 'url' => ['required', 'url'], - 'method' => ['required', Rule::in(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])], + 'method' => ['required', Rule::in(array_column(HttpMethod::cases(), 'value'))], 'payload_template' => ['nullable', 'string'], 'headers' => ['nullable', 'array'], 'headers.*' => ['string'], diff --git a/lang/en/automations.php b/lang/en/automations.php index db769e8f..8355bfce 100644 --- a/lang/en/automations.php +++ b/lang/en/automations.php @@ -377,6 +377,7 @@ 'no_generated_post' => 'No generated post found on run.', 'webhook_server_error' => 'Webhook server error.', 'webhook_request_failed' => 'The webhook request could not be completed.', + 'webhook_missing_url' => 'The webhook node is missing a URL.', 'webhook_invalid_payload_json' => 'The payload template is not valid JSON.', 'url_not_allowed' => 'The request URL points to a private or unreachable address and was blocked.', 'node_no_longer_exists' => 'Node :node_id no longer exists in the automation.', diff --git a/lang/es/automations.php b/lang/es/automations.php index 8a37f35d..b28a22c6 100644 --- a/lang/es/automations.php +++ b/lang/es/automations.php @@ -377,6 +377,7 @@ 'no_generated_post' => 'No se encontró un post generado en la ejecución.', 'webhook_server_error' => 'Error del servidor del webhook.', 'webhook_request_failed' => 'No se pudo completar la solicitud del webhook.', + 'webhook_missing_url' => 'Al nodo de webhook le falta la URL.', 'webhook_invalid_payload_json' => 'La plantilla de payload no es un JSON válido.', 'url_not_allowed' => 'La URL de la petición apunta a una dirección privada o inaccesible y fue bloqueada.', 'node_no_longer_exists' => 'El nodo :node_id ya no existe en la automatización.', diff --git a/lang/pt-BR/automations.php b/lang/pt-BR/automations.php index 39149b4b..ffc28c9e 100644 --- a/lang/pt-BR/automations.php +++ b/lang/pt-BR/automations.php @@ -377,6 +377,7 @@ 'no_generated_post' => 'Nenhum post gerado encontrado para esta execução.', 'webhook_server_error' => 'Erro no servidor do webhook.', 'webhook_request_failed' => 'Não foi possível completar a requisição do webhook.', + 'webhook_missing_url' => 'O nó de webhook está sem a URL.', 'webhook_invalid_payload_json' => 'O template do payload não é um JSON válido.', 'url_not_allowed' => 'A URL da requisição aponta para um endereço privado ou inacessível e foi bloqueada.', 'node_no_longer_exists' => 'O nó :node_id não existe mais nesta automação.', diff --git a/resources/js/components/automations/config/ConditionNodeConfig.vue b/resources/js/components/automations/config/ConditionNodeConfig.vue index 97c8f5df..926ceb7a 100644 --- a/resources/js/components/automations/config/ConditionNodeConfig.vue +++ b/resources/js/components/automations/config/ConditionNodeConfig.vue @@ -10,10 +10,11 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { ConditionOperator, type ConditionOperatorValue } from '@/types/automation/condition-operator'; interface ConditionConfig { field: string; - operator: string; + operator: ConditionOperatorValue; value: string; } @@ -25,7 +26,7 @@ const emit = defineEmits<{ update: [Record] }>(); const local = ref({ field: (props.data.field as string) ?? '', - operator: (props.data.operator as string) ?? 'contains', + operator: (props.data.operator as ConditionOperatorValue) ?? ConditionOperator.Contains, value: (props.data.value as string) ?? '', }); @@ -47,13 +48,13 @@ watch(local, (val) => emit('update', val), { deep: true }); - {{ $t('automations.config.condition.operators.contains') }} - {{ $t('automations.config.condition.operators.not_contains') }} - {{ $t('automations.config.condition.operators.equals') }} - {{ $t('automations.config.condition.operators.not_equals') }} - {{ $t('automations.config.condition.operators.matches') }} - {{ $t('automations.config.condition.operators.greater_than') }} - {{ $t('automations.config.condition.operators.less_than') }} + {{ $t('automations.config.condition.operators.contains') }} + {{ $t('automations.config.condition.operators.not_contains') }} + {{ $t('automations.config.condition.operators.equals') }} + {{ $t('automations.config.condition.operators.not_equals') }} + {{ $t('automations.config.condition.operators.matches') }} + {{ $t('automations.config.condition.operators.greater_than') }} + {{ $t('automations.config.condition.operators.less_than') }} diff --git a/resources/js/components/automations/config/DelayNodeConfig.vue b/resources/js/components/automations/config/DelayNodeConfig.vue index 654b8746..158500c0 100644 --- a/resources/js/components/automations/config/DelayNodeConfig.vue +++ b/resources/js/components/automations/config/DelayNodeConfig.vue @@ -10,10 +10,11 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { DelayUnit, type DelayUnitValue } from '@/types/automation/delay-unit'; interface DelayConfig { duration: number; - unit: 'minutes' | 'hours' | 'days'; + unit: DelayUnitValue; } const props = defineProps<{ @@ -24,7 +25,7 @@ const emit = defineEmits<{ update: [Record] }>(); const local = ref({ duration: (props.data.duration as number) ?? 1, - unit: (props.data.unit as DelayConfig['unit']) ?? 'hours', + unit: (props.data.unit as DelayConfig['unit']) ?? DelayUnit.Hours, }); watch(local, (val) => emit('update', val), { deep: true }); @@ -45,9 +46,9 @@ watch(local, (val) => emit('update', val), { deep: true }); - {{ $t('automations.config.delay.units.minutes') }} - {{ $t('automations.config.delay.units.hours') }} - {{ $t('automations.config.delay.units.days') }} + {{ $t('automations.config.delay.units.minutes') }} + {{ $t('automations.config.delay.units.hours') }} + {{ $t('automations.config.delay.units.days') }} diff --git a/resources/js/components/automations/config/HttpRequestNodeConfig.vue b/resources/js/components/automations/config/HttpRequestNodeConfig.vue index bbc063fd..b1df2719 100644 --- a/resources/js/components/automations/config/HttpRequestNodeConfig.vue +++ b/resources/js/components/automations/config/HttpRequestNodeConfig.vue @@ -1,7 +1,7 @@ diff --git a/resources/js/components/automations/nodes/HttpRequestNode.vue b/resources/js/components/automations/nodes/HttpRequestNode.vue index 7efefa66..2f89b16a 100644 --- a/resources/js/components/automations/nodes/HttpRequestNode.vue +++ b/resources/js/components/automations/nodes/HttpRequestNode.vue @@ -1,8 +1,10 @@