w4c-workflows-api/Models/Entities.cs
2026-09-12 01:02:46 +03:00

411 lines
17 KiB
C#

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace w4c_workflows.Models;
// ---------------------------------------------------------------------------
// Well-known string constants. These are stored as plain text columns (not PG
// enums) so new languages / modes / scopes can be added without a migration.
// ---------------------------------------------------------------------------
public static class WorkflowMode
{
public const string Function = "function";
public const string Durable = "durable";
public const string Handler = "handler";
}
public static class WorkflowStatus
{
public const string Compiled = "compiled";
public const string Invalid = "invalid";
}
public static class RunStatus
{
public const string Pending = "pending";
public const string Running = "running";
public const string Succeeded = "succeeded";
public const string Failed = "failed";
public const string Compensating = "compensating";
public const string Dead = "dead";
}
public static class TaskRunStatus
{
public const string Pending = "pending";
public const string Running = "running";
public const string Succeeded = "succeeded";
public const string Failed = "failed";
public const string Compensated = "compensated";
public const string Dead = "dead";
}
public static class TriggerType
{
public const string Cron = "cron";
public const string Interval = "interval";
public const string Webhook = "webhook";
public const string Event = "event"; // manual / external event
public const string Queue = "queue"; // handler-mode stream subscription
}
public static class ApiKeyScope
{
public const string Manage = "manage";
public const string Run = "run";
public const string Read = "read";
}
/// <summary>
/// The kind of a workflow runtime (which workflow-api instance executes a
/// tenant's workflows).
/// </summary>
public static class WorkflowRuntimeKind
{
/// <summary>The built-in platform runtime (multi-tenant, git/Forgejo-backed).</summary>
public const string Platform = "platform";
/// <summary>A tenant-owned workflows-api instance (self-hosted, Lite mode).</summary>
public const string SelfHosted = "self-hosted";
}
public static class WorkflowRuntimeStatus
{
public const string Online = "online";
public const string Offline = "offline";
public const string Unknown = "unknown";
}
/// <summary>
/// A compiled workflow definition. The authoritative source is a YAML file in
/// git; this row is the compiled snapshot keyed by <c>git_sha</c>.
/// </summary>
public class Workflow
{
public Guid Id { get; set; }
public required string TenantId { get; set; }
public required string Name { get; set; }
public required string Path { get; set; }
public string? GitSha { get; set; }
/// <summary>
/// The repository name (basename) this workflow was compiled from, e.g.
/// <c>workflows</c> (default) or a user-selected repo like <c>wiz4apps</c>.
/// The workflows module only lists / runs workflows whose <see cref="Repo"/>
/// matches the tenant's current workflow repo; switching repos "unloads"
/// workflows from other repos without deleting their history.
/// </summary>
public string Repo { get; set; } = "workflows";
public required string Status { get; set; } // compiled | invalid
public required string Mode { get; set; } // function | durable | handler
public string? TriggerJson { get; set; } // jsonb: { type, cron, interval, webhookPath, stream }
/// <summary>
/// Whether the workflow's auto-trigger (cron/interval/webhook/handler) is
/// enabled. Toggled from the Workflows UI; the trigger scheduler skips
/// disabled workflows. Manual "run now" is unaffected.
/// </summary>
public bool TriggerEnabled { get; set; } = true;
public required string Target { get; set; } // execution host
/// <summary>
/// User-facing build/version of the definition (e.g. "1.0.1"). Read from the
/// YAML <c>version:</c> key when present (default "1.0.0"); the UI bumps the
/// patch number on each save.
/// </summary>
public string Version { get; set; } = "1.0.0";
public DateTime? CompiledAt { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public List<WorkflowTask> Tasks { get; set; } = new();
/// <summary>
/// Node-mode connections between tasks (from output port to input port).
/// Legacy script workflows have no rows here and keep using <c>NextId</c>.
/// </summary>
public List<WorkflowTaskEdge> TaskEdges { get; set; } = new();
}
/// <summary>
/// One node in a workflow's task tree. <c>ParentId</c> groups tasks for
/// structure/Mermaid; execution follows <c>NextId</c> (success) and
/// <c>OnErrorId</c> (failure). All three are soft references — resolved to task
/// ids during YAML compilation and never foreign-key constrained.
/// </summary>
public class WorkflowTask
{
public Guid Id { get; set; }
public Guid WorkflowId { get; set; }
/// <summary>Original YAML task id ("validate", "enrich", …); "root" for the entry task.</summary>
public required string Key { get; set; }
public Guid? ParentId { get; set; }
public Guid? NextId { get; set; }
public Guid? OnErrorId { get; set; }
public required string Language { get; set; }
public required string Mode { get; set; }
public string? EntryJson { get; set; } // jsonb: { file, function }
public string? EnvJson { get; set; } // jsonb: { KEY: "value" }
public int Order { get; set; }
/// <summary>
/// Managed-server id (or alias) the task's code is executed on via SSH. Null =>
/// run as a local subprocess (the default). Resolved at compile time from the
/// task's <c>server</c> or the workflow header's <c>server</c> default.
/// </summary>
public string? Server { get; set; }
// ---- node-mode columns -------------------------------------------------
// A task is either a script task (legacy: Language + EntryJson, driven by the
// NextId chain) or a node task (NodeType + ParametersJson, driven by
// WorkflowTaskEdge rows). The two modes never mix within one workflow.
/// <summary>Catalog blueprint type, e.g. <c>core.set</c>. Null for script tasks.</summary>
public string? NodeType { get; set; }
/// <summary>Pinned blueprint version (the resolved default when null/0).</summary>
public double? NodeVersion { get; set; }
/// <summary>jsonb: the node's raw (unresolved) parameters.</summary>
public string? ParametersJson { get; set; }
/// <summary>jsonb array: credential aliases the node resolves at run time.</summary>
public string? CredentialsJson { get; set; }
/// <summary>jsonb: the node's retry policy.</summary>
public string? RetryJson { get; set; }
/// <summary>Route node failures to the error port instead of failing the run.</summary>
public bool ContinueOnFail { get; set; }
/// <summary>Overrides the blueprint run mode: <c>eachItem</c> | <c>allItems</c>.</summary>
public string? RunMode { get; set; }
/// <summary>
/// Set when a task is removed from the workflow YAML (renamed/removed) so its
/// historical <see cref="TaskRun"/> rows are preserved instead of being wiped.
/// Archived tasks are excluded from active-definition queries via a global query
/// filter; sync re-activates a row whose id reappears in the YAML.
/// </summary>
public DateTime? ArchivedAt { get; set; }
public Workflow Workflow { get; set; } = null!;
}
/// <summary>
/// A connection between two node-mode tasks. <see cref="FromTaskId"/> /
/// <see cref="ToTaskId"/> are soft references (like <c>NextId</c>): a removed
/// task is archived, not deleted, so the FKs can't be constrained without
/// blocking history. Ports are 0-based indexes into the blueprint's
/// input/output lists.
/// </summary>
public class WorkflowTaskEdge
{
public Guid Id { get; set; }
public Guid WorkflowId { get; set; }
/// <summary>Source task id.</summary>
public Guid FromTaskId { get; set; }
/// <summary>Source output port index.</summary>
public int FromOutput { get; set; }
/// <summary>Target task id.</summary>
public Guid ToTaskId { get; set; }
/// <summary>Target input port index.</summary>
public int ToInput { get; set; }
/// <summary>True when this edge closes a loop (targets a loop-capable node).</summary>
public bool IsLoopBack { get; set; }
public Workflow Workflow { get; set; } = null!;
}
/// <summary>
/// A tenant-scoped credential for node executors. The secret payload is stored
/// as ciphertext (<see cref="EncryptedData"/>) and only ever decrypted in
/// memory during a run; it is never persisted in run history.
/// </summary>
public class Credential
{
public Guid Id { get; set; }
public required string TenantId { get; set; }
public required string Name { get; set; }
/// <summary>Credential type id from the credential catalog, e.g. <c>httpHeaderAuth</c>.</summary>
public required string Type { get; set; }
/// <summary>Ciphertext of the credential's JSON data (see <c>ICredentialCipher</c>).</summary>
public required string EncryptedData { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public class WorkflowRun
{
public Guid Id { get; set; }
public Guid WorkflowId { get; set; }
public required string TenantId { get; set; }
public required string Status { get; set; }
public string? TriggerJson { get; set; }
public string? InputJson { get; set; }
public string? OutputJson { get; set; }
public string? Error { get; set; }
public string? CorrelationId { get; set; }
/// <summary>
/// When set (durable resume), execution starts at this task instead of the
/// root entry. Null for a normal run, which always starts at the root task.
/// </summary>
public Guid? StartTaskId { get; set; }
/// <summary>
/// Remaining saga compensation steps as a JSON array of
/// <c>{ taskId, input }</c>, in dispatch order. Only populated while the run
/// is <see cref="RunStatus.Compensating"/>; cleared when compensation ends.
/// </summary>
public string? CompensationPlanJson { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? FinishedAt { get; set; }
// ---- sub-workflow plumbing --------------------------------------------
// A run started by a core.executeWorkflow node is a child run: it points at
// the run that spawned it and at the calling task, and records its nesting
// depth. All three are soft references (like NextId/TaskId elsewhere), so a
// parent/child whose workflow was later removed still resolves for history.
/// <summary>Run that spawned this one via a sub-workflow node; null for a top-level run.</summary>
public Guid? ParentRunId { get; set; }
/// <summary>The calling node's <see cref="WorkflowTask.Id"/>; null for a top-level run.</summary>
public Guid? ParentTaskId { get; set; }
/// <summary>Sub-workflow nesting depth: 0 for a top-level run, +1 per nested call.</summary>
public int Depth { get; set; }
public Workflow Workflow { get; set; } = null!;
public List<TaskRun> TaskRuns { get; set; } = new();
}
public class TaskRun
{
public Guid Id { get; set; }
public Guid RunId { get; set; }
public Guid TaskId { get; set; }
public int Attempt { get; set; }
public required string Status { get; set; }
public string? InputJson { get; set; }
public string? OutputJson { get; set; }
public string? Error { get; set; }
public int RetryCount { get; set; }
/// <summary>
/// When a failed attempt is scheduled for retry, the earliest time the retry
/// may be re-dispatched (exponential backoff). Null when no retry is pending.
/// </summary>
public DateTime? NextAttemptAt { get; set; }
/// <summary>
/// True when this task run is a saga compensation step (an <c>onError</c>
/// target run to undo a completed task), not part of the success chain.
/// </summary>
public bool IsCompensation { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? FinishedAt { get; set; }
public WorkflowRun Run { get; set; } = null!;
public WorkflowTask Task { get; set; } = null!;
}
/// <summary>
/// Checkpoint state for <c>durable</c> and <c>handler</c> workflows. Keyed by
/// the long-lived instance id; enables resume via correlation id after an
/// external event.
/// </summary>
public class DurableState
{
[Key]
public required string InstanceId { get; set; }
public Guid TaskId { get; set; }
public string? StateJson { get; set; }
public DateTime CheckpointAt { get; set; }
public string? CorrelationId { get; set; }
}
/// <summary>
/// Per-tenant workflow repo setting. Records which repository (basename) is the
/// tenant's current workflow repo; the repository holds the workflow YAML +
/// sibling code files. Defaults to the configured <c>WorkflowSource:WorkflowRepoName</c>
/// ("workflows") when no row exists.
/// </summary>
public class TenantWorkflowRepo
{
public required string TenantId { get; set; }
public required string RepoName { get; set; }
public DateTime? UpdatedAt { get; set; }
}
/// <summary>
/// Per-tenant workflow execution counter for one quota period (a calendar month,
/// UTC). Incremented once per top-level run — a sub-workflow child run spawned by
/// a <c>core.executeWorkflow</c> node is part of its parent's execution and is not
/// counted separately. The row is append-only in practice: clearing run history
/// does NOT reset it, so history cleanup can never be used to regain quota.
/// </summary>
public class WorkflowUsage
{
public required string TenantId { get; set; }
/// <summary>Start of the quota period (first instant of the calendar month, UTC).</summary>
public DateTime PeriodStart { get; set; }
/// <summary>Top-level executions started in this period.</summary>
public long RunsUsed { get; set; }
public DateTime UpdatedAt { get; set; }
}
/// <summary>
/// Per-tenant operator API key. Only the SHA-256 hash is stored; the raw key is
/// shown once at mint time.
/// </summary>
public class ApiKey
{
public Guid Id { get; set; }
public required string TenantId { get; set; }
public required string KeyHash { get; set; }
public required string Label { get; set; }
public string? ScopesJson { get; set; } // jsonb array: ["manage","run","read"]
public DateTime CreatedAt { get; set; }
public DateTime? LastUsedAt { get; set; }
public DateTime? RevokedAt { get; set; }
}
/// <summary>
/// A workflow runtime — a workflow-api instance that can execute a tenant's
/// workflows. There is always one virtual <see cref="WorkflowRuntimeKind.Platform"/>
/// runtime per tenant (the built-in multi-tenant engine); a tenant may additionally
/// connect <see cref="WorkflowRuntimeKind.SelfHosted"/> instances.
///
/// Connectivity model:
/// - <c>platform</c>: local, git/Forgejo-backed, shared filesystem (no endpoint).
/// - <c>self-hosted</c> (inbound/legacy): <see cref="Endpoint"/> + <see cref="ApiKeyHash"/>
/// — the tenant exposes a public URL and the platform calls it directly.
/// - <c>self-hosted</c> (outbound/runner): <see cref="SecretHash"/> only — the runtime
/// dials out to the platform over WebSocket and holds the channel; no inbound URL.
/// </summary>
public class WorkflowRuntime
{
public Guid Id { get; set; }
public required string TenantId { get; set; }
/// <summary>User-facing name (e.g. "Platform" or "my-server").</summary>
public required string Label { get; set; }
/// <summary><see cref="WorkflowRuntimeKind"/>: "platform" | "self-hosted".</summary>
public required string Kind { get; set; }
/// <summary>Legacy/inbound URL; null for the platform runtime or outbound-only runtimes.</summary>
public string? Endpoint { get; set; }
/// <summary>SHA-256 hash of the inbound API key (legacy migration path).</summary>
public string? ApiKeyHash { get; set; }
/// <summary>SHA-256 hash of the outbound channel secret (runner model).</summary>
public string? SecretHash { get; set; }
/// <summary><see cref="WorkflowRuntimeStatus"/>: "online" | "offline" | "unknown".</summary>
public string Status { get; set; } = WorkflowRuntimeStatus.Unknown;
public DateTime? LastSeenAt { get; set; }
public string? LastError { get; set; }
/// <summary>Cached self-info json: { name, version, startedAt, multiTenant }.</summary>
public string? InfoJson { get; set; }
/// <summary>
/// True for the tenant's default <em>self-hosted</em> runtime (used when the
/// user has not picked a specific self-hosted runtime). The platform runtime
/// is the implicit global fallback (identified by <see cref="Kind"/>), not a
/// default among self-hosted runtimes, so its <see cref="IsDefault"/> is false.
/// </summary>
public bool IsDefault { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}