audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using StellaOps.Timeline.Core;
|
||||
|
||||
namespace StellaOps.Timeline.WebService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Export endpoints for timeline bundles.
|
||||
/// </summary>
|
||||
public static class ExportEndpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps export endpoints.
|
||||
/// </summary>
|
||||
public static void MapExportEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/api/v1/timeline")
|
||||
.WithTags("Export")
|
||||
.WithOpenApi();
|
||||
|
||||
group.MapPost("/{correlationId}/export", ExportTimelineAsync)
|
||||
.WithName("ExportTimeline")
|
||||
.WithDescription("Export timeline events as NDJSON bundle with optional DSSE signing");
|
||||
|
||||
group.MapGet("/export/{exportId}", GetExportStatusAsync)
|
||||
.WithName("GetExportStatus")
|
||||
.WithDescription("Get the status of an export operation");
|
||||
|
||||
group.MapGet("/export/{exportId}/download", DownloadExportAsync)
|
||||
.WithName("DownloadExport")
|
||||
.WithDescription("Download the completed export bundle");
|
||||
}
|
||||
|
||||
private static async Task<Results<Accepted<ExportInitiatedResponse>, BadRequest<string>>> ExportTimelineAsync(
|
||||
string correlationId,
|
||||
ExportRequest request,
|
||||
ITimelineQueryService queryService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(correlationId))
|
||||
{
|
||||
return TypedResults.BadRequest("Correlation ID is required");
|
||||
}
|
||||
|
||||
// Validate the correlation exists
|
||||
var result = await queryService.GetByCorrelationIdAsync(correlationId, new TimelineQueryOptions { Limit = 1 }, cancellationToken);
|
||||
if (result.Events.Count == 0)
|
||||
{
|
||||
return TypedResults.BadRequest($"No events found for correlation ID: {correlationId}");
|
||||
}
|
||||
|
||||
// TODO: Queue export job
|
||||
var exportId = Guid.NewGuid().ToString("N")[..16];
|
||||
|
||||
return TypedResults.Accepted(
|
||||
$"/api/v1/timeline/export/{exportId}",
|
||||
new ExportInitiatedResponse
|
||||
{
|
||||
ExportId = exportId,
|
||||
CorrelationId = correlationId,
|
||||
Format = request.Format,
|
||||
SignBundle = request.SignBundle,
|
||||
Status = "INITIATED",
|
||||
EstimatedEventCount = result.TotalCount
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<Results<Ok<ExportStatusResponse>, NotFound>> GetExportStatusAsync(
|
||||
string exportId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: Integrate with export state store
|
||||
await Task.CompletedTask;
|
||||
|
||||
return TypedResults.Ok(new ExportStatusResponse
|
||||
{
|
||||
ExportId = exportId,
|
||||
Status = "COMPLETED",
|
||||
Format = "ndjson",
|
||||
EventCount = 100,
|
||||
FileSizeBytes = 45678,
|
||||
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-1),
|
||||
CompletedAt = DateTimeOffset.UtcNow.AddSeconds(-30)
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task<Results<FileStreamHttpResult, NotFound>> DownloadExportAsync(
|
||||
string exportId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: Integrate with export storage
|
||||
await Task.CompletedTask;
|
||||
|
||||
// Return stub for now - real implementation would stream from storage
|
||||
var stubContent = """
|
||||
{"event_id":"abc123","correlation_id":"scan-1","kind":"ENQUEUE"}
|
||||
{"event_id":"def456","correlation_id":"scan-1","kind":"EXECUTE"}
|
||||
""";
|
||||
var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(stubContent));
|
||||
|
||||
return TypedResults.File(
|
||||
stream,
|
||||
contentType: "application/x-ndjson",
|
||||
fileDownloadName: $"timeline-{exportId}.ndjson");
|
||||
}
|
||||
}
|
||||
|
||||
// DTOs
|
||||
public sealed record ExportRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Export format: "ndjson" or "json".
|
||||
/// </summary>
|
||||
public string Format { get; init; } = "ndjson";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to DSSE-sign the bundle.
|
||||
/// </summary>
|
||||
public bool SignBundle { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Optional HLC range start.
|
||||
/// </summary>
|
||||
public string? FromHlc { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional HLC range end.
|
||||
/// </summary>
|
||||
public string? ToHlc { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ExportInitiatedResponse
|
||||
{
|
||||
public required string ExportId { get; init; }
|
||||
public required string CorrelationId { get; init; }
|
||||
public required string Format { get; init; }
|
||||
public bool SignBundle { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public long EstimatedEventCount { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ExportStatusResponse
|
||||
{
|
||||
public required string ExportId { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public required string Format { get; init; }
|
||||
public long EventCount { get; init; }
|
||||
public long FileSizeBytes { get; init; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset? CompletedAt { get; init; }
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user