using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.JobEngine.Infrastructure.Repositories; using StellaOps.JobEngine.WebService.Contracts; using StellaOps.JobEngine.WebService.Services; using static StellaOps.Localization.T; namespace StellaOps.JobEngine.WebService.Endpoints; /// /// REST API endpoints for runs (batch executions). /// public static class RunEndpoints { /// /// Maps run endpoints to the route builder. /// public static RouteGroupBuilder MapRunEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/v1/jobengine/runs") .WithTags("Orchestrator Runs") .RequireAuthorization(JobEnginePolicies.Read) .RequireTenant(); group.MapGet(string.Empty, ListRuns) .WithName("Orchestrator_ListRuns") .WithDescription(_t("orchestrator.run.list_description")); group.MapGet("{runId:guid}", GetRun) .WithName("Orchestrator_GetRun") .WithDescription(_t("orchestrator.run.get_description")); group.MapGet("{runId:guid}/jobs", GetRunJobs) .WithName("Orchestrator_GetRunJobs") .WithDescription(_t("orchestrator.run.get_jobs_description")); group.MapGet("{runId:guid}/summary", GetRunSummary) .WithName("Orchestrator_GetRunSummary") .WithDescription(_t("orchestrator.run.get_summary_description")); return group; } private static async Task ListRuns( HttpContext context, [FromServices] TenantResolver tenantResolver, [FromServices] IRunRepository repository, [FromQuery] Guid? sourceId = null, [FromQuery] string? runType = null, [FromQuery] string? status = null, [FromQuery] string? projectId = null, [FromQuery] string? createdAfter = null, [FromQuery] string? createdBefore = null, [FromQuery] int? limit = null, [FromQuery] string? cursor = null, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); var effectiveLimit = EndpointHelpers.GetLimit(limit); var offset = EndpointHelpers.ParseCursorOffset(cursor); var parsedStatus = EndpointHelpers.TryParseRunStatus(status); var parsedCreatedAfter = EndpointHelpers.TryParseDateTimeOffset(createdAfter); var parsedCreatedBefore = EndpointHelpers.TryParseDateTimeOffset(createdBefore); var runs = await repository.ListAsync( tenantId, sourceId, runType, parsedStatus, projectId, parsedCreatedAfter, parsedCreatedBefore, effectiveLimit, offset, cancellationToken).ConfigureAwait(false); var responses = runs.Select(RunResponse.FromDomain).ToList(); var nextCursor = EndpointHelpers.CreateNextCursor(offset, effectiveLimit, responses.Count); return Results.Ok(new RunListResponse(responses, nextCursor)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetRun( HttpContext context, [FromRoute] Guid runId, [FromServices] TenantResolver tenantResolver, [FromServices] IRunRepository repository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); var run = await repository.GetByIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); if (run is null) { return Results.NotFound(); } return Results.Ok(RunResponse.FromDomain(run)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetRunJobs( HttpContext context, [FromRoute] Guid runId, [FromServices] TenantResolver tenantResolver, [FromServices] IRunRepository runRepository, [FromServices] IJobRepository jobRepository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); // Verify run exists var run = await runRepository.GetByIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); if (run is null) { return Results.NotFound(); } var jobs = await jobRepository.GetByRunIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); var responses = jobs.Select(JobResponse.FromDomain).ToList(); return Results.Ok(new JobListResponse(responses, null)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetRunSummary( HttpContext context, [FromRoute] Guid runId, [FromServices] TenantResolver tenantResolver, [FromServices] IRunRepository runRepository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); var run = await runRepository.GetByIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); if (run is null) { return Results.NotFound(); } // Return the aggregate counts from the run itself var summary = new { runId = run.RunId, status = run.Status.ToString().ToLowerInvariant(), totalJobs = run.TotalJobs, completedJobs = run.CompletedJobs, succeededJobs = run.SucceededJobs, failedJobs = run.FailedJobs, pendingJobs = run.TotalJobs - run.CompletedJobs, createdAt = run.CreatedAt, startedAt = run.StartedAt, completedAt = run.CompletedAt }; return Results.Ok(summary); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } }