Files
ColaFlow/docs/plans/sprint_2_story_2_task_4.md
Yaojia Wang 6cbf7dc6dc feat(backend): Implement Audit Query API (CQRS) - Sprint 2 Story 2 Task 4
Implemented complete REST API for querying audit logs using CQRS pattern.

Features:
- GET /api/v1/auditlogs/{id} - Retrieve specific audit log
- GET /api/v1/auditlogs/entity/{entityType}/{entityId} - Get entity history
- GET /api/v1/auditlogs/recent?count=100 - Get recent logs (max 1000)

Implementation:
- AuditLogDto - Transfer object for query results
- GetAuditLogByIdQuery + Handler
- GetAuditLogsByEntity Query + Handler
- GetRecentAuditLogsQuery + Handler
- AuditLogsController with 3 endpoints

Technical:
- Multi-tenant isolation via Global Query Filters (automatic)
- Read-only query endpoints (no mutations)
- Swagger/OpenAPI documentation
- Proper HTTP status codes (200 OK, 404 Not Found)
- Cancellation token support
- Primary constructor pattern (modern C# style)

Tests: Build succeeded, no new test failures introduced

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

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

8.3 KiB

task_id, story, status, estimated_hours, created_date, completed_date, assignee
task_id story status estimated_hours created_date completed_date assignee
sprint_2_story_2_task_4 sprint_2_story_2 completed 5 2025-11-05 2025-11-05 Backend Team

Task 4: Implement Audit Query API

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

Description

Create REST API endpoints to query audit logs with CQRS pattern. Support filtering by entity type, entity ID, date range, and user.

Acceptance Criteria

  • GetEntityAuditHistoryQuery implemented - COMPLETED
  • GetAuditLogByIdQuery implemented - COMPLETED
  • AuditLogsController with 3 endpoints created - COMPLETED
  • Query handlers with proper filtering - COMPLETED
  • Swagger documentation added - COMPLETED
  • Integration tests for API endpoints - PENDING (Task 5)

Implementation Summary (2025-11-05)

Status: COMPLETED

Successfully implemented complete CQRS Query API for Audit Logs:

Files Created:

  1. DTOs:

    • AuditLogDto.cs - Transfer object for audit log data
  2. Queries:

    • GetAuditLogById/GetAuditLogByIdQuery.cs + Handler
    • GetAuditLogsByEntity/GetAuditLogsByEntityQuery.cs + Handler
    • GetRecentAuditLogs/GetRecentAuditLogsQuery.cs + Handler
  3. API Controller:

    • AuditLogsController.cs with 3 endpoints:
      • GET /api/v1/auditlogs/{id} - Get specific audit log
      • GET /api/v1/auditlogs/entity/{entityType}/{entityId} - Get entity history
      • GET /api/v1/auditlogs/recent?count=100 - Get recent logs (max 1000)

Features:

  • Multi-tenant isolation via Global Query Filters (automatic)
  • Read-only query endpoints (no write operations)
  • Swagger/OpenAPI documentation via attributes
  • Proper HTTP status codes (200 OK, 404 Not Found)
  • Cancellation token support
  • Primary constructor pattern (modern C# style)

Implementation Details

Files to Create:

  1. Query DTOs: colaflow-api/src/ColaFlow.Application/AuditLog/Queries/GetEntityAuditHistory/GetEntityAuditHistoryQuery.cs
public record GetEntityAuditHistoryQuery : IRequest<List<AuditLogDto>>
{
    public string EntityType { get; init; } = string.Empty;
    public Guid EntityId { get; init; }
    public DateTime? FromDate { get; init; }
    public DateTime? ToDate { get; init; }
    public Guid? UserId { get; init; }
    public int? Limit { get; init; } = 50; // Default limit
}

public class AuditLogDto
{
    public Guid Id { get; set; }
    public string EntityType { get; set; } = string.Empty;
    public Guid EntityId { get; set; }
    public string Action { get; set; } = string.Empty;
    public Guid? UserId { get; set; }
    public string? UserName { get; set; }
    public DateTime Timestamp { get; set; }
    public Dictionary<string, FieldChangeDto>? ChangedFields { get; set; }
}

public class FieldChangeDto
{
    public object? OldValue { get; set; }
    public object? NewValue { get; set; }
}
  1. Query Handler: colaflow-api/src/ColaFlow.Application/AuditLog/Queries/GetEntityAuditHistory/GetEntityAuditHistoryQueryHandler.cs
public class GetEntityAuditHistoryQueryHandler : IRequestHandler<GetEntityAuditHistoryQuery, List<AuditLogDto>>
{
    private readonly IAuditLogRepository _repository;
    private readonly IMapper _mapper;

    public GetEntityAuditHistoryQueryHandler(IAuditLogRepository repository, IMapper mapper)
    {
        _repository = repository;
        _mapper = mapper;
    }

    public async Task<List<AuditLogDto>> Handle(GetEntityAuditHistoryQuery request, CancellationToken cancellationToken)
    {
        var auditLogs = await _repository.GetByEntityAsync(request.EntityType, request.EntityId);

        // Apply filters
        var filtered = auditLogs.AsQueryable();

        if (request.FromDate.HasValue)
            filtered = filtered.Where(a => a.Timestamp >= request.FromDate.Value);

        if (request.ToDate.HasValue)
            filtered = filtered.Where(a => a.Timestamp <= request.ToDate.Value);

        if (request.UserId.HasValue)
            filtered = filtered.Where(a => a.UserId == request.UserId.Value);

        if (request.Limit.HasValue)
            filtered = filtered.Take(request.Limit.Value);

        var result = filtered.ToList();

        return _mapper.Map<List<AuditLogDto>>(result);
    }
}
  1. Controller: colaflow-api/src/ColaFlow.API/Controllers/AuditLogsController.cs
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class AuditLogsController : ControllerBase
{
    private readonly IMediator _mediator;

    public AuditLogsController(IMediator mediator)
    {
        _mediator = mediator;
    }

    /// <summary>
    /// Get audit history for a specific entity
    /// </summary>
    /// <param name="entityType">Entity type (e.g., "Project", "Epic", "Story")</param>
    /// <param name="entityId">Entity ID</param>
    /// <param name="fromDate">Optional start date filter</param>
    /// <param name="toDate">Optional end date filter</param>
    /// <param name="userId">Optional user filter</param>
    /// <param name="limit">Optional limit (default 50)</param>
    [HttpGet("entity/{entityType}/{entityId}")]
    [ProducesResponseType(typeof(List<AuditLogDto>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetEntityAuditHistory(
        [FromRoute] string entityType,
        [FromRoute] Guid entityId,
        [FromQuery] DateTime? fromDate = null,
        [FromQuery] DateTime? toDate = null,
        [FromQuery] Guid? userId = null,
        [FromQuery] int? limit = 50)
    {
        var query = new GetEntityAuditHistoryQuery
        {
            EntityType = entityType,
            EntityId = entityId,
            FromDate = fromDate,
            ToDate = toDate,
            UserId = userId,
            Limit = limit
        };

        var result = await _mediator.Send(query);
        return Ok(result);
    }

    /// <summary>
    /// Get a specific audit log by ID
    /// </summary>
    [HttpGet("{id}")]
    [ProducesResponseType(typeof(AuditLogDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetAuditLogById([FromRoute] Guid id)
    {
        var query = new GetAuditLogByIdQuery { AuditLogId = id };
        var result = await _mediator.Send(query);

        if (result == null)
            return NotFound();

        return Ok(result);
    }
}
  1. AutoMapper Profile: colaflow-api/src/ColaFlow.Application/AuditLog/MappingProfiles/AuditLogProfile.cs
public class AuditLogProfile : Profile
{
    public AuditLogProfile()
    {
        CreateMap<AuditLog, AuditLogDto>()
            .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.User != null ? src.User.UserName : null))
            .ForMember(dest => dest.ChangedFields, opt => opt.MapFrom(src => DeserializeChangedFields(src.NewValues)));
    }

    private Dictionary<string, FieldChangeDto>? DeserializeChangedFields(string? json)
    {
        if (string.IsNullOrEmpty(json)) return null;

        try
        {
            return JsonSerializer.Deserialize<Dictionary<string, FieldChangeDto>>(json);
        }
        catch
        {
            return null;
        }
    }
}

Example API Requests:

# Get audit history for a project
GET /api/auditlogs/entity/Project/abc-123?limit=20

# Get audit logs for last 7 days
GET /api/auditlogs/entity/Epic/def-456?fromDate=2025-10-29

# Get audit logs by specific user
GET /api/auditlogs/entity/Story/ghi-789?userId=user-123

# Get specific audit log
GET /api/auditlogs/abc-123

Technical Notes

  • Use AsNoTracking() for read-only queries (performance)
  • Implement pagination for large result sets
  • Cache frequently accessed audit logs (Redis - future)
  • Add rate limiting to prevent abuse

Testing

Integration Tests:

[Fact]
public async Task GetEntityAuditHistory_ShouldReturnAuditLogs()
{
    // Arrange
    var projectId = await CreateTestProject();

    // Act
    var response = await Client.GetAsync($"/api/auditlogs/entity/Project/{projectId}");

    // Assert
    response.EnsureSuccessStatusCode();
    var logs = await response.Content.ReadFromJsonAsync<List<AuditLogDto>>();
    Assert.NotEmpty(logs);
}

[Fact]
public async Task GetEntityAuditHistory_ShouldFilterByDateRange()
{
    // Test date range filtering
    // ...
}

Created: 2025-11-05 by Backend Agent