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. // --------------------------------------------------------------------------- /// Broad role of a blueprint. Stored as text so new roles need no migration. 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"; } /// Connection channel a port belongs to. "main" carries flow items. public static class PortKind { public const string Main = "main"; public const string Error = "error"; public const string Ai = "ai"; } /// How the runtime hands items to a node. public static class NodeRunMode { /// Invoke once per input item (the default). public const string EachItem = "eachItem"; /// Invoke once with the whole input array (aggregates, code nodes). public const string AllItems = "allItems"; } /// Parameter field kinds the authoring UI knows how to render. 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"; } /// Where a blueprint came from — drives provenance badges and filtering. public static class NodeOrigin { public const string BuiltIn = "builtIn"; public const string Connector = "connector"; public const string OpenApi = "openApi"; public const string Custom = "custom"; } /// /// 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). /// public sealed record NodeBlueprint { /// Stable machine id, e.g. "httpRequest" or "core.set". public required string Type { get; init; } public double Version { get; init; } = 1; public required string DisplayName { get; init; } public string? Description { get; init; } /// Optional expression string for the step-card subtitle. public string? Subtitle { get; init; } /// One of . public string Kind { get; init; } = NodeKind.Action; public List Categories { get; init; } = new(); public string? Icon { get; init; } public string? IconColor { get; init; } public List Inputs { get; init; } = new() { NodePort.Main }; public List Outputs { get; init; } = new() { NodePort.Main }; /// One of . public string RunMode { get; init; } = NodeRunMode.EachItem; public List Parameters { get; init; } = new(); public List Credentials { get; init; } = new(); /// Names of dynamic methods this node exposes (loadOptions/listSearch/schema). public List Methods { get; init; } = new(); /// /// Declarative REST connector definition. When present (and /// is ) the shared /// RestConnectorExecutor runs it, so an integration is catalog data, /// not a bespoke executor. /// public NodeConnector? Connector { get; init; } /// One of . public string Origin { get; init; } = NodeOrigin.BuiltIn; public string? DocumentationUrl { get; init; } /// True when the node is hidden from the default palette (internal/test). public bool Hidden { get; init; } /// /// 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. /// public bool LoopBack { get; init; } [JsonIgnore] public bool ProducesErrorBranch => Outputs.Any(p => string.Equals(p.Kind, PortKind.Error, StringComparison.Ordinal)); /// True when the blueprint exposes a parameter with the given name at any depth. public bool HasParameter(string name) => ParameterIndex.ContainsKey(name); /// /// Resolves a top-level parameter by name, or null. Precomputed so compile /// validation does not rescan the parameter list per supplied parameter. /// public NodeParameter? FindParameter(string name) => TopLevelParameterIndex.TryGetValue(name, out var parameter) ? parameter : null; private Dictionary? _parameterIndex; private Dictionary? _topLevelParameterIndex; private Dictionary ParameterIndex => _parameterIndex ??= BuildParameterIndex(NodeParameterDescender.DescendAll(Parameters)); private Dictionary TopLevelParameterIndex => _topLevelParameterIndex ??= BuildParameterIndex(Parameters); /// First-wins name index; duplicate names at one level are tolerated. private static Dictionary BuildParameterIndex(IEnumerable parameters) { var index = new Dictionary(StringComparer.Ordinal); foreach (var parameter in parameters) index.TryAdd(parameter.Name, parameter); return index; } } /// A named, typed connection point on a node. public sealed record NodePort { /// One of . public string Kind { get; init; } = PortKind.Main; /// Optional display label ("true", "false", "done", "loop"). 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" }; } /// A credential a node may need, referenced by alias in task parameters. public sealed record NodeCredentialLink { public required string Alias { get; init; } public string? DisplayName { get; init; } public bool Required { get; init; } } /// /// 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. /// public sealed record NodeParameter { /// Machine name used in task parameters and expressions. public required string Name { get; init; } public required string DisplayName { get; init; } /// One of . public required string Type { get; init; } /// /// Default value, or null when the parameter has none. Nullable is /// deliberate: a missing default deserializes to null, whereas a /// cannot be /// written by System.Text.Json (it throws on serialize). /// public JsonElement? Default { get; init; } public string? Description { get; init; } public string? Placeholder { get; init; } public bool Required { get; init; } /// When true the field is a configuration value and never interpolated. public bool NoDataExpression { get; init; } /// Value list for options/multiOptions. public List? Options { get; init; } /// Nested fields for collection/fixedCollection. public List? Fields { get; init; } /// Conditional visibility relative to sibling parameter values. public NodeVisibility? VisibleWhen { get; init; } /// Method on the blueprint used to fetch options dynamically. public string? LoadOptionsMethod { get; init; } /// Parameter names that must change before options are re-fetched. public List? DependsOn { get; init; } /// JSON Schema-ish extras (min/max, precision, multi-value, …); null when none. [JsonPropertyName("typeOptions")] public JsonElement? TypeOptions { get; init; } } /// A selectable value for an options/multiOptions parameter. public sealed record NodeParameterOption { public required string Name { get; init; } public required JsonElement Value { get; init; } public string? Description { get; init; } } /// /// Conditional visibility for a parameter. A field is shown when every entry in /// matches and no entry in matches; a /// missing parameter is treated as not matching. /// public sealed record NodeVisibility { public Dictionary>? Show { get; init; } public Dictionary>? Hide { get; init; } } /// Walks a parameter tree (nested collection fields) in depth-first order. internal static class NodeParameterDescender { internal static IEnumerable DescendAll(IEnumerable 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; } } } } /// Serialization options shared by catalog loading and API responses. public static class NodeJson { public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = false, }; }