using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using w4c_workflows.Models; namespace w4c_workflows.Data; public class WorkflowsDbContext : DbContext { public WorkflowsDbContext(DbContextOptions options) : base(options) { } /// /// The tenant's current workflow repo name (set per request by the source /// middleware, e.g. workflows or a user-selected repo). Every /// 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. /// 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 Workflows => Set(); public DbSet Tasks => Set(); public DbSet TaskEdges => Set(); public DbSet WorkflowRuns => Set(); public DbSet TaskRuns => Set(); public DbSet DurableStates => Set(); public DbSet WorkflowUsages => Set(); public DbSet ApiKeys => Set(); public DbSet WorkflowRepos => Set(); public DbSet WorkflowRuntimes => Set(); public DbSet Credentials => Set(); 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(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); 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); // 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(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.Property(t => t.NodeType).HasMaxLength(200); e.Property(t => t.ParametersJson).HasColumnType("jsonb"); e.Property(t => t.CredentialsJson).HasColumnType("jsonb"); e.Property(t => t.RetryJson).HasColumnType("jsonb"); e.Property(t => t.RunMode).HasMaxLength(32); 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(e => { e.ToTable("WorkflowTaskEdges"); e.HasKey(x => x.Id); e.Property(x => x.Id).ValueGeneratedOnAdd(); e.HasOne(x => x.Workflow) .WithMany(w => w.TaskEdges) .HasForeignKey(x => x.WorkflowId) .OnDelete(DeleteBehavior.Cascade); e.HasIndex(x => x.WorkflowId); }); modelBuilder.Entity(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"); // Sub-workflow plumbing: soft references (no FK constraint, added by // the idempotent raw SQL at startup — see Program.cs). e.Property(r => r.ParentRunId); e.Property(r => r.ParentTaskId); e.Property(r => r.Depth).HasDefaultValue(0); e.HasOne(r => r.Workflow) .WithMany() .HasForeignKey(r => r.WorkflowId) .OnDelete(DeleteBehavior.Restrict); e.HasIndex(r => r.TenantId); e.HasIndex(r => r.CorrelationId); e.HasIndex(r => r.ParentRunId); }); modelBuilder.Entity(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(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(e => { // Composite key: one counter row per tenant per quota period. e.HasKey(u => new { u.TenantId, u.PeriodStart }); e.Property(u => u.TenantId).HasMaxLength(120).IsRequired(); e.Property(u => u.RunsUsed).IsRequired(); }); modelBuilder.Entity(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); }); modelBuilder.Entity(e => { e.HasKey(r => r.TenantId); e.Property(r => r.TenantId).HasMaxLength(120).IsRequired(); e.Property(r => r.RepoName).HasMaxLength(120).IsRequired(); }); modelBuilder.Entity(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); }); modelBuilder.Entity(e => { e.HasKey(c => c.Id); e.Property(c => c.Id).ValueGeneratedOnAdd(); e.Property(c => c.TenantId).HasMaxLength(120).IsRequired(); e.Property(c => c.Name).HasMaxLength(200).IsRequired(); e.Property(c => c.Type).HasMaxLength(100).IsRequired(); e.Property(c => c.EncryptedData).HasColumnType("text").IsRequired(); e.HasIndex(c => new { c.TenantId, c.Name }).IsUnique(); e.HasIndex(c => c.TenantId); }); } }