w4c-workflows-api/Services/Nodes/INodeExecutor.cs
2026-09-12 01:02:46 +03:00

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);