using System.Text.Json.Nodes; using w4c_workflows.Models.Credentials; using w4c_workflows.Models.Nodes; namespace w4c_workflows.Services.Nodes; /// /// 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. /// public interface INodeExecutor { /// Blueprint type this executor implements, e.g. "core.set". string Type { get; } Task RunAsync(NodeExecutionContext context, CancellationToken ct); } /// /// 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. /// public sealed record NodeExecutionContext { public required NodeBlueprint Blueprint { get; init; } /// Resolved parameters keyed by parameter name. public required JsonObject Parameters { get; init; } /// Input items per input port index. public required IReadOnlyList> Inputs { get; init; } /// Decrypted credentials keyed by credential alias. public IReadOnlyDictionary Credentials { get; init; } = new Dictionary(); /// Workflow-level environment variables. 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; } /// Index of the item this invocation targets (0 for all-items mode). public int ItemIndex { get; init; } /// Run iteration index for loops (0 on the first pass). public int RunIndex { get; init; } /// Directory a code node resolves its entry file from. public string? WorkingDirectory { get; init; } /// /// 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. /// public bool LoopBackInput { get; init; } /// Run-scoped mutable state, shared by nodes so loop nodes can keep a cursor. public IDictionary State { get; init; } = new Dictionary(); /// /// Runs another workflow in-process (for core.executeWorkflow). Null /// when the host does not support sub-workflows. /// public ISubWorkflowInvoker? SubWorkflows { get; init; } /// Nesting depth of this run: 0 top-level, +1 per sub-workflow call. public int Depth { get; init; } public IServiceProvider? Services { get; init; } /// Items from the given input port (empty when the port is absent). public IReadOnlyList Input(int portIndex = 0) => portIndex >= 0 && portIndex < Inputs.Count ? Inputs[portIndex] : Array.Empty(); } /// /// Result of one node invocation. is indexed by output /// port; a missing port means "no items on that port". /// public sealed record NodeExecutionOutcome { public required IReadOnlyList> Outputs { get; init; } /// Set when the node failed but the engine may route to the error port. public NodeFailure? Failure { get; init; } /// /// 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. /// public bool LoopComplete { get; init; } public bool Succeeded => Failure == null; /// Single-output convenience wrapper. public static NodeExecutionOutcome Single(IReadOnlyList items) => new() { Outputs = new[] { items } }; /// No items on any port (used by control nodes that consume input). public static readonly NodeExecutionOutcome Empty = new() { Outputs = Array.Empty>(), }; /// Failure on the error port. public static NodeExecutionOutcome Failed(string message, string? code = null, string? description = null) => new() { Outputs = Array.Empty>(), Failure = new NodeFailure(message, code, description), }; } /// A node-level failure, optionally mapped to a user-facing message. public sealed record NodeFailure( string Message, string? Code = null, string? Description = null, int? HttpStatus = null);