*/ 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'], ]; // 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'); foreach ($this->dataRulesForNodeType($type) as $field => $fieldRules) { $rules["nodes.{$i}.data.{$field}"] = $fieldRules; } } } return $rules; } /** * @return array */ 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.image_source' => 'image source', 'nodes.*.data.accounts' => 'accounts', ]; } /** * @return array> */ private function dataRulesForNodeType(?string $type): array { return match ($type) { NodeType::Trigger->value => [ 'trigger_type' => ['required', Rule::in(array_column(TriggerType::cases(), 'value'))], 'cron' => ['required_if:nodes.*.data.trigger_type,'.TriggerType::Schedule->value, 'string'], ], NodeType::FetchRss->value => [ 'feed_url' => ['required', 'url'], ], 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'])], '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'], 'image_source' => ['required', Rule::in(['ai', 'unsplash', 'none'])], 'target_slide_count' => ['nullable', 'integer', 'min:1', 'max:20'], ], NodeType::Delay->value => [ 'duration' => ['required', 'integer', 'min:1'], 'unit' => ['required', Rule::in(['minutes', 'hours', 'days'])], ], 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'))], 'scheduled_offset' => ['nullable', 'integer', 'min:0'], ], NodeType::Webhook->value => [ 'url' => ['required', 'url'], 'method' => ['required', Rule::in(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])], 'payload_template' => ['nullable', 'string'], 'headers' => ['nullable', 'array'], 'headers.*' => ['string'], ], NodeType::End->value => [ 'reason' => ['nullable', 'string'], ], default => [], }; } }