w4c-workflows-api/Services/Nodes/INodeExecutor.cs
2026-09-11 19:04:50 +03:00

97 lines
3.7 KiB
C#

using System.Text.Json.Nodes;
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 credential data keyed by credential alias.</summary>
public IReadOnlyDictionary<string, JsonObject> Credentials { get; init; } =
new Dictionary<string, JsonObject>();
/// <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; }
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; }
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);