using System.Text.Json.Nodes; namespace w4c_workflows.Models.Nodes; // --------------------------------------------------------------------------- // A compiled node graph: the executable form of the node-mode half of a // workflow definition. It is deliberately free of EF/DB types so it can be // built, validated and run in isolation (unit tests, previews, dry runs). // // `next`/`onError` from the YAML are lowered to edges here, so the run kernel // only ever follows edges — never a single "next" pointer. // --------------------------------------------------------------------------- /// One step in a node graph: a resolved blueprint plus its parameters. public sealed record NodeGraphNode { /// The YAML task id; stable and used in expressions such as $("stepId"). public required string Id { get; init; } /// Blueprint resolved from the catalog (validated at compile time). public required NodeBlueprint Blueprint { get; init; } /// Raw (unresolved) parameters; interpolation happens per item at run time. public required JsonObject Parameters { get; init; } /// /// Credentials this step needs, keyed by the blueprint credential alias; /// the value is a vault credential id or unique name, resolved at run time. /// public IReadOnlyDictionary CredentialRefs { get; init; } = new Dictionary(); /// Run-mode override; null means "use the blueprint default". public string? RunMode { get; init; } /// Effective run mode (override, else blueprint). public string EffectiveRunMode => string.IsNullOrWhiteSpace(RunMode) ? Blueprint.RunMode : RunMode!; public bool ContinueOnFail { get; init; } public TaskRetryDefinition? Retry { get; init; } public NodePort Output(int index) => index >= 0 && index < Blueprint.Outputs.Count ? Blueprint.Outputs[index] : NodePort.Main; public NodePort Input(int index) => index >= 0 && index < Blueprint.Inputs.Count ? Blueprint.Inputs[index] : NodePort.Main; /// Index of the blueprint's error output, or -1 when it has none. public int ErrorOutputIndex => Blueprint.Outputs.FindIndex(p => string.Equals(p.Kind, PortKind.Error, StringComparison.Ordinal)); } /// A directed connection between two steps on specific ports. public sealed record NodeGraphEdge { public required string FromNodeId { get; init; } public int FromOutput { get; init; } public required string ToNodeId { get; init; } public int ToInput { get; init; } /// /// True when this edge closes a loop (a back edge into a loop node). It does /// not count toward the target's readiness; instead it re-triggers the loop /// node for the next iteration. /// public bool IsLoopBack { get; init; } } /// /// A validated, runnable node graph. is the single /// node with no incoming non-loop edge. Cycles are rejected at compile time /// unless they close through a loop-capable node (a loop-back edge). /// public sealed record NodeGraph { public required IReadOnlyList Nodes { get; init; } public required IReadOnlyList Edges { get; init; } public required string EntryNodeId { get; init; } private Dictionary? _byId; private Dictionary? _byDisplayName; private Dictionary<(string NodeId, int Output), List>? _from; private Dictionary>? _to; public NodeGraphNode? Find(string nodeId) { _byId ??= Nodes.ToDictionary(n => n.Id, StringComparer.Ordinal); return _byId.TryGetValue(nodeId, out var node) ? node : null; } /// /// First node whose blueprint display name matches, or null. Lets /// $("Send message") resolve without scanning per /// item; built once and reused. /// public NodeGraphNode? FindByDisplayName(string displayName) { if (_byDisplayName == null) { // First-wins: duplicate display names are ambiguous but legal. var index = new Dictionary(StringComparer.Ordinal); foreach (var node in Nodes) index.TryAdd(node.Blueprint.DisplayName, node); _byDisplayName = index; } return _byDisplayName.TryGetValue(displayName, out var match) ? match : null; } public NodeGraphNode Require(string nodeId) => Find(nodeId) ?? throw new InvalidOperationException($"node '{nodeId}' is not in the graph"); /// Edges leaving a specific output port. public IEnumerable EdgesFrom(string nodeId, int output) { if (_from == null) { var index = new Dictionary<(string, int), List>(); foreach (var edge in Edges) { if (!index.TryGetValue((edge.FromNodeId, edge.FromOutput), out var outgoing)) index[(edge.FromNodeId, edge.FromOutput)] = outgoing = new List(); outgoing.Add(edge); } _from = index; } return _from.TryGetValue((nodeId, output), out var edges) ? edges : Array.Empty(); } /// Edges arriving at a node, across all inputs. public IEnumerable EdgesTo(string nodeId) { if (_to == null) { var index = new Dictionary>(StringComparer.Ordinal); foreach (var edge in Edges) { if (!index.TryGetValue(edge.ToNodeId, out var incoming)) index[edge.ToNodeId] = incoming = new List(); incoming.Add(edge); } _to = index; } return _to.TryGetValue(nodeId, out var edges) ? edges : Array.Empty(); } }