141 lines
5.3 KiB
C#
141 lines
5.3 KiB
C#
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<NodeExecutionOutcome> InvokeAsync(
|
|
NodeGraphNode node,
|
|
IReadOnlyList<IReadOnlyList<FlowItem>> inputs,
|
|
NodeRunEnvironment environment,
|
|
IReadOnlyDictionary<string, List<IReadOnlyList<FlowItem>>> 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<FlowItem>();
|
|
|
|
// 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<List<FlowItem>>();
|
|
|
|
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<FlowItem>)new[] { mainItems[index] } }, environment, index, runIndex, loopBackInput),
|
|
ct);
|
|
|
|
if (!outcome.Succeeded)
|
|
return outcome;
|
|
|
|
while (accumulated.Count < outcome.Outputs.Count)
|
|
accumulated.Add(new List<FlowItem>());
|
|
|
|
for (var port = 0; port < outcome.Outputs.Count; port++)
|
|
accumulated[port].AddRange(outcome.Outputs[port]);
|
|
}
|
|
|
|
return new NodeExecutionOutcome
|
|
{
|
|
Outputs = accumulated.Select(list => (IReadOnlyList<FlowItem>)list).ToList(),
|
|
};
|
|
}
|
|
|
|
private NodeExecutionContext BuildContext(
|
|
NodeGraphNode node,
|
|
JsonObject parameters,
|
|
IReadOnlyList<IReadOnlyList<FlowItem>> 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<string, List<IReadOnlyList<FlowItem>>> 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<FlowItem> ResolveNodeItems(
|
|
string name,
|
|
IReadOnlyDictionary<string, List<IReadOnlyList<FlowItem>>> 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<FlowItem>();
|
|
}
|
|
}
|