Files
ColaFlow/docs/plans/sprint_2_story_2_task_1.md
Yaojia Wang ebb56cc9f8 feat(backend): Create Sprint 2 backend Stories and Tasks
Created detailed implementation plans for Sprint 2 backend work:

Story 1: Audit Log Foundation (Phase 1)
- Task 1: Design AuditLog database schema and create migration
- Task 2: Create AuditLog entity and Repository
- Task 3: Implement EF Core SaveChangesInterceptor
- Task 4: Write unit tests for audit logging
- Task 5: Integrate with ProjectManagement Module

Story 2: Audit Log Core Features (Phase 2)
- Task 1: Implement Changed Fields Detection (JSON Diff)
- Task 2: Integrate User Context Tracking
- Task 3: Add Multi-Tenant Isolation
- Task 4: Implement Audit Query API
- Task 5: Write Integration Tests

Story 3: Sprint Management Module
- Task 1: Create Sprint Aggregate Root and Domain Events
- Task 2: Implement Sprint Repository and EF Core Configuration
- Task 3: Create CQRS Commands and Queries
- Task 4: Implement Burndown Chart Calculation
- Task 5: Add SignalR Real-Time Notifications
- Task 6: Write Integration Tests

Total: 3 Stories, 16 Tasks, 24 Story Points (8+8+8)
Estimated Duration: 10-12 days

All tasks include:
- Detailed technical implementation guidance
- Code examples and file paths
- Testing requirements (>= 90% coverage)
- Performance benchmarks (< 5ms audit overhead)
- Multi-tenant security validation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 22:56:31 +01:00

5.4 KiB

task_id, story, status, estimated_hours, created_date, assignee
task_id story status estimated_hours created_date assignee
sprint_2_story_2_task_1 sprint_2_story_2 not_started 6 2025-11-05 Backend Team

Task 1: Implement Changed Fields Detection (JSON Diff)

Story: Story 2 - Audit Log Core Features (Phase 2) Estimated: 6 hours

Description

Enhance the audit logging to detect and store only the changed fields (JSON diff) instead of full entity snapshots. This optimizes storage and makes audit logs more readable.

Acceptance Criteria

  • JSON diff algorithm implemented
  • Only changed fields stored in OldValues/NewValues
  • Nested object changes detected
  • Unit tests for diff algorithm
  • Storage size reduced by 50-70%

Implementation Details

Files to Update:

  1. Diff Service: colaflow-api/src/ColaFlow.Infrastructure/Services/JsonDiffService.cs
public class JsonDiffService : IJsonDiffService
{
    public ChangedFields GetChangedFields(EntityEntry entry)
    {
        var changedFields = new Dictionary<string, FieldChange>();

        foreach (var property in entry.Properties.Where(p => p.IsModified && !p.Metadata.IsPrimaryKey()))
        {
            changedFields[property.Metadata.Name] = new FieldChange
            {
                OldValue = property.OriginalValue,
                NewValue = property.CurrentValue
            };
        }

        return new ChangedFields { Fields = changedFields };
    }

    public string SerializeChangedFields(ChangedFields changes)
    {
        return JsonSerializer.Serialize(changes, new JsonSerializerOptions
        {
            WriteIndented = false,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        });
    }
}

public class ChangedFields
{
    public Dictionary<string, FieldChange> Fields { get; set; } = new();
}

public class FieldChange
{
    public object? OldValue { get; set; }
    public object? NewValue { get; set; }
}
  1. Update Interceptor: colaflow-api/src/ColaFlow.Infrastructure/Interceptors/AuditLogInterceptor.cs
public class AuditLogInterceptor : SaveChangesInterceptor
{
    private readonly IJsonDiffService _diffService;

    public AuditLogInterceptor(
        ITenantContext tenantContext,
        IHttpContextAccessor httpContextAccessor,
        IJsonDiffService diffService)
    {
        _tenantContext = tenantContext;
        _httpContextAccessor = httpContextAccessor;
        _diffService = diffService;
    }

    private List<AuditLog> CreateAuditEntries(DbContext context)
    {
        // ... existing code ...

        foreach (var entry in entries)
        {
            string? oldValues = null;
            string? newValues = null;

            if (entry.State == EntityState.Modified)
            {
                // Use diff service to get only changed fields
                var changes = _diffService.GetChangedFields(entry);
                var diffJson = _diffService.SerializeChangedFields(changes);

                // Store diff in both OldValues and NewValues
                // OldValues: { "Title": { "OldValue": "Old", "NewValue": "New" } }
                oldValues = diffJson;
                newValues = diffJson;
            }
            else if (entry.State == EntityState.Added)
            {
                // For Create, store all current values
                newValues = SerializeAllFields(entry);
            }
            else if (entry.State == EntityState.Deleted)
            {
                // For Delete, store all original values
                oldValues = SerializeAllFields(entry);
            }

            var auditLog = new AuditLog
            {
                // ... existing fields ...
                OldValues = oldValues,
                NewValues = newValues
            };

            auditLogs.Add(auditLog);
        }

        return auditLogs;
    }

    private string SerializeAllFields(EntityEntry entry)
    {
        var allFields = entry.Properties
            .Where(p => !p.Metadata.IsPrimaryKey())
            .ToDictionary(p => p.Metadata.Name, p => p.CurrentValue);
        return JsonSerializer.Serialize(allFields);
    }
}

Example Output:

// Before (Full Snapshot - 500 bytes):
{
  "OldValues": {"Id":"abc","Title":"Old Title","Description":"Long description...","Status":"InProgress","Priority":1},
  "NewValues": {"Id":"abc","Title":"New Title","Description":"Long description...","Status":"InProgress","Priority":1}
}

// After (Diff Only - 80 bytes, 84% reduction):
{
  "OldValues": {"Title":{"OldValue":"Old Title","NewValue":"New Title"}},
  "NewValues": {"Title":{"OldValue":"Old Title","NewValue":"New Title"}}
}

Technical Notes

  • Use System.Text.Json for performance
  • Store diff in both OldValues and NewValues for query flexibility
  • Consider nested object changes (e.g., Address.City)
  • Ignore computed properties and navigation properties

Testing

Unit Tests:

[Fact]
public void GetChangedFields_ShouldReturnOnlyModifiedFields()
{
    // Arrange
    var entry = CreateMockEntry(
        original: new { Title = "Old", Status = "InProgress" },
        current: new { Title = "New", Status = "InProgress" }
    );

    // Act
    var changes = _diffService.GetChangedFields(entry);

    // Assert
    Assert.Single(changes.Fields);
    Assert.True(changes.Fields.ContainsKey("Title"));
    Assert.Equal("Old", changes.Fields["Title"].OldValue);
    Assert.Equal("New", changes.Fields["Title"].NewValue);
}

Created: 2025-11-05 by Backend Agent