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";
}
///
/// The kind of a workflow runtime (which workflow-api instance executes a
/// tenant's workflows).
///
public static class WorkflowRuntimeKind
{
/// The built-in platform runtime (multi-tenant, git/Forgejo-backed).
public const string Platform = "platform";
/// A tenant-owned workflows-api instance (self-hosted, Lite mode).
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";
}
///
/// A compiled workflow definition. The authoritative source is a YAML file in
/// git; this row is the compiled snapshot keyed by git_sha.
///
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; }
///
/// The repository name (basename) this workflow was compiled from, e.g.
/// workflows (default) or a user-selected repo like wiz4apps.
/// The workflows module only lists / runs workflows whose
/// matches the tenant's current workflow repo; switching repos "unloads"
/// workflows from other repos without deleting their history.
///
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 }
///
/// 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.
///
public bool TriggerEnabled { get; set; } = true;
public required string Target { get; set; } // execution host
///
/// User-facing build/version of the definition (e.g. "1.0.1"). Read from the
/// YAML version: key when present (default "1.0.0"); the UI bumps the
/// patch number on each save.
///
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 Tasks { get; set; } = new();
///
/// Node-mode connections between tasks (from output port to input port).
/// Legacy script workflows have no rows here and keep using NextId.
///
public List TaskEdges { get; set; } = new();
}
///
/// One node in a workflow's task tree. ParentId groups tasks for
/// structure/Mermaid; execution follows NextId (success) and
/// OnErrorId (failure). All three are soft references — resolved to task
/// ids during YAML compilation and never foreign-key constrained.
///
public class WorkflowTask
{
public Guid Id { get; set; }
public Guid WorkflowId { get; set; }
/// Original YAML task id ("validate", "enrich", …); "root" for the entry task.
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; }
///
/// 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 server or the workflow header's server default.
///
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.
/// Catalog blueprint type, e.g. core.set. Null for script tasks.
public string? NodeType { get; set; }
/// Pinned blueprint version (the resolved default when null/0).
public double? NodeVersion { get; set; }
/// jsonb: the node's raw (unresolved) parameters.
public string? ParametersJson { get; set; }
/// jsonb array: credential aliases the node resolves at run time.
public string? CredentialsJson { get; set; }
/// jsonb: the node's retry policy.
public string? RetryJson { get; set; }
/// Route node failures to the error port instead of failing the run.
public bool ContinueOnFail { get; set; }
/// Overrides the blueprint run mode: eachItem | allItems.
public string? RunMode { get; set; }
///
/// Set when a task is removed from the workflow YAML (renamed/removed) so its
/// historical 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.
///
public DateTime? ArchivedAt { get; set; }
public Workflow Workflow { get; set; } = null!;
}
///
/// A connection between two node-mode tasks. /
/// are soft references (like NextId): 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.
///
public class WorkflowTaskEdge
{
public Guid Id { get; set; }
public Guid WorkflowId { get; set; }
/// Source task id.
public Guid FromTaskId { get; set; }
/// Source output port index.
public int FromOutput { get; set; }
/// Target task id.
public Guid ToTaskId { get; set; }
/// Target input port index.
public int ToInput { get; set; }
/// True when this edge closes a loop (targets a loop-capable node).
public bool IsLoopBack { get; set; }
public Workflow Workflow { get; set; } = null!;
}
///
/// A tenant-scoped credential for node executors. The secret payload is stored
/// as ciphertext () and only ever decrypted in
/// memory during a run; it is never persisted in run history.
///
public class Credential
{
public Guid Id { get; set; }
public required string TenantId { get; set; }
public required string Name { get; set; }
/// Credential type id from the credential catalog, e.g. httpHeaderAuth.
public required string Type { get; set; }
/// Ciphertext of the credential's JSON data (see ICredentialCipher).
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; }
///
/// 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.
///
public Guid? StartTaskId { get; set; }
///
/// Remaining saga compensation steps as a JSON array of
/// { taskId, input }, in dispatch order. Only populated while the run
/// is ; cleared when compensation ends.
///
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.
/// Run that spawned this one via a sub-workflow node; null for a top-level run.
public Guid? ParentRunId { get; set; }
/// The calling node's ; null for a top-level run.
public Guid? ParentTaskId { get; set; }
/// Sub-workflow nesting depth: 0 for a top-level run, +1 per nested call.
public int Depth { get; set; }
public Workflow Workflow { get; set; } = null!;
public List 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; }
///
/// 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.
///
public DateTime? NextAttemptAt { get; set; }
///
/// True when this task run is a saga compensation step (an onError
/// target run to undo a completed task), not part of the success chain.
///
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!;
}
///
/// Checkpoint state for durable and handler workflows. Keyed by
/// the long-lived instance id; enables resume via correlation id after an
/// external event.
///
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; }
}
///
/// 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 WorkflowSource:WorkflowRepoName
/// ("workflows") when no row exists.
///
public class TenantWorkflowRepo
{
public required string TenantId { get; set; }
public required string RepoName { get; set; }
public DateTime? UpdatedAt { get; set; }
}
///
/// 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 core.executeWorkflow 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.
///
public class WorkflowUsage
{
public required string TenantId { get; set; }
/// Start of the quota period (first instant of the calendar month, UTC).
public DateTime PeriodStart { get; set; }
/// Top-level executions started in this period.
public long RunsUsed { get; set; }
public DateTime UpdatedAt { get; set; }
}
///
/// Per-tenant operator API key. Only the SHA-256 hash is stored; the raw key is
/// shown once at mint time.
///
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; }
}
///
/// A workflow runtime — a workflow-api instance that can execute a tenant's
/// workflows. There is always one virtual
/// runtime per tenant (the built-in multi-tenant engine); a tenant may additionally
/// connect instances.
///
/// Connectivity model:
/// - platform: local, git/Forgejo-backed, shared filesystem (no endpoint).
/// - self-hosted (inbound/legacy): +
/// — the tenant exposes a public URL and the platform calls it directly.
/// - self-hosted (outbound/runner): only — the runtime
/// dials out to the platform over WebSocket and holds the channel; no inbound URL.
///
public class WorkflowRuntime
{
public Guid Id { get; set; }
public required string TenantId { get; set; }
/// User-facing name (e.g. "Platform" or "my-server").
public required string Label { get; set; }
/// : "platform" | "self-hosted".
public required string Kind { get; set; }
/// Legacy/inbound URL; null for the platform runtime or outbound-only runtimes.
public string? Endpoint { get; set; }
/// SHA-256 hash of the inbound API key (legacy migration path).
public string? ApiKeyHash { get; set; }
/// SHA-256 hash of the outbound channel secret (runner model).
public string? SecretHash { get; set; }
/// : "online" | "offline" | "unknown".
public string Status { get; set; } = WorkflowRuntimeStatus.Unknown;
public DateTime? LastSeenAt { get; set; }
public string? LastError { get; set; }
/// Cached self-info json: { name, version, startedAt, multiTenant }.
public string? InfoJson { get; set; }
///
/// True for the tenant's default self-hosted runtime (used when the
/// user has not picked a specific self-hosted runtime). The platform runtime
/// is the implicit global fallback (identified by ), not a
/// default among self-hosted runtimes, so its is false.
///
public bool IsDefault { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}