feat(backend): Implement Sprint Repository and EF Core Configuration (Task 2)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -108,4 +108,26 @@ public interface IProjectRepository
|
||||
/// Gets project with all epics/stories/tasks hierarchy (read-only, AsNoTracking)
|
||||
/// </summary>
|
||||
Task<Project?> GetProjectWithFullHierarchyReadOnlyAsync(ProjectId projectId, CancellationToken cancellationToken = default);
|
||||
|
||||
// ========== Sprint Operations ==========
|
||||
|
||||
/// <summary>
|
||||
/// Gets project containing specific sprint (with tracking, for modification)
|
||||
/// </summary>
|
||||
Task<Project?> GetProjectWithSprintAsync(SprintId sprintId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets sprint by ID (read-only, AsNoTracking)
|
||||
/// </summary>
|
||||
Task<Sprint?> GetSprintByIdReadOnlyAsync(SprintId sprintId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all sprints for a project (read-only, AsNoTracking)
|
||||
/// </summary>
|
||||
Task<List<Sprint>> GetSprintsByProjectIdAsync(ProjectId projectId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all active sprints across all projects (read-only, AsNoTracking)
|
||||
/// </summary>
|
||||
Task<List<Sprint>> GetActiveSprintsAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<DateTime?>("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<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("character varying(2000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("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<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Goal")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("_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<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ActualHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<Guid>("EpicId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("EstimatedHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("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<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<decimal?>("ActualHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<Guid?>("AssigneeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<decimal?>("EstimatedHours")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Priority")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<Guid>("StoryId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTime?>("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<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("EntityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("EntityType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("NewValues")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("OldValues")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("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<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSprintEntity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Sprints",
|
||||
schema: "project_management",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Goal = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
StartDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
EndDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
TaskIds = table.Column<string>(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" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Sprints",
|
||||
schema: "project_management");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Guid>("Id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("CreatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Goal")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("_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<Guid>("Id")
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// EF Core configuration for Sprint entity
|
||||
/// </summary>
|
||||
public class SprintConfiguration : IEntityTypeConfiguration<Sprint>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Sprint> 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<List<TaskId>>("_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<List<Guid>>(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");
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ public class PMDbContext : DbContext
|
||||
public DbSet<Epic> Epics => Set<Epic>();
|
||||
public DbSet<Story> Stories => Set<Story>();
|
||||
public DbSet<WorkTask> Tasks => Set<WorkTask>();
|
||||
public DbSet<Sprint> Sprints => Set<Sprint>();
|
||||
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
@@ -49,6 +50,9 @@ public class PMDbContext : DbContext
|
||||
modelBuilder.Entity<WorkTask>().HasQueryFilter(t =>
|
||||
t.TenantId == GetCurrentTenantId());
|
||||
|
||||
modelBuilder.Entity<Sprint>().HasQueryFilter(s =>
|
||||
s.TenantId == GetCurrentTenantId());
|
||||
|
||||
modelBuilder.Entity<AuditLog>().HasQueryFilter(a =>
|
||||
a.TenantId == GetCurrentTenantId());
|
||||
}
|
||||
|
||||
@@ -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<Project?> GetProjectWithSprintAsync(SprintId sprintId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Find the project containing the sprint
|
||||
var sprint = await _context.Set<Sprint>()
|
||||
.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<Sprint?> GetSprintByIdReadOnlyAsync(SprintId sprintId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Set<Sprint>()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(s => s.Id == sprintId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Sprint>> GetSprintsByProjectIdAsync(ProjectId projectId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Set<Sprint>()
|
||||
.AsNoTracking()
|
||||
.Where(s => s.ProjectId == projectId)
|
||||
.OrderByDescending(s => s.StartDate)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Sprint>> GetActiveSprintsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.Set<Sprint>()
|
||||
.AsNoTracking()
|
||||
.Where(s => s.Status.Name == "Active")
|
||||
.OrderBy(s => s.StartDate)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user