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>
This commit is contained in:
Yaojia Wang
2025-11-04 23:56:37 +01:00
parent 408da02b57
commit 6cbf7dc6dc
11 changed files with 297 additions and 14 deletions

View File

@@ -0,0 +1,15 @@
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.AuditLogs;
/// <summary>
/// Data Transfer Object for AuditLog query results
/// </summary>
public record AuditLogDto(
Guid Id,
string EntityType,
Guid EntityId,
string Action,
Guid? UserId,
DateTime Timestamp,
string? OldValues,
string? NewValues
);

View File

@@ -0,0 +1,8 @@
using MediatR;
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.AuditLogs.GetAuditLogById;
/// <summary>
/// Query to retrieve a specific audit log by its ID
/// </summary>
public record GetAuditLogByIdQuery(Guid AuditLogId) : IRequest<AuditLogDto?>;

View File

@@ -0,0 +1,37 @@
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using MediatR;
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.AuditLogs.GetAuditLogById;
/// <summary>
/// Handler for GetAuditLogByIdQuery
/// Retrieves a single audit log entry by its unique identifier
/// </summary>
public class GetAuditLogByIdQueryHandler : IRequestHandler<GetAuditLogByIdQuery, AuditLogDto?>
{
private readonly IAuditLogRepository _auditLogRepository;
public GetAuditLogByIdQueryHandler(IAuditLogRepository auditLogRepository)
{
_auditLogRepository = auditLogRepository;
}
public async Task<AuditLogDto?> Handle(GetAuditLogByIdQuery request, CancellationToken cancellationToken)
{
var auditLog = await _auditLogRepository.GetByIdAsync(request.AuditLogId, cancellationToken);
if (auditLog == null)
return null;
return new AuditLogDto(
auditLog.Id,
auditLog.EntityType,
auditLog.EntityId,
auditLog.Action,
auditLog.UserId?.Value,
auditLog.Timestamp,
auditLog.OldValues,
auditLog.NewValues
);
}
}

View File

@@ -0,0 +1,11 @@
using MediatR;
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.AuditLogs.GetAuditLogsByEntity;
/// <summary>
/// Query to retrieve all audit logs for a specific entity
/// </summary>
public record GetAuditLogsByEntityQuery(
string EntityType,
Guid EntityId
) : IRequest<IReadOnlyList<AuditLogDto>>;

View File

@@ -0,0 +1,40 @@
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using MediatR;
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.AuditLogs.GetAuditLogsByEntity;
/// <summary>
/// Handler for GetAuditLogsByEntityQuery
/// Retrieves all audit log entries for a specific entity (e.g., all changes to a Project)
/// Results are automatically filtered by tenant via global query filter
/// </summary>
public class GetAuditLogsByEntityQueryHandler : IRequestHandler<GetAuditLogsByEntityQuery, IReadOnlyList<AuditLogDto>>
{
private readonly IAuditLogRepository _auditLogRepository;
public GetAuditLogsByEntityQueryHandler(IAuditLogRepository auditLogRepository)
{
_auditLogRepository = auditLogRepository;
}
public async Task<IReadOnlyList<AuditLogDto>> Handle(GetAuditLogsByEntityQuery request, CancellationToken cancellationToken)
{
var auditLogs = await _auditLogRepository.GetByEntityAsync(
request.EntityType,
request.EntityId,
cancellationToken);
return auditLogs
.Select(a => new AuditLogDto(
a.Id,
a.EntityType,
a.EntityId,
a.Action,
a.UserId?.Value,
a.Timestamp,
a.OldValues,
a.NewValues
))
.ToList();
}
}

View File

@@ -0,0 +1,8 @@
using MediatR;
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.AuditLogs.GetRecentAuditLogs;
/// <summary>
/// Query to retrieve the most recent audit logs across all entities
/// </summary>
public record GetRecentAuditLogsQuery(int Count = 100) : IRequest<IReadOnlyList<AuditLogDto>>;

View File

@@ -0,0 +1,37 @@
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using MediatR;
namespace ColaFlow.Modules.ProjectManagement.Application.Queries.AuditLogs.GetRecentAuditLogs;
/// <summary>
/// Handler for GetRecentAuditLogsQuery
/// Retrieves the most recent audit log entries across all entities
/// Results are automatically filtered by tenant via global query filter
/// </summary>
public class GetRecentAuditLogsQueryHandler : IRequestHandler<GetRecentAuditLogsQuery, IReadOnlyList<AuditLogDto>>
{
private readonly IAuditLogRepository _auditLogRepository;
public GetRecentAuditLogsQueryHandler(IAuditLogRepository auditLogRepository)
{
_auditLogRepository = auditLogRepository;
}
public async Task<IReadOnlyList<AuditLogDto>> Handle(GetRecentAuditLogsQuery request, CancellationToken cancellationToken)
{
var auditLogs = await _auditLogRepository.GetRecentAsync(request.Count, cancellationToken);
return auditLogs
.Select(a => new AuditLogDto(
a.Id,
a.EntityType,
a.EntityId,
a.Action,
a.UserId?.Value,
a.Timestamp,
a.OldValues,
a.NewValues
))
.ToList();
}
}

View File

@@ -10,6 +10,7 @@ using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence;
using ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.Interceptors;
using ColaFlow.Modules.ProjectManagement.Infrastructure.Repositories;
using ColaFlow.Modules.ProjectManagement.Infrastructure.Services;
@@ -25,18 +26,25 @@ public class ProjectManagementModule : IModule
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
{
// Register DbContext
// Register tenant context service (must be before DbContext for interceptor)
services.AddScoped<ITenantContext, TenantContext>();
// Register audit interceptor
services.AddScoped<AuditInterceptor>();
// Register DbContext with interceptor
var connectionString = configuration.GetConnectionString("PMDatabase");
services.AddDbContext<PMDbContext>(options =>
options.UseNpgsql(connectionString));
services.AddDbContext<PMDbContext>((serviceProvider, options) =>
{
var auditInterceptor = serviceProvider.GetRequiredService<AuditInterceptor>();
options.UseNpgsql(connectionString)
.AddInterceptors(auditInterceptor);
});
// Register repositories
services.AddScoped<IProjectRepository, ProjectRepository>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
// Register tenant context service
services.AddScoped<ITenantContext, TenantContext>();
// Note: IProjectNotificationService is registered in the API layer (Program.cs)
// as it depends on IRealtimeNotificationService which is API-specific