using ClaudeDo.Data.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Data.Configuration; public class TaskEntityConfiguration : IEntityTypeConfiguration { private static string StatusToString(TaskStatus v) => v switch { TaskStatus.Idle => "idle", TaskStatus.Queued => "queued", TaskStatus.Running => "running", TaskStatus.Done => "done", TaskStatus.Failed => "failed", TaskStatus.Cancelled => "cancelled", _ => throw new ArgumentOutOfRangeException(nameof(v)), }; private static TaskStatus StatusFromString(string v) => v switch { "idle" => TaskStatus.Idle, "queued" => TaskStatus.Queued, "running" => TaskStatus.Running, "done" => TaskStatus.Done, "failed" => TaskStatus.Failed, "cancelled" => TaskStatus.Cancelled, _ => throw new ArgumentOutOfRangeException(nameof(v)), }; private static readonly ValueConverter StatusConverter = new(v => StatusToString(v), v => StatusFromString(v)); private static string PhaseToString(PlanningPhase v) => v switch { PlanningPhase.None => "none", PlanningPhase.Active => "active", PlanningPhase.Finalized => "finalized", _ => throw new ArgumentOutOfRangeException(nameof(v)), }; private static PlanningPhase PhaseFromString(string v) => v switch { "none" => PlanningPhase.None, "active" => PlanningPhase.Active, "finalized" => PlanningPhase.Finalized, _ => throw new ArgumentOutOfRangeException(nameof(v)), }; private static readonly ValueConverter PhaseConverter = new(v => PhaseToString(v), v => PhaseFromString(v)); public void Configure(EntityTypeBuilder builder) { builder.ToTable("tasks"); builder.HasKey(t => t.Id); builder.Property(t => t.Id).HasColumnName("id"); builder.Property(t => t.ListId).HasColumnName("list_id").IsRequired(); builder.Property(t => t.Title).HasColumnName("title").IsRequired(); builder.Property(t => t.Description).HasColumnName("description"); builder.Property(t => t.Status).HasColumnName("status").IsRequired() .HasConversion(StatusConverter); builder.Property(t => t.PlanningPhase).HasColumnName("planning_phase").IsRequired() .HasConversion(PhaseConverter).HasDefaultValue(PlanningPhase.None); builder.Property(t => t.BlockedByTaskId).HasColumnName("blocked_by_task_id"); builder.Property(t => t.ScheduledFor).HasColumnName("scheduled_for"); builder.Property(t => t.Result).HasColumnName("result"); builder.Property(t => t.LogPath).HasColumnName("log_path"); builder.Property(t => t.CreatedAt).HasColumnName("created_at").IsRequired(); builder.Property(t => t.StartedAt).HasColumnName("started_at"); builder.Property(t => t.FinishedAt).HasColumnName("finished_at"); builder.Property(t => t.CommitType).HasColumnName("commit_type").IsRequired().HasDefaultValue("chore"); builder.Property(t => t.Model).HasColumnName("model"); builder.Property(t => t.SystemPrompt).HasColumnName("system_prompt"); builder.Property(t => t.AgentPath).HasColumnName("agent_path"); builder.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false); builder.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false); builder.Property(t => t.Notes).HasColumnName("notes"); builder.Property(t => t.SortOrder).HasColumnName("sort_order").IsRequired().HasDefaultValue(0); builder.Property(t => t.ParentTaskId).HasColumnName("parent_task_id"); builder.Property(t => t.PlanningSessionId).HasColumnName("planning_session_id"); builder.Property(t => t.PlanningSessionToken).HasColumnName("planning_session_token"); builder.Property(t => t.PlanningFinalizedAt).HasColumnName("planning_finalized_at"); builder.Property(t => t.CreatedBy).HasColumnName("created_by"); builder.HasOne(t => t.Parent) .WithMany(t => t.Children) .HasForeignKey(t => t.ParentTaskId) .OnDelete(DeleteBehavior.Restrict); // BlockedBy: predecessor in a sequential chain. SetNull on delete so child becomes pickable. builder.HasOne() .WithMany() .HasForeignKey(t => t.BlockedByTaskId) .OnDelete(DeleteBehavior.SetNull); builder.HasOne(t => t.List) .WithMany(l => l.Tasks) .HasForeignKey(t => t.ListId) .OnDelete(DeleteBehavior.Cascade); builder.HasOne(t => t.Worktree) .WithOne(w => w.Task) .HasForeignKey(w => w.TaskId); builder.HasIndex(t => t.ListId).HasDatabaseName("idx_tasks_list_id"); builder.HasIndex(t => t.Status).HasDatabaseName("idx_tasks_status"); builder.HasIndex(t => new { t.ListId, t.SortOrder }).HasDatabaseName("idx_tasks_list_sort"); builder.HasIndex(t => t.ParentTaskId).HasDatabaseName("idx_tasks_parent_task_id"); builder.HasIndex(t => t.BlockedByTaskId).HasDatabaseName("idx_tasks_blocked_by"); } }