2026-05-24 12:17:19 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Requests\App\Automations;
|
|
|
|
|
|
2026-06-18 14:50:10 +00:00
|
|
|
use App\Enums\Ai\ContentStyle;
|
Back fixed-set automation strings with enums and consts
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
2026-06-13 18:04:24 +00:00
|
|
|
use App\Enums\Automation\AuthType;
|
2026-05-24 12:17:19 +00:00
|
|
|
use App\Enums\Automation\Condition\Operator as ConditionOperator;
|
Back fixed-set automation strings with enums and consts
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
2026-06-13 18:04:24 +00:00
|
|
|
use App\Enums\Automation\DelayUnit;
|
|
|
|
|
use App\Enums\Automation\HttpMethod;
|
2026-05-24 12:17:19 +00:00
|
|
|
use App\Enums\Automation\Node\Type as NodeType;
|
|
|
|
|
use App\Enums\Automation\Publish\Mode as PublishMode;
|
Back fixed-set automation strings with enums and consts
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
2026-06-13 18:04:24 +00:00
|
|
|
use App\Enums\Automation\ScheduleField;
|
2026-05-24 12:17:19 +00:00
|
|
|
use App\Enums\Automation\Trigger\Type as TriggerType;
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
use App\Rules\ResolvableUrl;
|
2026-06-13 19:03:19 +00:00
|
|
|
use App\Services\Automation\AutomationConfigValidator;
|
2026-06-11 18:47:29 +00:00
|
|
|
use App\Services\Automation\GenerateNodeValidator;
|
|
|
|
|
use Illuminate\Contracts\Validation\Validator;
|
2026-05-24 12:17:19 +00:00
|
|
|
use Illuminate\Foundation\Http\FormRequest;
|
|
|
|
|
use Illuminate\Validation\Rule;
|
|
|
|
|
|
|
|
|
|
class UpdateAutomationRequest extends FormRequest
|
|
|
|
|
{
|
|
|
|
|
public function authorize(): bool
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array<string, mixed>
|
|
|
|
|
*/
|
|
|
|
|
public function rules(): array
|
|
|
|
|
{
|
|
|
|
|
$rules = [
|
|
|
|
|
'name' => ['sometimes', 'string', 'max:120'],
|
|
|
|
|
'nodes' => ['sometimes', 'array'],
|
|
|
|
|
'nodes.*.id' => ['required', 'string'],
|
|
|
|
|
'nodes.*.type' => ['required', 'string', Rule::in(array_column(NodeType::cases(), 'value'))],
|
|
|
|
|
'nodes.*.position' => ['required', 'array'],
|
|
|
|
|
'nodes.*.position.x' => ['required', 'numeric'],
|
|
|
|
|
'nodes.*.position.y' => ['required', 'numeric'],
|
|
|
|
|
'nodes.*.data' => ['required', 'array'],
|
|
|
|
|
'connections' => ['sometimes', 'array'],
|
|
|
|
|
'connections.*.id' => ['required', 'string'],
|
|
|
|
|
'connections.*.source' => ['required', 'string'],
|
|
|
|
|
'connections.*.target' => ['required', 'string'],
|
|
|
|
|
'connections.*.source_handle' => ['nullable', 'string'],
|
|
|
|
|
'connections.*.target_handle' => ['nullable', 'string'],
|
2026-06-11 18:47:29 +00:00
|
|
|
'variables' => ['sometimes', 'array', 'max:50'],
|
|
|
|
|
'variables.*.key' => ['required', 'string', 'max:60', 'regex:/^[A-Za-z_][A-Za-z0-9_]*$/', 'distinct'],
|
|
|
|
|
'variables.*.value' => ['nullable', 'string'],
|
2026-05-24 12:17:19 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Per-node data validation. We build these dynamically so each node's
|
|
|
|
|
// type drives the shape of its `data` payload, and so errors come back
|
|
|
|
|
// with full paths like `nodes.2.data.feed_url` for the frontend to map.
|
|
|
|
|
$nodes = $this->input('nodes', []);
|
|
|
|
|
if (is_array($nodes)) {
|
|
|
|
|
foreach ($nodes as $i => $node) {
|
|
|
|
|
$type = data_get($node, 'type');
|
2026-06-13 16:59:52 +00:00
|
|
|
foreach ($this->dataRulesForNodeType($type, (int) $i) as $field => $fieldRules) {
|
2026-05-24 12:17:19 +00:00
|
|
|
$rules["nodes.{$i}.data.{$field}"] = $fieldRules;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $rules;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 18:47:29 +00:00
|
|
|
/**
|
2026-06-13 19:03:19 +00:00
|
|
|
* Block saving a node whose config can't run: a Generate node whose image
|
|
|
|
|
* count doesn't fit a selected account's content-type, or a Webhook node
|
|
|
|
|
* whose payload template isn't valid JSON. Each issue is keyed to the field
|
|
|
|
|
* the frontend surfaces it under (mirrors the inline frontend validation).
|
2026-06-11 18:47:29 +00:00
|
|
|
*/
|
|
|
|
|
public function withValidator(Validator $validator): void
|
|
|
|
|
{
|
|
|
|
|
$validator->after(function (Validator $validator): void {
|
|
|
|
|
$nodes = $this->input('nodes', []);
|
|
|
|
|
|
|
|
|
|
if (! is_array($nodes)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 19:03:19 +00:00
|
|
|
foreach (app(AutomationConfigValidator::class)->issues($nodes) as $issue) {
|
|
|
|
|
$validator->errors()->add("nodes.{$issue['node_index']}.data.{$issue['field']}", $issue['message']);
|
2026-06-11 18:47:29 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 12:17:19 +00:00
|
|
|
/**
|
|
|
|
|
* @return array<string, mixed>
|
|
|
|
|
*/
|
|
|
|
|
public function attributes(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'nodes.*.data.feed_url' => 'Feed URL',
|
|
|
|
|
'nodes.*.data.url' => 'URL',
|
|
|
|
|
'nodes.*.data.cron' => 'cron expression',
|
|
|
|
|
'nodes.*.data.duration' => 'duration',
|
|
|
|
|
'nodes.*.data.unit' => 'unit',
|
|
|
|
|
'nodes.*.data.field' => 'field',
|
|
|
|
|
'nodes.*.data.operator' => 'operator',
|
|
|
|
|
'nodes.*.data.mode' => 'mode',
|
|
|
|
|
'nodes.*.data.method' => 'method',
|
|
|
|
|
'nodes.*.data.trigger_type' => 'trigger type',
|
|
|
|
|
'nodes.*.data.prompt_template' => 'prompt template',
|
|
|
|
|
'nodes.*.data.accounts' => 'accounts',
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array<string, array<int, mixed>>
|
|
|
|
|
*/
|
2026-06-13 16:59:52 +00:00
|
|
|
private function dataRulesForNodeType(?string $type, int $i): array
|
2026-05-24 12:17:19 +00:00
|
|
|
{
|
|
|
|
|
return match ($type) {
|
|
|
|
|
NodeType::Trigger->value => [
|
|
|
|
|
'trigger_type' => ['required', Rule::in(array_column(TriggerType::cases(), 'value'))],
|
2026-06-13 16:59:52 +00:00
|
|
|
'cron' => ['required_if:nodes.'.$i.'.data.trigger_type,'.TriggerType::Schedule->value, 'string'],
|
Back fixed-set automation strings with enums and consts
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
2026-06-13 18:04:24 +00:00
|
|
|
'schedule_field' => ['sometimes', Rule::in(array_column(ScheduleField::cases(), 'value'))],
|
2026-06-12 20:09:39 +00:00
|
|
|
'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'],
|
|
|
|
|
'schedule_hour' => ['sometimes', 'integer', 'min:0', 'max:23'],
|
|
|
|
|
'schedule_minute' => ['sometimes', 'integer', 'min:0', 'max:59'],
|
|
|
|
|
'schedule_weekdays' => ['sometimes', 'array'],
|
|
|
|
|
'schedule_weekdays.*' => ['integer', 'min:0', 'max:6'],
|
|
|
|
|
'schedule_day_of_month' => ['sometimes', 'integer', 'min:1', 'max:31'],
|
|
|
|
|
'schedule_timezone' => ['sometimes', 'string', 'timezone'],
|
2026-05-24 12:17:19 +00:00
|
|
|
],
|
|
|
|
|
NodeType::FetchRss->value => [
|
feat(automations): multi-format RSS/Atom feeds with dynamic variables
Replace the RSS-2.0-only SimpleXML parser with SimplePie so the Fetch RSS
node reads Atom 1.0 (YouTube, GitHub, The Verge…) and RSS 2.0 + namespace
extensions (dc:, content:, media:, yt:, itunes:). Each item exposes stable
cross-format aliases (title, link, date, content, author, …) plus every
namespaced field flattened for use as {{ fetched.* }}.
Add a feed-inspection endpoint that discovers a feed's real fields and feeds
them into the editor's expression autocomplete. Allow {{ }} expressions in the
feed URL via a ResolvableUrl rule. Parsing moves to a dedicated FeedParser
service; the fetch keeps the SSRF guard and gains XXE-safe parsing.
2026-06-16 13:56:08 +00:00
|
|
|
'feed_url' => ['required', new ResolvableUrl],
|
|
|
|
|
'discovered_fields' => ['sometimes', 'array'],
|
|
|
|
|
'discovered_fields.*.path' => ['required', 'string'],
|
|
|
|
|
'discovered_fields.*.sample' => ['nullable', 'string'],
|
2026-05-24 12:17:19 +00:00
|
|
|
],
|
|
|
|
|
NodeType::HttpRequest->value => [
|
2026-06-16 15:09:16 +00:00
|
|
|
'url' => ['required', new ResolvableUrl],
|
Back fixed-set automation strings with enums and consts
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
2026-06-13 18:04:24 +00:00
|
|
|
'method' => ['required', Rule::in(array_column(HttpMethod::cases(), 'value'))],
|
|
|
|
|
'auth_type' => ['required', Rule::in(array_column(AuthType::cases(), 'value'))],
|
2026-05-24 12:17:19 +00:00
|
|
|
'auth_token' => ['nullable', 'string'],
|
|
|
|
|
'auth_username' => ['nullable', 'string'],
|
|
|
|
|
'auth_password' => ['nullable', 'string'],
|
|
|
|
|
'auth_header_name' => ['nullable', 'string'],
|
|
|
|
|
'body_template' => ['nullable', 'string'],
|
|
|
|
|
'headers' => ['nullable', 'array'],
|
|
|
|
|
'headers.*' => ['string'],
|
|
|
|
|
'items_path' => ['nullable', 'string'],
|
|
|
|
|
'item_key_path' => ['nullable', 'string'],
|
|
|
|
|
'item_date_path' => ['nullable', 'string'],
|
|
|
|
|
],
|
|
|
|
|
NodeType::Generate->value => [
|
|
|
|
|
'accounts' => ['required', 'array', 'min:1'],
|
|
|
|
|
'prompt_template' => ['required', 'string'],
|
2026-06-12 20:09:39 +00:00
|
|
|
'target_slide_count' => ['nullable', 'integer', 'min:0', 'max:'.GenerateNodeValidator::MAX_GENERATED_IMAGES],
|
|
|
|
|
'use_brand_voice' => ['sometimes', 'boolean'],
|
|
|
|
|
'use_brand_visuals' => ['sometimes', 'boolean'],
|
2026-06-18 14:50:10 +00:00
|
|
|
'style' => ['sometimes', Rule::in(array_column(ContentStyle::cases(), 'value'))],
|
2026-05-24 12:17:19 +00:00
|
|
|
],
|
|
|
|
|
NodeType::Delay->value => [
|
|
|
|
|
'duration' => ['required', 'integer', 'min:1'],
|
Back fixed-set automation strings with enums and consts
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
2026-06-13 18:04:24 +00:00
|
|
|
'unit' => ['required', Rule::in(array_column(DelayUnit::cases(), 'value'))],
|
2026-05-24 12:17:19 +00:00
|
|
|
],
|
|
|
|
|
NodeType::Condition->value => [
|
|
|
|
|
'field' => ['required', 'string'],
|
|
|
|
|
'operator' => ['required', Rule::in(array_column(ConditionOperator::cases(), 'value'))],
|
|
|
|
|
'value' => ['nullable', 'string'],
|
|
|
|
|
],
|
|
|
|
|
NodeType::Publish->value => [
|
|
|
|
|
'mode' => ['required', Rule::in(array_column(PublishMode::cases(), 'value'))],
|
2026-06-13 16:59:52 +00:00
|
|
|
'scheduled_offset' => ['required_if:nodes.'.$i.'.data.mode,'.PublishMode::Scheduled->value, 'integer', 'min:0'],
|
2026-05-24 12:17:19 +00:00
|
|
|
],
|
|
|
|
|
NodeType::Webhook->value => [
|
2026-06-16 15:09:16 +00:00
|
|
|
'url' => ['required', new ResolvableUrl],
|
Back fixed-set automation strings with enums and consts
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
2026-06-13 18:04:24 +00:00
|
|
|
'method' => ['required', Rule::in(array_column(HttpMethod::cases(), 'value'))],
|
2026-05-24 12:17:19 +00:00
|
|
|
'payload_template' => ['nullable', 'string'],
|
|
|
|
|
'headers' => ['nullable', 'array'],
|
|
|
|
|
'headers.*' => ['string'],
|
|
|
|
|
],
|
|
|
|
|
NodeType::End->value => [
|
|
|
|
|
'reason' => ['nullable', 'string'],
|
|
|
|
|
],
|
|
|
|
|
default => [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|