using System.Text.RegularExpressions; using w4c_workflows.Models; using w4c_workflows.Models.Nodes; using w4c_workflows.Services.Security; namespace w4c_workflows.Services.Nodes; /// Outcome of compiling a node workflow: the graph, or the reasons it is invalid. public sealed class NodeGraphCompileResult { public List Errors { get; } = new(); public bool Success => Errors.Count == 0; public NodeGraph? Graph { get; set; } } /// /// Turns the node-mode half of a into a /// validated : /// - resolves each step's blueprint (and pinned version) from the catalog; /// - checks parameters against the blueprint's schema; /// - lowers next/onError into port edges and merges explicit edges; /// - finds the single entry node and rejects cycles, except loops that close /// through a loop-capable node (). /// /// Script (legacy) tasks are handled by and are /// out of scope here: a definition is either node-mode or script-mode, not both. /// /// Per-step/parameter validation lives in NodeGraphCompiler.Validation.cs; /// edge lowering and forward-graph shape analysis in /// NodeGraphCompiler.Edges.cs. /// public sealed partial class NodeGraphCompiler { private static readonly string[] RunModes = { NodeRunMode.EachItem, NodeRunMode.AllItems }; [GeneratedRegex(@"^[a-zA-Z0-9._-]+$", RegexOptions.Compiled)] private static partial Regex StepIdRegex(); private readonly NodeBlueprintCatalog _catalog; private readonly NodePermissionPolicy? _permissions; public NodeGraphCompiler(NodeBlueprintCatalog catalog, NodePermissionPolicy? permissions = null) { _catalog = catalog; _permissions = permissions; } /// True when at least one step declares a node type. public static bool IsNodeWorkflow(WorkflowDefinition def) => def.Tasks?.Any(t => t.Node != null) == true; public NodeGraphCompileResult Compile(WorkflowDefinition def, string? tenantId = null) { var result = new NodeGraphCompileResult(); var tasks = def.Tasks ?? new List(); if (tasks.Count == 0) { result.Errors.Add("workflow has no tasks"); return result; } var nodes = new List(); var byId = new Dictionary(StringComparer.Ordinal); var blueprints = new Dictionary(StringComparer.Ordinal); foreach (var (task, index) in tasks.Select((t, i) => (t, i))) { var label = $"tasks[{index}]"; if (!ValidateStep(task, label, result.Errors)) continue; var blueprint = ResolveBlueprint(task, result.Errors); if (blueprint == null) continue; // Reject a barred node at save time so the author learns about the // policy immediately instead of at run time. The run kernel checks // again, because a policy can change after a workflow is stored. var permission = _permissions?.Evaluate(blueprint, tenantId); if (permission is { Allowed: false }) { result.Errors.Add( $"task '{task.Id}': {permission.Reason ?? $"node type '{blueprint.Type}' is not permitted"}"); continue; } byId[task.Id!] = task; blueprints[task.Id!] = blueprint; nodes.Add(BuildNode(task, blueprint, result.Errors)); } if (result.Errors.Count > 0) return result; ValidateCredentialAliases(nodes, blueprints, result.Errors); if (result.Errors.Count > 0) return result; var edges = BuildEdges(def, byId, blueprints, result.Errors); if (result.Errors.Count > 0) return result; var (markedEdges, loopError) = NodeGraphLinks.MarkLoopBackEdges(nodes, edges); if (loopError != null) { result.Errors.Add(loopError); return result; } edges = markedEdges; var entry = FindEntry(nodes, edges, result.Errors); if (result.Errors.Count > 0) return result; result.Graph = new NodeGraph { Nodes = nodes, Edges = edges, EntryNodeId = entry!, }; return result; } }