using System.Text.Json.Nodes; using w4c_workflows.Models.Credentials; using w4c_workflows.Models.Nodes; using w4c_workflows.Services.Nodes.Interpolation; using w4c_workflows.Services.Security; namespace w4c_workflows.Services.Nodes; // Invocation half of the graph runner: resolving a node's parameters per item, // building its execution context and running the executor (one call per item, or // the whole array at once for all-items nodes). public sealed partial class NodeGraphRunner { private async Task InvokeAsync( NodeGraphNode node, IReadOnlyList> inputs, NodeRunEnvironment environment, IReadOnlyDictionary>> outputs, NodeGraph graph, int runIndex, CancellationToken ct) { var executor = _executors.Resolve(node.Blueprint.Type) ?? throw new InvalidOperationException($"no executor for '{node.Blueprint.Type}'"); var loopBackInput = graph.EdgesTo(node.Id).Any(e => e.IsLoopBack); // Nodes with several inputs, or an explicit all-items run mode, receive the // whole array at once; the default is one invocation per input item. var allItems = node.EffectiveRunMode == NodeRunMode.AllItems || node.Blueprint.Inputs.Count > 1; if (allItems) { var parameters = ResolveParameters(node, null, 0, environment, outputs, graph, runIndex); return await executor.RunAsync( BuildContext(node, parameters, inputs, environment, 0, runIndex, loopBackInput), ct); } var mainItems = inputs.Count > 0 ? inputs[0] : Array.Empty(); // A node with no incoming items is skipped, except the entry node: like a // trigger/source it runs once with an empty item so it can generate data. var isEntry = string.Equals(node.Id, graph.EntryNodeId, StringComparison.Ordinal); if (mainItems.Count == 0 && !isEntry) return NodeExecutionOutcome.Empty; if (mainItems.Count == 0) mainItems = new[] { new FlowItem { Json = new JsonObject() } }; var accumulated = new List>(); for (var index = 0; index < mainItems.Count; index++) { var parameters = ResolveParameters(node, mainItems[index], index, environment, outputs, graph, runIndex); var outcome = await executor.RunAsync( BuildContext(node, parameters, new[] { (IReadOnlyList)new[] { mainItems[index] } }, environment, index, runIndex, loopBackInput), ct); if (!outcome.Succeeded) return outcome; while (accumulated.Count < outcome.Outputs.Count) accumulated.Add(new List()); for (var port = 0; port < outcome.Outputs.Count; port++) accumulated[port].AddRange(outcome.Outputs[port]); } return new NodeExecutionOutcome { Outputs = accumulated.Select(list => (IReadOnlyList)list).ToList(), }; } private NodeExecutionContext BuildContext( NodeGraphNode node, JsonObject parameters, IReadOnlyList> inputs, NodeRunEnvironment environment, int itemIndex, int runIndex, bool loopBackInput = false) => new() { Blueprint = node.Blueprint, Parameters = parameters, Inputs = inputs, Credentials = environment.Credentials, Environment = environment.Variables, TenantId = environment.TenantId, RunId = environment.RunId, TaskId = node.Id, NodeName = node.Id, ItemIndex = itemIndex, RunIndex = runIndex, WorkingDirectory = environment.WorkingDirectory, LoopBackInput = loopBackInput, State = environment.State, SubWorkflows = environment.SubWorkflows, Depth = environment.Depth, Services = environment.Services, }; private JsonObject ResolveParameters( NodeGraphNode node, FlowItem? item, int itemIndex, NodeRunEnvironment environment, IReadOnlyDictionary>> outputs, NodeGraph graph, int runIndex) { var scope = new InterpolationScope { Item = item?.Json, Parameters = node.Parameters, Environment = environment.Variables, ItemIndex = itemIndex, RunIndex = runIndex, NodeItems = name => ResolveNodeItems(name, outputs, graph), }; return _interpolator.ResolveObject(node.Parameters, scope); } private static IReadOnlyList ResolveNodeItems( string name, IReadOnlyDictionary>> outputs, NodeGraph graph) { if (outputs.TryGetValue(name, out var direct) && direct.Count > 0) return direct[0]; // Fall back to the blueprint display name, so $("Send message") also works. var byName = graph.FindByDisplayName(name); if (byName != null && outputs.TryGetValue(byName.Id, out var named) && named.Count > 0) return named[0]; return Array.Empty(); } }