feat(backend): Add AuditLog database schema and migration
Implement AuditLog entity and EF Core configuration for Sprint 2 Story 1 Task 1. Changes: - Created AuditLog entity with multi-tenant support - Added EF Core configuration with JSONB columns for PostgreSQL - Created composite indexes for query optimization - Generated database migration (20251104220842_AddAuditLogTable) - Updated PMDbContext with AuditLog DbSet and query filter - Updated task status to in_progress in sprint plan Technical Details: - PostgreSQL JSONB type for OldValues/NewValues (flexible schema) - Composite index on (TenantId, EntityType, EntityId) for entity history queries - Timestamp index (DESC) for recent logs queries - UserId index for user activity tracking - Multi-tenant query filter applied to AuditLog 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
using ColaFlow.Shared.Kernel.Common;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
|
|
||||||
|
namespace ColaFlow.Modules.ProjectManagement.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AuditLog entity for tracking changes to entities (Create/Update/Delete operations)
|
||||||
|
/// Supports multi-tenant isolation and stores old/new values as JSON
|
||||||
|
/// </summary>
|
||||||
|
public class AuditLog : Entity
|
||||||
|
{
|
||||||
|
public TenantId TenantId { get; private set; }
|
||||||
|
public string EntityType { get; private set; }
|
||||||
|
public Guid EntityId { get; private set; }
|
||||||
|
public string Action { get; private set; } // "Create", "Update", "Delete"
|
||||||
|
public UserId? UserId { get; private set; }
|
||||||
|
public DateTime Timestamp { get; private set; }
|
||||||
|
public string? OldValues { get; private set; } // JSONB - serialized old values
|
||||||
|
public string? NewValues { get; private set; } // JSONB - serialized new values
|
||||||
|
|
||||||
|
// EF Core constructor
|
||||||
|
private AuditLog()
|
||||||
|
{
|
||||||
|
TenantId = null!;
|
||||||
|
EntityType = null!;
|
||||||
|
Action = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory method to create a new AuditLog entry
|
||||||
|
/// </summary>
|
||||||
|
public static AuditLog Create(
|
||||||
|
TenantId tenantId,
|
||||||
|
string entityType,
|
||||||
|
Guid entityId,
|
||||||
|
string action,
|
||||||
|
UserId? userId,
|
||||||
|
string? oldValues,
|
||||||
|
string? newValues)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(entityType))
|
||||||
|
throw new ArgumentException("Entity type cannot be empty", nameof(entityType));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(action))
|
||||||
|
throw new ArgumentException("Action cannot be empty", nameof(action));
|
||||||
|
|
||||||
|
return new AuditLog
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TenantId = tenantId,
|
||||||
|
EntityType = entityType,
|
||||||
|
EntityId = entityId,
|
||||||
|
Action = action,
|
||||||
|
UserId = userId,
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
OldValues = oldValues,
|
||||||
|
NewValues = newValues
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
// <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("20251104220842_AddAuditLogTable")]
|
||||||
|
partial class AddAuditLogTable
|
||||||
|
{
|
||||||
|
/// <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.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,62 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAuditLogTable : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AuditLogs",
|
||||||
|
schema: "project_management",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
EntityType = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
EntityId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Action = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
Timestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
OldValues = table.Column<string>(type: "jsonb", nullable: true),
|
||||||
|
NewValues = table.Column<string>(type: "jsonb", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AuditLogs", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AuditLogs_TenantId_EntityType_EntityId",
|
||||||
|
schema: "project_management",
|
||||||
|
table: "AuditLogs",
|
||||||
|
columns: new[] { "TenantId", "EntityType", "EntityId" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AuditLogs_Timestamp",
|
||||||
|
schema: "project_management",
|
||||||
|
table: "AuditLogs",
|
||||||
|
column: "Timestamp",
|
||||||
|
descending: new bool[0]);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AuditLogs_UserId",
|
||||||
|
schema: "project_management",
|
||||||
|
table: "AuditLogs",
|
||||||
|
column: "UserId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AuditLogs",
|
||||||
|
schema: "project_management");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -247,6 +247,54 @@ namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Migrations
|
|||||||
b.ToTable("Tasks", "project_management");
|
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 =>
|
modelBuilder.Entity("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Epic", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null)
|
b.HasOne("ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate.Project", null)
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Domain.Entities;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
|
|
||||||
|
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.Configurations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entity configuration for AuditLog
|
||||||
|
/// Configures multi-tenant isolation, JSONB columns, and composite indexes
|
||||||
|
/// </summary>
|
||||||
|
public class AuditLogConfiguration : IEntityTypeConfiguration<AuditLog>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<AuditLog> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("AuditLogs");
|
||||||
|
|
||||||
|
// Primary key
|
||||||
|
builder.HasKey(a => a.Id);
|
||||||
|
|
||||||
|
builder.Property(a => a.Id)
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedNever();
|
||||||
|
|
||||||
|
// TenantId conversion (StronglyTypedId to Guid)
|
||||||
|
builder.Property(a => a.TenantId)
|
||||||
|
.HasConversion(
|
||||||
|
id => id.Value,
|
||||||
|
value => TenantId.From(value))
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// EntityType - the type name of the entity being audited
|
||||||
|
builder.Property(a => a.EntityType)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
// EntityId - the ID of the entity being audited
|
||||||
|
builder.Property(a => a.EntityId)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// Action - Create, Update, Delete
|
||||||
|
builder.Property(a => a.Action)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20);
|
||||||
|
|
||||||
|
// UserId conversion (nullable StronglyTypedId to Guid)
|
||||||
|
builder.Property(a => a.UserId)
|
||||||
|
.HasConversion(
|
||||||
|
id => id != null ? id.Value : (Guid?)null,
|
||||||
|
value => value.HasValue ? UserId.From(value.Value) : null);
|
||||||
|
|
||||||
|
// Timestamp with UTC
|
||||||
|
builder.Property(a => a.Timestamp)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// OldValues as JSONB (PostgreSQL-specific)
|
||||||
|
builder.Property(a => a.OldValues)
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
// NewValues as JSONB (PostgreSQL-specific)
|
||||||
|
builder.Property(a => a.NewValues)
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
// Composite index for efficient entity history queries
|
||||||
|
// Query pattern: Get all audit logs for a specific entity within a tenant
|
||||||
|
builder.HasIndex(a => new { a.TenantId, a.EntityType, a.EntityId })
|
||||||
|
.HasDatabaseName("IX_AuditLogs_TenantId_EntityType_EntityId");
|
||||||
|
|
||||||
|
// Index for recent logs queries (DESC order for performance)
|
||||||
|
// Query pattern: Get most recent audit logs across all entities
|
||||||
|
builder.HasIndex(a => a.Timestamp)
|
||||||
|
.HasDatabaseName("IX_AuditLogs_Timestamp")
|
||||||
|
.IsDescending();
|
||||||
|
|
||||||
|
// Index for user activity tracking
|
||||||
|
// Query pattern: Get all actions performed by a specific user
|
||||||
|
builder.HasIndex(a => a.UserId)
|
||||||
|
.HasDatabaseName("IX_AuditLogs_UserId");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System.Reflection;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
||||||
|
using ColaFlow.Modules.ProjectManagement.Domain.Entities;
|
||||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||||
|
|
||||||
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
namespace ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
|
||||||
@@ -23,6 +24,7 @@ public class PMDbContext : DbContext
|
|||||||
public DbSet<Epic> Epics => Set<Epic>();
|
public DbSet<Epic> Epics => Set<Epic>();
|
||||||
public DbSet<Story> Stories => Set<Story>();
|
public DbSet<Story> Stories => Set<Story>();
|
||||||
public DbSet<WorkTask> Tasks => Set<WorkTask>();
|
public DbSet<WorkTask> Tasks => Set<WorkTask>();
|
||||||
|
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -46,6 +48,9 @@ public class PMDbContext : DbContext
|
|||||||
|
|
||||||
modelBuilder.Entity<WorkTask>().HasQueryFilter(t =>
|
modelBuilder.Entity<WorkTask>().HasQueryFilter(t =>
|
||||||
t.TenantId == GetCurrentTenantId());
|
t.TenantId == GetCurrentTenantId());
|
||||||
|
|
||||||
|
modelBuilder.Entity<AuditLog>().HasQueryFilter(a =>
|
||||||
|
a.TenantId == GetCurrentTenantId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private TenantId GetCurrentTenantId()
|
private TenantId GetCurrentTenantId()
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
task_id: sprint_2_story_1_task_1
|
task_id: sprint_2_story_1_task_1
|
||||||
story: sprint_2_story_1
|
story: sprint_2_story_1
|
||||||
status: not_started
|
status: in_progress
|
||||||
estimated_hours: 4
|
estimated_hours: 4
|
||||||
created_date: 2025-11-05
|
created_date: 2025-11-05
|
||||||
|
start_date: 2025-11-05
|
||||||
assignee: Backend Team
|
assignee: Backend Team
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user