125 lines
4.9 KiB
C#
125 lines
4.9 KiB
C#
using System.Text.Json.Nodes;
|
|
using w4c_workflows.Models.Credentials;
|
|
using w4c_workflows.Models.Nodes;
|
|
|
|
namespace w4c_workflows.Services.Nodes;
|
|
|
|
/// <summary>
|
|
/// Runs one node type. Implementations are registered in the node catalog and
|
|
/// resolved by the executor registry; the run engine feeds them resolved
|
|
/// parameters and input items and routes the returned outputs by port index.
|
|
/// </summary>
|
|
public interface INodeExecutor
|
|
{
|
|
/// <summary>Blueprint type this executor implements, e.g. "core.set".</summary>
|
|
string Type { get; }
|
|
|
|
Task<NodeExecutionOutcome> RunAsync(NodeExecutionContext context, CancellationToken ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Everything a node needs for one invocation. Parameters are already resolved
|
|
/// (interpolation applied) for the target item; the raw scope stays available so
|
|
/// a node can re-evaluate a parameter per item if it needs to.
|
|
/// </summary>
|
|
public sealed record NodeExecutionContext
|
|
{
|
|
public required NodeBlueprint Blueprint { get; init; }
|
|
|
|
/// <summary>Resolved parameters keyed by parameter name.</summary>
|
|
public required JsonObject Parameters { get; init; }
|
|
|
|
/// <summary>Input items per input port index.</summary>
|
|
public required IReadOnlyList<IReadOnlyList<FlowItem>> Inputs { get; init; }
|
|
|
|
/// <summary>Decrypted credentials keyed by credential alias.</summary>
|
|
public IReadOnlyDictionary<string, CredentialData> Credentials { get; init; } =
|
|
new Dictionary<string, CredentialData>();
|
|
|
|
/// <summary>Workflow-level environment variables.</summary>
|
|
public JsonObject Environment { get; init; } = new();
|
|
|
|
public string TenantId { get; init; } = string.Empty;
|
|
public string RunId { get; init; } = string.Empty;
|
|
public string TaskId { get; init; } = string.Empty;
|
|
public string? NodeName { get; init; }
|
|
|
|
/// <summary>Index of the item this invocation targets (0 for all-items mode).</summary>
|
|
public int ItemIndex { get; init; }
|
|
|
|
/// <summary>Run iteration index for loops (0 on the first pass).</summary>
|
|
public int RunIndex { get; init; }
|
|
|
|
/// <summary>Directory a code node resolves its entry file from.</summary>
|
|
public string? WorkingDirectory { get; init; }
|
|
|
|
/// <summary>
|
|
/// True when a loop-back edge targets this node, i.e. it is the pivot of a
|
|
/// loop and should advance its own iteration state on each invocation.
|
|
/// </summary>
|
|
public bool LoopBackInput { get; init; }
|
|
|
|
/// <summary>Run-scoped mutable state, shared by nodes so loop nodes can keep a cursor.</summary>
|
|
public IDictionary<string, object?> State { get; init; } = new Dictionary<string, object?>();
|
|
|
|
/// <summary>
|
|
/// Runs another workflow in-process (for <c>core.executeWorkflow</c>). Null
|
|
/// when the host does not support sub-workflows.
|
|
/// </summary>
|
|
public ISubWorkflowInvoker? SubWorkflows { get; init; }
|
|
|
|
/// <summary>Nesting depth of this run: 0 top-level, +1 per sub-workflow call.</summary>
|
|
public int Depth { get; init; }
|
|
|
|
public IServiceProvider? Services { get; init; }
|
|
|
|
/// <summary>Items from the given input port (empty when the port is absent).</summary>
|
|
public IReadOnlyList<FlowItem> Input(int portIndex = 0)
|
|
=> portIndex >= 0 && portIndex < Inputs.Count ? Inputs[portIndex] : Array.Empty<FlowItem>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of one node invocation. <see cref="Outputs"/> is indexed by output
|
|
/// port; a missing port means "no items on that port".
|
|
/// </summary>
|
|
public sealed record NodeExecutionOutcome
|
|
{
|
|
public required IReadOnlyList<IReadOnlyList<FlowItem>> Outputs { get; init; }
|
|
|
|
/// <summary>Set when the node failed but the engine may route to the error port.</summary>
|
|
public NodeFailure? Failure { get; init; }
|
|
|
|
/// <summary>
|
|
/// Set by a loop node on the invocation that emits its final ("done") output,
|
|
/// so the runner stops re-triggering it via the loop-back edge.
|
|
/// </summary>
|
|
public bool LoopComplete { get; init; }
|
|
|
|
public bool Succeeded => Failure == null;
|
|
|
|
/// <summary>Single-output convenience wrapper.</summary>
|
|
public static NodeExecutionOutcome Single(IReadOnlyList<FlowItem> items)
|
|
=> new() { Outputs = new[] { items } };
|
|
|
|
/// <summary>No items on any port (used by control nodes that consume input).</summary>
|
|
public static readonly NodeExecutionOutcome Empty = new()
|
|
{
|
|
Outputs = Array.Empty<IReadOnlyList<FlowItem>>(),
|
|
};
|
|
|
|
/// <summary>Failure on the error port.</summary>
|
|
public static NodeExecutionOutcome Failed(string message, string? code = null, string? description = null)
|
|
=> new()
|
|
{
|
|
Outputs = Array.Empty<IReadOnlyList<FlowItem>>(),
|
|
Failure = new NodeFailure(message, code, description),
|
|
};
|
|
}
|
|
|
|
/// <summary>A node-level failure, optionally mapped to a user-facing message.</summary>
|
|
public sealed record NodeFailure(
|
|
string Message,
|
|
string? Code = null,
|
|
string? Description = null,
|
|
int? HttpStatus = null);
|