w4c-workflows-api/Data/WorkflowsDbContext.cs

176 lines
8 KiB
C#
Raw Normal View History

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using w4c_workflows.Models;
namespace w4c_workflows.Data;
public class WorkflowsDbContext : DbContext
{
public WorkflowsDbContext(DbContextOptions<WorkflowsDbContext> options) : base(options)
{
}
2026-09-03 14:44:39 +00:00
/// <summary>
/// The tenant's current workflow repo name (set per request by the source
/// middleware, e.g. <c>workflows</c> or a user-selected repo). Every
/// <see cref="Workflow"/> query is scoped to this repo, so switching repos
/// "unloads" workflows that belong to a previously-selected repo without
/// deleting their history. Null in background/worker scopes (no scoping),
/// which is the same repo as the current tenant's selection.
/// </summary>
public string? CurrentRepo { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
// Additive schema (WorkflowTask.ArchivedAt for W7, WorkflowTask.Server for S8) is
// applied by idempotent raw SQL at startup — the project's house rule forbids new
// EF migrations. Suppress EF Core 10's "pending model changes" error so
// MigrateAsync (which only applies recorded migrations) does not reject those
// migration-less columns on a long-lived database. Runtime queries are unaffected.
optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));
}
public DbSet<Workflow> Workflows => Set<Workflow>();
public DbSet<WorkflowTask> Tasks => Set<WorkflowTask>();
public DbSet<WorkflowRun> WorkflowRuns => Set<WorkflowRun>();
public DbSet<TaskRun> TaskRuns => Set<TaskRun>();
public DbSet<DurableState> DurableStates => Set<DurableState>();
public DbSet<ApiKey> ApiKeys => Set<ApiKey>();
2026-09-03 14:44:39 +00:00
public DbSet<TenantWorkflowRepo> WorkflowRepos => Set<TenantWorkflowRepo>();
2026-09-09 13:09:48 +00:00
public DbSet<WorkflowRuntime> WorkflowRuntimes => Set<WorkflowRuntime>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Separate schema so the workflow tables never collide with the rest of
// the shared w4c database.
modelBuilder.HasDefaultSchema("workflows");
modelBuilder.Entity<Workflow>(e =>
{
e.HasKey(w => w.Id);
e.Property(w => w.Id).ValueGeneratedOnAdd();
e.Property(w => w.Name).HasMaxLength(200).IsRequired();
e.Property(w => w.Path).HasMaxLength(500).IsRequired();
e.Property(w => w.GitSha).HasMaxLength(64);
2026-09-03 14:44:39 +00:00
e.Property(w => w.Repo).HasMaxLength(120).IsRequired().HasDefaultValue("workflows");
e.Property(w => w.Status).HasMaxLength(32).IsRequired();
e.Property(w => w.Mode).HasMaxLength(32).IsRequired();
e.Property(w => w.Target).HasMaxLength(100).IsRequired();
e.Property(w => w.TriggerJson).HasColumnType("jsonb");
e.HasIndex(w => new { w.TenantId, w.Name }).IsUnique();
e.HasIndex(w => w.TenantId);
2026-09-03 14:44:39 +00:00
// Every active-query works against the tenant's CURRENT workflow repo.
// Workflows from a previously-selected repo are excluded (unloaded) but
// preserved for history. Background/worker scopes (CurrentRepo == null)
// see all; the sync service scopes itself explicitly by repo.
e.HasQueryFilter(w => CurrentRepo == null || w.Repo == CurrentRepo);
});
modelBuilder.Entity<WorkflowTask>(e =>
{
e.HasKey(t => t.Id);
e.Property(t => t.Id).ValueGeneratedOnAdd();
e.Property(t => t.Key).HasMaxLength(100).IsRequired();
e.Property(t => t.Language).HasMaxLength(32).IsRequired();
e.Property(t => t.Mode).HasMaxLength(32).IsRequired();
e.Property(t => t.EntryJson).HasColumnType("jsonb");
e.Property(t => t.EnvJson).HasColumnType("jsonb");
e.HasOne(t => t.Workflow)
.WithMany(w => w.Tasks)
.HasForeignKey(t => t.WorkflowId)
.OnDelete(DeleteBehavior.Cascade);
e.HasIndex(t => t.WorkflowId);
// Preserve historical TaskRuns when a task is removed from the YAML:
// removed tasks are archived (ArchivedAt != null) rather than deleted, and
// every "active workflow definition" query excludes them automatically.
e.HasQueryFilter(t => t.ArchivedAt == null);
});
modelBuilder.Entity<WorkflowRun>(e =>
{
e.HasKey(r => r.Id);
e.Property(r => r.Id).ValueGeneratedOnAdd();
e.Property(r => r.Status).HasMaxLength(32).IsRequired();
e.Property(r => r.TriggerJson).HasColumnType("jsonb");
e.Property(r => r.InputJson).HasColumnType("jsonb");
e.Property(r => r.OutputJson).HasColumnType("jsonb");
e.Property(r => r.Error).HasColumnType("text");
e.Property(r => r.CorrelationId).HasMaxLength(128);
e.Property(r => r.StartTaskId);
e.Property(r => r.CompensationPlanJson).HasColumnType("jsonb");
e.HasOne(r => r.Workflow)
.WithMany()
.HasForeignKey(r => r.WorkflowId)
.OnDelete(DeleteBehavior.Restrict);
e.HasIndex(r => r.TenantId);
e.HasIndex(r => r.CorrelationId);
});
modelBuilder.Entity<TaskRun>(e =>
{
e.HasKey(t => t.Id);
e.Property(t => t.Id).ValueGeneratedOnAdd();
e.Property(t => t.Status).HasMaxLength(32).IsRequired();
e.Property(t => t.InputJson).HasColumnType("jsonb");
e.Property(t => t.OutputJson).HasColumnType("jsonb");
e.Property(t => t.Error).HasColumnType("text");
e.HasOne(t => t.Run)
.WithMany(r => r.TaskRuns)
.HasForeignKey(t => t.RunId)
.OnDelete(DeleteBehavior.Cascade);
e.HasOne(t => t.Task)
.WithMany()
.HasForeignKey(t => t.TaskId)
.OnDelete(DeleteBehavior.Restrict);
e.HasIndex(t => t.RunId);
});
modelBuilder.Entity<DurableState>(e =>
{
e.HasKey(d => d.InstanceId);
e.Property(d => d.InstanceId).HasMaxLength(128);
e.Property(d => d.StateJson).HasColumnType("jsonb");
e.Property(d => d.CorrelationId).HasMaxLength(128);
e.HasIndex(d => d.CorrelationId);
});
modelBuilder.Entity<ApiKey>(e =>
{
e.HasKey(k => k.Id);
e.Property(k => k.Id).ValueGeneratedOnAdd();
e.Property(k => k.TenantId).IsRequired();
e.Property(k => k.KeyHash).HasMaxLength(64).IsRequired();
e.Property(k => k.Label).HasMaxLength(200).IsRequired();
e.Property(k => k.ScopesJson).HasColumnType("jsonb");
e.HasIndex(k => k.TenantId);
});
2026-09-03 14:44:39 +00:00
modelBuilder.Entity<TenantWorkflowRepo>(e =>
{
e.HasKey(r => r.TenantId);
e.Property(r => r.TenantId).HasMaxLength(120).IsRequired();
e.Property(r => r.RepoName).HasMaxLength(120).IsRequired();
});
2026-09-09 13:09:48 +00:00
modelBuilder.Entity<WorkflowRuntime>(e =>
{
e.HasKey(r => r.Id);
e.Property(r => r.Id).ValueGeneratedOnAdd();
e.Property(r => r.TenantId).HasMaxLength(120).IsRequired();
e.Property(r => r.Label).HasMaxLength(200).IsRequired();
e.Property(r => r.Kind).HasMaxLength(32).IsRequired();
e.Property(r => r.Endpoint).HasMaxLength(500);
e.Property(r => r.ApiKeyHash).HasMaxLength(64);
e.Property(r => r.SecretHash).HasMaxLength(64);
e.Property(r => r.Status).HasMaxLength(32).IsRequired().HasDefaultValue(WorkflowRuntimeStatus.Unknown);
e.Property(r => r.LastError).HasColumnType("text");
e.Property(r => r.InfoJson).HasColumnType("jsonb");
e.HasIndex(r => r.TenantId);
});
}
}