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>
This commit is contained in:
191
docs/plans/sprint_2_story_2_task_1.md
Normal file
191
docs/plans/sprint_2_story_2_task_1.md
Normal file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
task_id: sprint_2_story_2_task_1
|
||||
story: sprint_2_story_2
|
||||
status: not_started
|
||||
estimated_hours: 6
|
||||
created_date: 2025-11-05
|
||||
assignee: 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`
|
||||
```csharp
|
||||
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; }
|
||||
}
|
||||
```
|
||||
|
||||
2. **Update Interceptor**: `colaflow-api/src/ColaFlow.Infrastructure/Interceptors/AuditLogInterceptor.cs`
|
||||
```csharp
|
||||
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**:
|
||||
```json
|
||||
// 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**:
|
||||
```csharp
|
||||
[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
|
||||
Reference in New Issue
Block a user