From ee73d56759d6bcb5248b8a650feca66cd07e81ca Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Wed, 5 Nov 2025 00:10:57 +0100 Subject: [PATCH] feat(backend): Implement Sprint Repository and EF Core Configuration (Task 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented complete Sprint data access layer: - Extended IProjectRepository with Sprint operations - Created SprintConfiguration for EF Core mapping - Added Sprint DbSet and multi-tenant query filter to PMDbContext - Implemented 4 Sprint repository methods (Get, GetByProject, GetActive, GetProjectWithSprint) - Created EF Core migration for Sprints table with JSONB TaskIds column - Multi-tenant isolation enforced via Global Query Filter Database schema: - Sprints table with indexes on (TenantId, ProjectId), (TenantId, Status), StartDate, EndDate - TaskIds stored as JSONB array for performance Story 3 Task 2/6 completed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../Repositories/IProjectRepository.cs | 22 + ...20251104231026_AddSprintEntity.Designer.cs | 435 ++++++++++++++++++ .../20251104231026_AddSprintEntity.cs | 70 +++ .../Migrations/PMDbContextModelSnapshot.cs | 62 +++ .../Configurations/SprintConfiguration.cs | 97 ++++ .../Persistence/PMDbContext.cs | 4 + .../Repositories/ProjectRepository.cs | 41 ++ 7 files changed, 731 insertions(+) create mode 100644 colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104231026_AddSprintEntity.Designer.cs create mode 100644 colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104231026_AddSprintEntity.cs create mode 100644 colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/SprintConfiguration.cs diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Repositories/IProjectRepository.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Repositories/IProjectRepository.cs index a768c84..f6bd650 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Repositories/IProjectRepository.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Domain/Repositories/IProjectRepository.cs @@ -108,4 +108,26 @@ public interface IProjectRepository /// Gets project with all epics/stories/tasks hierarchy (read-only, AsNoTracking) /// Task GetProjectWithFullHierarchyReadOnlyAsync(ProjectId projectId, CancellationToken cancellationToken = default); + + // ========== Sprint Operations ========== + + /// + /// Gets project containing specific sprint (with tracking, for modification) + /// + Task GetProjectWithSprintAsync(SprintId sprintId, CancellationToken cancellationToken = default); + + /// + /// Gets sprint by ID (read-only, AsNoTracking) + /// + Task GetSprintByIdReadOnlyAsync(SprintId sprintId, CancellationToken cancellationToken = default); + + /// + /// Gets all sprints for a project (read-only, AsNoTracking) + /// + Task> GetSprintsByProjectIdAsync(ProjectId projectId, CancellationToken cancellationToken = default); + + /// + /// Gets all active sprints across all projects (read-only, AsNoTracking) + /// + Task> GetActiveSprintsAsync(CancellationToken cancellationToken = default); } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104231026_AddSprintEntity.Designer.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104231026_AddSprintEntity.Designer.cs new file mode 100644 index 0000000..219d28a --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104231026_AddSprintEntity.Designer.cs @@ -0,0 +1,435 @@ +// +using System; +using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations +{ + [DbContext(typeof(PMDbContext))] + [Migration("20251104231026_AddSprintEntity")] + partial class AddSprintEntity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("project_management") + .HasAnnotation("ProductVersion", "9.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_epics_tenant_id"); + + b.ToTable("Epics", "project_management"); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("OwnerId"); + + b.HasIndex("TenantId"); + + b.ToTable("Projects", "project_management"); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Sprint", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Goal") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("_taskIds") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("TaskIds"); + + b.HasKey("Id"); + + b.HasIndex("EndDate") + .HasDatabaseName("IX_Sprints_EndDate"); + + b.HasIndex("StartDate") + .HasDatabaseName("IX_Sprints_StartDate"); + + b.HasIndex("TenantId", "ProjectId") + .HasDatabaseName("IX_Sprints_TenantId_ProjectId"); + + b.HasIndex("TenantId", "Status") + .HasDatabaseName("IX_Sprints_TenantId_Status"); + + b.ToTable("Sprints", "project_management"); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActualHours") + .HasColumnType("numeric"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("EpicId") + .HasColumnType("uuid"); + + b.Property("EstimatedHours") + .HasColumnType("numeric"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("EpicId"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_stories_tenant_id"); + + b.ToTable("Stories", "project_management"); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActualHours") + .HasColumnType("numeric"); + + b.Property("AssigneeId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("EstimatedHours") + .HasColumnType("numeric"); + + b.Property("Priority") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StoryId") + .HasColumnType("uuid"); + + b.Property("TenantId") + .HasColumnType("uuid") + .HasColumnName("tenant_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AssigneeId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("StoryId"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_tasks_tenant_id"); + + b.ToTable("Tasks", "project_management"); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("EntityId") + .HasColumnType("uuid"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NewValues") + .HasColumnType("jsonb"); + + b.Property("OldValues") + .HasColumnType("jsonb"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Timestamp") + .IsDescending() + .HasDatabaseName("IX_AuditLogs_Timestamp"); + + b.HasIndex("UserId") + .HasDatabaseName("IX_AuditLogs_UserId"); + + b.HasIndex("TenantId", "EntityType", "EntityId") + .HasDatabaseName("IX_AuditLogs_TenantId_EntityType_EntityId"); + + b.ToTable("AuditLogs", "project_management"); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b => + { + b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null) + .WithMany("Epics") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b => + { + b.OwnsOne("ColaFlow.Modules.ProjectManagement.Domain.ValueObjects.ProjectKey", "Key", b1 => + { + b1.Property("ProjectId") + .HasColumnType("uuid"); + + b1.Property("Value") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("Key"); + + b1.HasKey("ProjectId"); + + b1.HasIndex("Value") + .IsUnique(); + + b1.ToTable("Projects", "project_management"); + + b1.WithOwner() + .HasForeignKey("ProjectId"); + }); + + b.Navigation("Key") + .IsRequired(); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b => + { + b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", null) + .WithMany("Stories") + .HasForeignKey("EpicId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.WorkTask", b => + { + b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", null) + .WithMany("Tasks") + .HasForeignKey("StoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b => + { + b.Navigation("Stories"); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", b => + { + b.Navigation("Epics"); + }); + + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b => + { + b.Navigation("Tasks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104231026_AddSprintEntity.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104231026_AddSprintEntity.cs new file mode 100644 index 0000000..8d1e681 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/20251104231026_AddSprintEntity.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations +{ + /// + public partial class AddSprintEntity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Sprints", + schema: "project_management", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TenantId = table.Column(type: "uuid", nullable: false), + ProjectId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + Goal = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + StartDate = table.Column(type: "timestamp with time zone", nullable: false), + EndDate = table.Column(type: "timestamp with time zone", nullable: false), + Status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true), + TaskIds = table.Column(type: "jsonb", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Sprints", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Sprints_EndDate", + schema: "project_management", + table: "Sprints", + column: "EndDate"); + + migrationBuilder.CreateIndex( + name: "IX_Sprints_StartDate", + schema: "project_management", + table: "Sprints", + column: "StartDate"); + + migrationBuilder.CreateIndex( + name: "IX_Sprints_TenantId_ProjectId", + schema: "project_management", + table: "Sprints", + columns: new[] { "TenantId", "ProjectId" }); + + migrationBuilder.CreateIndex( + name: "IX_Sprints_TenantId_Status", + schema: "project_management", + table: "Sprints", + columns: new[] { "TenantId", "Status" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Sprints", + schema: "project_management"); + } + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/PMDbContextModelSnapshot.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/PMDbContextModelSnapshot.cs index c76ed88..3bb29f3 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/PMDbContextModelSnapshot.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Migrations/PMDbContextModelSnapshot.cs @@ -119,6 +119,68 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations b.ToTable("Projects", "project_management"); }); + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Sprint", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Goal") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("_taskIds") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("TaskIds"); + + b.HasKey("Id"); + + b.HasIndex("EndDate") + .HasDatabaseName("IX_Sprints_EndDate"); + + b.HasIndex("StartDate") + .HasDatabaseName("IX_Sprints_StartDate"); + + b.HasIndex("TenantId", "ProjectId") + .HasDatabaseName("IX_Sprints_TenantId_ProjectId"); + + b.HasIndex("TenantId", "Status") + .HasDatabaseName("IX_Sprints_TenantId_Status"); + + b.ToTable("Sprints", "project_management"); + }); + modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Story", b => { b.Property("Id") diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/SprintConfiguration.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/SprintConfiguration.cs new file mode 100644 index 0000000..8d10a44 --- /dev/null +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/Configurations/SprintConfiguration.cs @@ -0,0 +1,97 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate; +using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; +using ColaFlow.Modules.ProjectManagement.Domain.Enums; + +namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.Configurations; + +/// +/// EF Core configuration for Sprint entity +/// +public class SprintConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Sprints"); + + // Primary Key + builder.HasKey(s => s.Id); + + // Value Objects + builder.Property(s => s.Id) + .HasConversion( + id => id.Value, + value => SprintId.From(value)) + .ValueGeneratedNever(); + + builder.Property(s => s.TenantId) + .HasConversion( + id => id.Value, + value => TenantId.From(value)) + .IsRequired(); + + builder.Property(s => s.ProjectId) + .HasConversion( + id => id.Value, + value => ProjectId.From(value)) + .IsRequired(); + + builder.Property(s => s.CreatedBy) + .HasConversion( + id => id.Value, + value => UserId.From(value)) + .IsRequired(); + + // Properties + builder.Property(s => s.Name) + .IsRequired() + .HasMaxLength(200); + + builder.Property(s => s.Goal) + .HasMaxLength(1000); + + builder.Property(s => s.StartDate) + .IsRequired(); + + builder.Property(s => s.EndDate) + .IsRequired(); + + builder.Property(s => s.Status) + .IsRequired() + .HasConversion( + status => status.Name, + name => SprintStatus.FromString(name)) + .HasMaxLength(20); + + builder.Property(s => s.CreatedAt) + .IsRequired(); + + builder.Property(s => s.UpdatedAt); + + // TaskIds as JSON column (PostgreSQL JSONB) + builder.Property>("_taskIds") + .HasColumnName("TaskIds") + .HasColumnType("jsonb") + .HasConversion( + taskIds => System.Text.Json.JsonSerializer.Serialize(taskIds.Select(t => t.Value).ToList(), (System.Text.Json.JsonSerializerOptions?)null), + json => System.Text.Json.JsonSerializer.Deserialize>(json, (System.Text.Json.JsonSerializerOptions?)null)! + .Select(id => TaskId.From(id)).ToList()); + + // Ignore read-only collection + builder.Ignore(s => s.TaskIds); + + // Indexes for performance + builder.HasIndex(s => new { s.TenantId, s.ProjectId }) + .HasDatabaseName("IX_Sprints_TenantId_ProjectId"); + + builder.HasIndex(s => new { s.TenantId, s.Status }) + .HasDatabaseName("IX_Sprints_TenantId_Status"); + + builder.HasIndex(s => s.StartDate) + .HasDatabaseName("IX_Sprints_StartDate"); + + builder.HasIndex(s => s.EndDate) + .HasDatabaseName("IX_Sprints_EndDate"); + } +} diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs index 192eeeb..01100a9 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Persistence/PMDbContext.cs @@ -24,6 +24,7 @@ public class PMDbContext : DbContext public DbSet Epics => Set(); public DbSet Stories => Set(); public DbSet Tasks => Set(); + public DbSet Sprints => Set(); public DbSet AuditLogs => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -49,6 +50,9 @@ public class PMDbContext : DbContext modelBuilder.Entity().HasQueryFilter(t => t.TenantId == GetCurrentTenantId()); + modelBuilder.Entity().HasQueryFilter(s => + s.TenantId == GetCurrentTenantId()); + modelBuilder.Entity().HasQueryFilter(a => a.TenantId == GetCurrentTenantId()); } diff --git a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/ProjectRepository.cs b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/ProjectRepository.cs index d162976..2c68513 100644 --- a/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/ProjectRepository.cs +++ b/colaflow-api/src/Modules/ProjectManagement/ColaFlow.Modules.ProjectManagement.Infrastructure/Repositories/ProjectRepository.cs @@ -171,4 +171,45 @@ public class ProjectRepository(PMDbContext context) : IProjectRepository .ThenInclude(s => s.Tasks) .FirstOrDefaultAsync(p => p.Id == projectId, cancellationToken); } + + // ========== Sprint Operations ========== + + public async Task GetProjectWithSprintAsync(SprintId sprintId, CancellationToken cancellationToken = default) + { + // Find the project containing the sprint + var sprint = await _context.Set() + .FirstOrDefaultAsync(s => s.Id == sprintId, cancellationToken); + + if (sprint == null) + return null; + + // Load the project (sprint is part of project aggregate but stored separately) + return await _context.Projects + .FirstOrDefaultAsync(p => p.Id == sprint.ProjectId, cancellationToken); + } + + public async Task GetSprintByIdReadOnlyAsync(SprintId sprintId, CancellationToken cancellationToken = default) + { + return await _context.Set() + .AsNoTracking() + .FirstOrDefaultAsync(s => s.Id == sprintId, cancellationToken); + } + + public async Task> GetSprintsByProjectIdAsync(ProjectId projectId, CancellationToken cancellationToken = default) + { + return await _context.Set() + .AsNoTracking() + .Where(s => s.ProjectId == projectId) + .OrderByDescending(s => s.StartDate) + .ToListAsync(cancellationToken); + } + + public async Task> GetActiveSprintsAsync(CancellationToken cancellationToken = default) + { + return await _context.Set() + .AsNoTracking() + .Where(s => s.Status.Name == "Active") + .OrderBy(s => s.StartDate) + .ToListAsync(cancellationToken); + } }