125 lines
4.5 KiB
C#
125 lines
4.5 KiB
C#
using System.Text.RegularExpressions;
|
|
using w4c_workflows.Models;
|
|
using w4c_workflows.Models.Nodes;
|
|
using w4c_workflows.Services.Security;
|
|
|
|
namespace w4c_workflows.Services.Nodes;
|
|
|
|
/// <summary>Outcome of compiling a node workflow: the graph, or the reasons it is invalid.</summary>
|
|
public sealed class NodeGraphCompileResult
|
|
{
|
|
public List<string> Errors { get; } = new();
|
|
public bool Success => Errors.Count == 0;
|
|
public NodeGraph? Graph { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Turns the node-mode half of a <see cref="WorkflowDefinition"/> into a
|
|
/// validated <see cref="NodeGraph"/>:
|
|
/// - resolves each step's blueprint (and pinned version) from the catalog;
|
|
/// - checks parameters against the blueprint's schema;
|
|
/// - lowers <c>next</c>/<c>onError</c> into port edges and merges explicit edges;
|
|
/// - finds the single entry node and rejects cycles, except loops that close
|
|
/// through a loop-capable node (<see cref="NodeGraphLinks"/>).
|
|
///
|
|
/// Script (legacy) tasks are handled by <see cref="WorkflowCompiler"/> and are
|
|
/// out of scope here: a definition is either node-mode or script-mode, not both.
|
|
///
|
|
/// Per-step/parameter validation lives in <c>NodeGraphCompiler.Validation.cs</c>;
|
|
/// edge lowering and forward-graph shape analysis in
|
|
/// <c>NodeGraphCompiler.Edges.cs</c>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>True when at least one step declares a node type.</summary>
|
|
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<TaskDefinition>();
|
|
|
|
if (tasks.Count == 0)
|
|
{
|
|
result.Errors.Add("workflow has no tasks");
|
|
return result;
|
|
}
|
|
|
|
var nodes = new List<NodeGraphNode>();
|
|
var byId = new Dictionary<string, TaskDefinition>(StringComparer.Ordinal);
|
|
var blueprints = new Dictionary<string, NodeBlueprint>(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;
|
|
}
|
|
}
|