78 lines
2.5 KiB
C#
78 lines
2.5 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
|
|
/// <summary>
|
|
/// Provides cursor-paginated access to the audit trail of digitization events
|
|
/// for a given batch. Events are ordered by occurred_at ascending with id
|
|
/// as a tie-breaker for events at the same timestamp.
|
|
/// </summary>
|
|
public class BatchEventService : IBatchEventService
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private const int MaxPageSize = 200;
|
|
private const int DefaultPageSize = 50;
|
|
|
|
public BatchEventService(AppDbContext db)
|
|
{
|
|
_db = db;
|
|
}
|
|
|
|
public async Task<CursorPagedResult<BatchEventResponse>> GetEventsAsync(
|
|
Guid batchId, DateTimeOffset? after, int pageSize)
|
|
{
|
|
// Validate batch exists
|
|
var batchExists = await _db.DigitizationBatches
|
|
.AnyAsync(b => b.Id == batchId);
|
|
|
|
if (!batchExists)
|
|
throw new NotFoundException("Batch not found.", "BATCH_NOT_FOUND");
|
|
|
|
// Clamp page size
|
|
pageSize = Math.Clamp(pageSize, 1, MaxPageSize);
|
|
|
|
// Build query
|
|
var query = _db.DigitizationEvents
|
|
.AsNoTracking()
|
|
.Include(e => e.Actor)
|
|
.Where(e => e.BatchId == batchId);
|
|
|
|
// Apply cursor filter — only events strictly after the cursor timestamp
|
|
if (after.HasValue)
|
|
{
|
|
query = query.Where(e => e.OccurredAt > after.Value);
|
|
}
|
|
|
|
// Fetch pageSize+1 rows to determine HasMore without a COUNT query
|
|
var events = await query
|
|
.OrderBy(e => e.OccurredAt)
|
|
.ThenBy(e => e.Id) // tie-breaker for events at the same timestamp
|
|
.Take(pageSize + 1)
|
|
.Select(e => new BatchEventResponse(
|
|
e.Id,
|
|
e.BatchId,
|
|
e.EventType.ToDbString(),
|
|
e.ActorUserId,
|
|
e.Actor.Username,
|
|
e.Actor.FullName,
|
|
e.OccurredAt,
|
|
e.MetadataJson))
|
|
.ToListAsync();
|
|
|
|
var hasMore = events.Count > pageSize;
|
|
var page = hasMore ? events.Take(pageSize).ToList() : events;
|
|
|
|
// Build next cursor from the last item's OccurredAt
|
|
string? nextCursor = null;
|
|
if (hasMore && page.Count > 0)
|
|
{
|
|
var lastEvent = page[^1];
|
|
// ISO-8601 round-trip format preserves full precision
|
|
nextCursor = lastEvent.OccurredAt.ToString("o");
|
|
}
|
|
|
|
return new CursorPagedResult<BatchEventResponse>(
|
|
Items: page,
|
|
PageSize: pageSize,
|
|
NextCursor: nextCursor,
|
|
HasMore: hasMore);
|
|
}
|
|
} |