From 51a5dcbb732fcc61d75086ba4acda1a9c3dc652a Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 16 Apr 2026 08:37:37 +0200 Subject: [PATCH] feat(data): add ClaudeDoDbContext with Fluent API configurations --- src/ClaudeDo.Data/ClaudeDoDbContext.cs | 22 ++++++ .../ListConfigEntityConfiguration.cs | 19 +++++ .../Configuration/ListEntityConfiguration.cs | 36 +++++++++ .../SubtaskEntityConfiguration.cs | 28 +++++++ .../Configuration/TagEntityConfiguration.cs | 22 ++++++ .../Configuration/TaskEntityConfiguration.cs | 73 +++++++++++++++++++ .../TaskRunEntityConfiguration.cs | 38 ++++++++++ .../WorktreeEntityConfiguration.cs | 41 +++++++++++ 8 files changed, 279 insertions(+) create mode 100644 src/ClaudeDo.Data/ClaudeDoDbContext.cs create mode 100644 src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs create mode 100644 src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs create mode 100644 src/ClaudeDo.Data/Configuration/SubtaskEntityConfiguration.cs create mode 100644 src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs create mode 100644 src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs create mode 100644 src/ClaudeDo.Data/Configuration/TaskRunEntityConfiguration.cs create mode 100644 src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs diff --git a/src/ClaudeDo.Data/ClaudeDoDbContext.cs b/src/ClaudeDo.Data/ClaudeDoDbContext.cs new file mode 100644 index 0000000..99a3086 --- /dev/null +++ b/src/ClaudeDo.Data/ClaudeDoDbContext.cs @@ -0,0 +1,22 @@ +using ClaudeDo.Data.Models; +using Microsoft.EntityFrameworkCore; + +namespace ClaudeDo.Data; + +public class ClaudeDoDbContext : DbContext +{ + public ClaudeDoDbContext(DbContextOptions options) : base(options) { } + + public DbSet Tasks => Set(); + public DbSet Lists => Set(); + public DbSet Tags => Set(); + public DbSet ListConfigs => Set(); + public DbSet Worktrees => Set(); + public DbSet TaskRuns => Set(); + public DbSet Subtasks => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(typeof(ClaudeDoDbContext).Assembly); + } +} diff --git a/src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs new file mode 100644 index 0000000..97d3451 --- /dev/null +++ b/src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs @@ -0,0 +1,19 @@ +using ClaudeDo.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ClaudeDo.Data.Configuration; + +public class ListConfigEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("list_config"); + + builder.HasKey(c => c.ListId); + builder.Property(c => c.ListId).HasColumnName("list_id"); + builder.Property(c => c.Model).HasColumnName("model"); + builder.Property(c => c.SystemPrompt).HasColumnName("system_prompt"); + builder.Property(c => c.AgentPath).HasColumnName("agent_path"); + } +} diff --git a/src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs new file mode 100644 index 0000000..17e5ebc --- /dev/null +++ b/src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs @@ -0,0 +1,36 @@ +using ClaudeDo.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ClaudeDo.Data.Configuration; + +public class ListEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("lists"); + + builder.HasKey(l => l.Id); + builder.Property(l => l.Id).HasColumnName("id"); + builder.Property(l => l.Name).HasColumnName("name").IsRequired(); + builder.Property(l => l.CreatedAt).HasColumnName("created_at").IsRequired(); + builder.Property(l => l.WorkingDir).HasColumnName("working_dir"); + builder.Property(l => l.DefaultCommitType).HasColumnName("default_commit_type").IsRequired().HasDefaultValue("chore"); + + builder.HasOne(l => l.Config) + .WithOne(c => c.List) + .HasForeignKey(c => c.ListId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(l => l.Tags) + .WithMany(tag => tag.Lists) + .UsingEntity("list_tags", + l => l.HasOne(typeof(TagEntity)).WithMany().HasForeignKey("tag_id").OnDelete(DeleteBehavior.Cascade), + r => r.HasOne(typeof(ListEntity)).WithMany().HasForeignKey("list_id").OnDelete(DeleteBehavior.Cascade), + j => + { + j.HasKey("list_id", "tag_id"); + j.ToTable("list_tags"); + }); + } +} diff --git a/src/ClaudeDo.Data/Configuration/SubtaskEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/SubtaskEntityConfiguration.cs new file mode 100644 index 0000000..23ac819 --- /dev/null +++ b/src/ClaudeDo.Data/Configuration/SubtaskEntityConfiguration.cs @@ -0,0 +1,28 @@ +using ClaudeDo.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ClaudeDo.Data.Configuration; + +public class SubtaskEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("subtasks"); + + builder.HasKey(s => s.Id); + builder.Property(s => s.Id).HasColumnName("id"); + builder.Property(s => s.TaskId).HasColumnName("task_id").IsRequired(); + builder.Property(s => s.Title).HasColumnName("title").IsRequired(); + builder.Property(s => s.Completed).HasColumnName("completed").IsRequired().HasDefaultValue(false); + builder.Property(s => s.OrderNum).HasColumnName("order_num").IsRequired(); + builder.Property(s => s.CreatedAt).HasColumnName("created_at").IsRequired(); + + builder.HasOne(s => s.Task) + .WithMany(t => t.Subtasks) + .HasForeignKey(s => s.TaskId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasIndex(s => s.TaskId).HasDatabaseName("idx_subtasks_task_id"); + } +} diff --git a/src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs new file mode 100644 index 0000000..066b0ec --- /dev/null +++ b/src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs @@ -0,0 +1,22 @@ +using ClaudeDo.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ClaudeDo.Data.Configuration; + +public class TagEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("tags"); + + builder.HasKey(t => t.Id); + builder.Property(t => t.Id).HasColumnName("id").ValueGeneratedOnAdd(); + builder.Property(t => t.Name).HasColumnName("name").IsRequired(); + builder.HasIndex(t => t.Name).IsUnique(); + + builder.HasData( + new TagEntity { Id = 1, Name = "agent" }, + new TagEntity { Id = 2, Name = "manual" }); + } +} diff --git a/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs new file mode 100644 index 0000000..204ae9d --- /dev/null +++ b/src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs @@ -0,0 +1,73 @@ +using ClaudeDo.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TaskStatus = ClaudeDo.Data.Models.TaskStatus; + +namespace ClaudeDo.Data.Configuration; + +public class TaskEntityConfiguration : IEntityTypeConfiguration +{ + 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( + v => v switch + { + TaskStatus.Manual => "manual", + TaskStatus.Queued => "queued", + TaskStatus.Running => "running", + TaskStatus.Done => "done", + TaskStatus.Failed => "failed", + _ => throw new ArgumentOutOfRangeException() + }, + v => v switch + { + "manual" => TaskStatus.Manual, + "queued" => TaskStatus.Queued, + "running" => TaskStatus.Running, + "done" => TaskStatus.Done, + "failed" => TaskStatus.Failed, + _ => throw new ArgumentOutOfRangeException() + }); + 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.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.HasMany(t => t.Tags) + .WithMany(tag => tag.Tasks) + .UsingEntity("task_tags", + l => l.HasOne(typeof(TagEntity)).WithMany().HasForeignKey("tag_id").OnDelete(DeleteBehavior.Cascade), + r => r.HasOne(typeof(TaskEntity)).WithMany().HasForeignKey("task_id").OnDelete(DeleteBehavior.Cascade), + j => + { + j.HasKey("task_id", "tag_id"); + j.ToTable("task_tags"); + }); + + builder.HasIndex(t => t.ListId).HasDatabaseName("idx_tasks_list_id"); + builder.HasIndex(t => t.Status).HasDatabaseName("idx_tasks_status"); + } +} diff --git a/src/ClaudeDo.Data/Configuration/TaskRunEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/TaskRunEntityConfiguration.cs new file mode 100644 index 0000000..ecad46b --- /dev/null +++ b/src/ClaudeDo.Data/Configuration/TaskRunEntityConfiguration.cs @@ -0,0 +1,38 @@ +using ClaudeDo.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ClaudeDo.Data.Configuration; + +public class TaskRunEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("task_runs"); + + builder.HasKey(r => r.Id); + builder.Property(r => r.Id).HasColumnName("id"); + builder.Property(r => r.TaskId).HasColumnName("task_id").IsRequired(); + builder.Property(r => r.RunNumber).HasColumnName("run_number").IsRequired(); + builder.Property(r => r.SessionId).HasColumnName("session_id"); + builder.Property(r => r.IsRetry).HasColumnName("is_retry").IsRequired().HasDefaultValue(false); + builder.Property(r => r.Prompt).HasColumnName("prompt").IsRequired(); + builder.Property(r => r.ResultMarkdown).HasColumnName("result_markdown"); + builder.Property(r => r.StructuredOutputJson).HasColumnName("structured_output"); + builder.Property(r => r.ErrorMarkdown).HasColumnName("error_markdown"); + builder.Property(r => r.ExitCode).HasColumnName("exit_code"); + builder.Property(r => r.TurnCount).HasColumnName("turn_count"); + builder.Property(r => r.TokensIn).HasColumnName("tokens_in"); + builder.Property(r => r.TokensOut).HasColumnName("tokens_out"); + builder.Property(r => r.LogPath).HasColumnName("log_path"); + builder.Property(r => r.StartedAt).HasColumnName("started_at"); + builder.Property(r => r.FinishedAt).HasColumnName("finished_at"); + + builder.HasOne(r => r.Task) + .WithMany(t => t.Runs) + .HasForeignKey(r => r.TaskId) + .OnDelete(DeleteBehavior.Cascade); + + builder.HasIndex(r => r.TaskId).HasDatabaseName("idx_task_runs_task_id"); + } +} diff --git a/src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs b/src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs new file mode 100644 index 0000000..e463f0f --- /dev/null +++ b/src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs @@ -0,0 +1,41 @@ +using ClaudeDo.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ClaudeDo.Data.Configuration; + +public class WorktreeEntityConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("worktrees"); + + builder.HasKey(w => w.TaskId); + builder.Property(w => w.TaskId).HasColumnName("task_id"); + builder.Property(w => w.Path).HasColumnName("path").IsRequired(); + builder.Property(w => w.BranchName).HasColumnName("branch_name").IsRequired(); + builder.Property(w => w.BaseCommit).HasColumnName("base_commit").IsRequired(); + builder.Property(w => w.HeadCommit).HasColumnName("head_commit"); + builder.Property(w => w.DiffStat).HasColumnName("diff_stat"); + builder.Property(w => w.State).HasColumnName("state").IsRequired() + .HasDefaultValue(WorktreeState.Active) + .HasConversion( + v => v switch + { + WorktreeState.Active => "active", + WorktreeState.Merged => "merged", + WorktreeState.Discarded => "discarded", + WorktreeState.Kept => "kept", + _ => throw new ArgumentOutOfRangeException() + }, + v => v switch + { + "active" => WorktreeState.Active, + "merged" => WorktreeState.Merged, + "discarded" => WorktreeState.Discarded, + "kept" => WorktreeState.Kept, + _ => throw new ArgumentOutOfRangeException() + }); + builder.Property(w => w.CreatedAt).HasColumnName("created_at").IsRequired(); + } +}