trypost/app/Actions/Automation/Node/RunWebhookNode.php
Paulo Castellano 448ae73389 Add expression autocomplete, side-panel editor, and richer HTTP fetch
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
  graph-aware (suggests only what upstream nodes provide + variables + now);
  migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
  (matching the sidebar card), with a minimize control; the inline field
  collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
  buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form

HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
  dates; first poll records a baseline and emits nothing (date path too)

Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
  so the test panel aggregates all branches instead of one

Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
2026-06-12 11:31:58 -03:00

85 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Actions\Automation\Node;
use App\DataTransferObjects\Automation\NodeRunResult;
use App\Models\AutomationRun;
use App\Services\Automation\ExpressionResolver;
use App\Services\Brand\SafeHttpFetcher;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class RunWebhookNode
{
public function __construct(
private ExpressionResolver $resolver,
private SafeHttpFetcher $safeHttp,
) {}
public function __invoke(AutomationRun $run, array $config): NodeRunResult
{
$context = $run->resolverContext();
$url = $this->resolver->resolve($config['url'] ?? '', $context);
$method = strtoupper($config['method'] ?? 'POST');
try {
$this->safeHttp->guardAgainstSsrf($url);
} catch (RuntimeException) {
return NodeRunResult::failed(__('automations.errors.url_not_allowed'), [
'reason' => 'url_not_allowed',
'url' => $url,
]);
}
$headers = [];
foreach ($config['headers'] ?? [] as $k => $v) {
$headers[$k] = $this->resolver->resolve((string) $v, $context);
}
// Parse the template as JSON FIRST, then resolve placeholders in its
// string leaves — so a value containing `"`/`&`/newlines can't corrupt
// the JSON (the final json_encode escapes it).
$template = $config['payload_template'] ?? '{}';
$trimmedTemplate = trim($template);
if ($trimmedTemplate === '' || $trimmedTemplate === 'null') {
$payload = [];
} else {
$decodedTemplate = json_decode($template, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return NodeRunResult::failed(__('automations.errors.webhook_invalid_payload_json'), [
'reason' => 'invalid_payload_json',
]);
}
$payload = $this->resolver->resolveStructured($decodedTemplate ?? [], $context);
}
if ($run->is_dry_run) {
return NodeRunResult::completed(output: [
'webhook' => ['method' => $method, 'url' => $url, 'dry_run' => true],
]);
}
$response = Http::withHeaders($headers)
->withUserAgent(config('trypost.user_agent'))
->send($method, $url, ['json' => $payload]);
if ($response->serverError()) {
return NodeRunResult::failed(__('automations.errors.webhook_server_error'), [
'status' => $response->status(),
'body' => substr($response->body(), 0, 500),
]);
}
return NodeRunResult::completed(output: [
'webhook' => [
'status' => $response->status(),
'body' => substr($response->body(), 0, 500),
],
]);
}
}