283 lines
11 KiB
C#
283 lines
11 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace w4c_workflows.Models.Nodes;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The declarative definition of a node TYPE ("blueprint"). This is the catalog
|
|
// entry that both the compiler/validator and the authoring UI consume: it says
|
|
// what a node is called, which ports it has, which parameters it exposes and
|
|
// which credentials it needs. Runtime behaviour lives behind INodeExecutor.
|
|
//
|
|
// Field names are camelCase on the wire (System.Text.Json Web defaults) so the
|
|
// catalog JSON files in Services/Nodes/Catalog read naturally.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// <summary>Broad role of a blueprint. Stored as text so new roles need no migration.</summary>
|
|
public static class NodeKind
|
|
{
|
|
public const string Action = "action";
|
|
public const string Trigger = "trigger";
|
|
public const string Transform = "transform";
|
|
public const string Control = "control";
|
|
public const string Ai = "ai";
|
|
}
|
|
|
|
/// <summary>Connection channel a port belongs to. "main" carries flow items.</summary>
|
|
public static class PortKind
|
|
{
|
|
public const string Main = "main";
|
|
public const string Error = "error";
|
|
public const string Ai = "ai";
|
|
}
|
|
|
|
/// <summary>How the runtime hands items to a node.</summary>
|
|
public static class NodeRunMode
|
|
{
|
|
/// <summary>Invoke once per input item (the default).</summary>
|
|
public const string EachItem = "eachItem";
|
|
/// <summary>Invoke once with the whole input array (aggregates, code nodes).</summary>
|
|
public const string AllItems = "allItems";
|
|
}
|
|
|
|
/// <summary>Parameter field kinds the authoring UI knows how to render.</summary>
|
|
public static class NodeParameterType
|
|
{
|
|
public const string String = "string";
|
|
public const string Number = "number";
|
|
public const string Boolean = "boolean";
|
|
public const string Options = "options";
|
|
public const string MultiOptions = "multiOptions";
|
|
public const string Collection = "collection";
|
|
public const string FixedCollection = "fixedCollection";
|
|
public const string ResourceLocator = "resourceLocator";
|
|
public const string Json = "json";
|
|
public const string Notice = "notice";
|
|
public const string DateTime = "dateTime";
|
|
}
|
|
|
|
/// <summary>Where a blueprint came from — drives provenance badges and filtering.</summary>
|
|
public static class NodeOrigin
|
|
{
|
|
public const string BuiltIn = "builtIn";
|
|
public const string Connector = "connector";
|
|
public const string OpenApi = "openApi";
|
|
public const string Custom = "custom";
|
|
}
|
|
|
|
/// <summary>
|
|
/// A node type definition. Version is a decimal so it can track the same
|
|
/// minor-version history the reference integrations use (e.g. 2.7).
|
|
/// </summary>
|
|
public sealed record NodeBlueprint
|
|
{
|
|
/// <summary>Stable machine id, e.g. "httpRequest" or "core.set".</summary>
|
|
public required string Type { get; init; }
|
|
|
|
public double Version { get; init; } = 1;
|
|
|
|
public required string DisplayName { get; init; }
|
|
|
|
public string? Description { get; init; }
|
|
|
|
/// <summary>Optional expression string for the step-card subtitle.</summary>
|
|
public string? Subtitle { get; init; }
|
|
|
|
/// <summary>One of <see cref="NodeKind"/>.</summary>
|
|
public string Kind { get; init; } = NodeKind.Action;
|
|
|
|
public List<string> Categories { get; init; } = new();
|
|
|
|
public string? Icon { get; init; }
|
|
|
|
public string? IconColor { get; init; }
|
|
|
|
public List<NodePort> Inputs { get; init; } = new() { NodePort.Main };
|
|
|
|
public List<NodePort> Outputs { get; init; } = new() { NodePort.Main };
|
|
|
|
/// <summary>One of <see cref="NodeRunMode"/>.</summary>
|
|
public string RunMode { get; init; } = NodeRunMode.EachItem;
|
|
|
|
public List<NodeParameter> Parameters { get; init; } = new();
|
|
|
|
public List<NodeCredentialLink> Credentials { get; init; } = new();
|
|
|
|
/// <summary>Names of dynamic methods this node exposes (loadOptions/listSearch/schema).</summary>
|
|
public List<string> Methods { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// Declarative REST connector definition. When present (and
|
|
/// <see cref="Origin"/> is <see cref="NodeOrigin.Connector"/>) the shared
|
|
/// <c>RestConnectorExecutor</c> runs it, so an integration is catalog data,
|
|
/// not a bespoke executor.
|
|
/// </summary>
|
|
public NodeConnector? Connector { get; init; }
|
|
|
|
/// <summary>One of <see cref="NodeOrigin"/>.</summary>
|
|
public string Origin { get; init; } = NodeOrigin.BuiltIn;
|
|
|
|
public string? DocumentationUrl { get; init; }
|
|
|
|
/// <summary>True when the node is hidden from the default palette (internal/test).</summary>
|
|
public bool Hidden { get; init; }
|
|
|
|
/// <summary>
|
|
/// True when the node can host a loop: an edge back into it is treated as a
|
|
/// loop-back edge (excluded from the readiness count) so the loop body can
|
|
/// re-run. Only nodes that keep their own iteration state should set this.
|
|
/// </summary>
|
|
public bool LoopBack { get; init; }
|
|
|
|
[JsonIgnore]
|
|
public bool ProducesErrorBranch =>
|
|
Outputs.Any(p => string.Equals(p.Kind, PortKind.Error, StringComparison.Ordinal));
|
|
|
|
/// <summary>True when the blueprint exposes a parameter with the given name at any depth.</summary>
|
|
public bool HasParameter(string name) => ParameterIndex.ContainsKey(name);
|
|
|
|
/// <summary>
|
|
/// Resolves a top-level parameter by name, or null. Precomputed so compile
|
|
/// validation does not rescan the parameter list per supplied parameter.
|
|
/// </summary>
|
|
public NodeParameter? FindParameter(string name)
|
|
=> TopLevelParameterIndex.TryGetValue(name, out var parameter) ? parameter : null;
|
|
|
|
private Dictionary<string, NodeParameter>? _parameterIndex;
|
|
private Dictionary<string, NodeParameter>? _topLevelParameterIndex;
|
|
|
|
private Dictionary<string, NodeParameter> ParameterIndex => _parameterIndex ??=
|
|
BuildParameterIndex(NodeParameterDescender.DescendAll(Parameters));
|
|
|
|
private Dictionary<string, NodeParameter> TopLevelParameterIndex => _topLevelParameterIndex ??=
|
|
BuildParameterIndex(Parameters);
|
|
|
|
/// <summary>First-wins name index; duplicate names at one level are tolerated.</summary>
|
|
private static Dictionary<string, NodeParameter> BuildParameterIndex(IEnumerable<NodeParameter> parameters)
|
|
{
|
|
var index = new Dictionary<string, NodeParameter>(StringComparer.Ordinal);
|
|
foreach (var parameter in parameters)
|
|
index.TryAdd(parameter.Name, parameter);
|
|
return index;
|
|
}
|
|
}
|
|
|
|
/// <summary>A named, typed connection point on a node.</summary>
|
|
public sealed record NodePort
|
|
{
|
|
/// <summary>One of <see cref="PortKind"/>.</summary>
|
|
public string Kind { get; init; } = PortKind.Main;
|
|
|
|
/// <summary>Optional display label ("true", "false", "done", "loop").</summary>
|
|
public string? Label { get; init; }
|
|
|
|
public static readonly NodePort Main = new() { Kind = PortKind.Main };
|
|
public static readonly NodePort Error = new() { Kind = PortKind.Error, Label = "error" };
|
|
}
|
|
|
|
/// <summary>A credential a node may need, referenced by alias in task parameters.</summary>
|
|
public sealed record NodeCredentialLink
|
|
{
|
|
public required string Alias { get; init; }
|
|
public string? DisplayName { get; init; }
|
|
public bool Required { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// One form field in a node's parameter schema. Intentionally close to the
|
|
/// reference corpora's property model so migrated integrations map cleanly, but
|
|
/// with our own (clearer) names.
|
|
/// </summary>
|
|
public sealed record NodeParameter
|
|
{
|
|
/// <summary>Machine name used in task parameters and expressions.</summary>
|
|
public required string Name { get; init; }
|
|
|
|
public required string DisplayName { get; init; }
|
|
|
|
/// <summary>One of <see cref="NodeParameterType"/>.</summary>
|
|
public required string Type { get; init; }
|
|
|
|
/// <summary>
|
|
/// Default value, or <c>null</c> when the parameter has none. Nullable is
|
|
/// deliberate: a missing default deserializes to <c>null</c>, whereas a
|
|
/// <see cref="JsonValueKind.Undefined"/> <see cref="JsonElement"/> cannot be
|
|
/// written by System.Text.Json (it throws on serialize).
|
|
/// </summary>
|
|
public JsonElement? Default { get; init; }
|
|
|
|
public string? Description { get; init; }
|
|
|
|
public string? Placeholder { get; init; }
|
|
|
|
public bool Required { get; init; }
|
|
|
|
/// <summary>When true the field is a configuration value and never interpolated.</summary>
|
|
public bool NoDataExpression { get; init; }
|
|
|
|
/// <summary>Value list for options/multiOptions.</summary>
|
|
public List<NodeParameterOption>? Options { get; init; }
|
|
|
|
/// <summary>Nested fields for collection/fixedCollection.</summary>
|
|
public List<NodeParameter>? Fields { get; init; }
|
|
|
|
/// <summary>Conditional visibility relative to sibling parameter values.</summary>
|
|
public NodeVisibility? VisibleWhen { get; init; }
|
|
|
|
/// <summary>Method on the blueprint used to fetch options dynamically.</summary>
|
|
public string? LoadOptionsMethod { get; init; }
|
|
|
|
/// <summary>Parameter names that must change before options are re-fetched.</summary>
|
|
public List<string>? DependsOn { get; init; }
|
|
|
|
/// <summary>JSON Schema-ish extras (min/max, precision, multi-value, …); null when none.</summary>
|
|
[JsonPropertyName("typeOptions")]
|
|
public JsonElement? TypeOptions { get; init; }
|
|
}
|
|
|
|
/// <summary>A selectable value for an options/multiOptions parameter.</summary>
|
|
public sealed record NodeParameterOption
|
|
{
|
|
public required string Name { get; init; }
|
|
public required JsonElement Value { get; init; }
|
|
public string? Description { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Conditional visibility for a parameter. A field is shown when every entry in
|
|
/// <see cref="Show"/> matches and no entry in <see cref="Hide"/> matches; a
|
|
/// missing parameter is treated as not matching.
|
|
/// </summary>
|
|
public sealed record NodeVisibility
|
|
{
|
|
public Dictionary<string, List<JsonElement>>? Show { get; init; }
|
|
public Dictionary<string, List<JsonElement>>? Hide { get; init; }
|
|
}
|
|
|
|
/// <summary>Walks a parameter tree (nested collection fields) in depth-first order.</summary>
|
|
internal static class NodeParameterDescender
|
|
{
|
|
internal static IEnumerable<NodeParameter> DescendAll(IEnumerable<NodeParameter> parameters)
|
|
{
|
|
foreach (var parameter in parameters)
|
|
{
|
|
yield return parameter;
|
|
if (parameter.Fields is { Count: > 0 })
|
|
{
|
|
foreach (var nested in DescendAll(parameter.Fields))
|
|
yield return nested;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Serialization options shared by catalog loading and API responses.</summary>
|
|
public static class NodeJson
|
|
{
|
|
public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
|
|
{
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
WriteIndented = false,
|
|
};
|
|
}
|