Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion api.Tests/Services/WorkflowServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ public async Task TriggerWorkflow_ArgoReturnsError_MarksWorkflowAndRunFailed()
outputBlobStorageLocation: _db.NewBlobStorageLocation()
);

await TriggerWorkflowInScope(workflow.Id);
await Assert.ThrowsAsync<WorkflowTriggerFailedException>(
() => TriggerWorkflowInScope(workflow.Id)
);

await _context.Entry(workflow).ReloadAsync(TestContext.Current.CancellationToken);
await _context.Entry(run).ReloadAsync(TestContext.Current.CancellationToken);
Expand Down
18 changes: 18 additions & 0 deletions api/Controllers/AnalysisController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,22 @@ public async Task<IActionResult> Delete([FromRoute] Guid id)
return NotFound(ex.Message);
}
}

[HttpGet]
[Authorize(Roles = Role.Any)]
[Route("available")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<List<string>>> GetAvailableAnalyses()
{
try
{
var availableAnalyses = await analysisService.GetAvailableAnalyses();
return Ok(availableAnalyses);
}
catch (Exception e)
{
logger.LogError(e, "Error during GET of available analyses");
throw;
}
}
}
44 changes: 44 additions & 0 deletions api/Controllers/InspectionRecordController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,48 @@ public async Task<IActionResult> Delete([FromRoute] Guid id)
return NotFound(ex.Message);
}
}

[HttpPost]
[Authorize(Roles = Role.Any)]
[Route("id/{id:guid}/analyses")]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status502BadGateway)]
public async Task<IActionResult> AddAnalysis(
[FromRoute] Guid id,
[FromBody] AddAnalysisRequest request
)
{
request.AnalysisName = Sanitize.SanitizeUserInput(request.AnalysisName);
try
{
await inspectionRecordService.AddAnalysis(id, request.AnalysisName);
return AcceptedAtAction(nameof(GetById), new { id }, null);
}
catch (KeyNotFoundException ex)
{
return NotFound(ex.Message);
}
catch (WorkflowTriggerFailedException ex)
{
logger.LogError(ex, "Upstream workflow trigger failed for inspection record {Id}", id);
return StatusCode(StatusCodes.Status502BadGateway, ex.Message);
}
catch (InvalidOperationException ex)
{
return Conflict(ex.Message);
}
catch (Exception e)
{
logger.LogError(e, "Error adding analysis to inspection record");
throw;
}
}
}

public class AddAnalysisRequest
{
public required string AnalysisName { get; set; }
}
14 changes: 13 additions & 1 deletion api/Services/AnalysisService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using api.Configurations;
using api.Database.Context;
using api.Database.Models;
using api.Utilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace api.Services;

Expand All @@ -14,6 +16,8 @@ public interface IAnalysisService
public Task<PagedList<Analysis>> GetAnalyses(AnalysisParameters parameters);

public Task Delete(Guid id);

public Task<List<string>> GetAvailableAnalyses();
}

public class AnalysisParameters
Expand All @@ -25,8 +29,11 @@ public class AnalysisParameters
public Guid? InspectionRecordId { get; set; }
}

public class AnalysisService(SaraDbContext context) : IAnalysisService
public class AnalysisService(SaraDbContext context, IOptions<AnalysisOptions> analysisOptions)
: IAnalysisService
{
private readonly AnalysisOptions _analysisOptions = analysisOptions.Value;

public async Task<Analysis?> ReadById(Guid id)
{
return await context
Expand Down Expand Up @@ -90,4 +97,9 @@ public async Task Delete(Guid id)
context.Analyses.Remove(analysis);
await context.SaveChangesAsync();
}

public Task<List<string>> GetAvailableAnalyses()
{
return Task.FromResult(_analysisOptions.Analyses.Keys.ToList());
}
}
34 changes: 34 additions & 0 deletions api/Services/InspectionRecordService.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using api.Configurations;
using api.Database.Context;
using api.Database.Models;
using api.MQTT;
using api.Utilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace api.Services;

Expand All @@ -25,6 +27,8 @@ InspectionRecordParameters parameters
);

public Task Delete(Guid id);

public Task<InspectionRecord> AddAnalysis(Guid inspectionRecordId, string analysisName);
}

public class InspectionRecordParameters
Expand Down Expand Up @@ -60,9 +64,12 @@ public class CreateInspectionRecordAnalysisGroup
public class InspectionRecordService(
SaraDbContext context,
IAnalysisTriggerService analysisTriggerService,
IOptions<AnalysisOptions> analysisOptions,
ILogger<InspectionRecordService> logger
) : IInspectionRecordService
{
private readonly AnalysisOptions _analysisOptions = analysisOptions.Value;

public async Task<InspectionRecord> CreateFromMqttMessage(IsarInspectionResultMessage message)
{
var inspectionId = Sanitize.SanitizeUserInput(message.InspectionId);
Expand Down Expand Up @@ -253,4 +260,31 @@ InspectionRecordParameters parameters
parameters.PageSize
);
}

public async Task<InspectionRecord> AddAnalysis(Guid inspectionRecordId, string analysisName)
{
if (!_analysisOptions.Analyses.ContainsKey(analysisName))
{
throw new InvalidOperationException(
$"Unknown analysis '{analysisName}'. Valid analyses are: "
+ string.Join(", ", _analysisOptions.Analyses.Keys)
);
}

var record =
await ReadById(inspectionRecordId)
?? throw new KeyNotFoundException(
$"Inspection record with id {inspectionRecordId} not found"
);

await analysisTriggerService.OnInspectionRecordCreated(
new InspectionRecordCreatedEvent
{
InspectionRecordId = record.Id,
RequiredAnalysis = [analysisName],
}
);

return record;
}
}
17 changes: 16 additions & 1 deletion api/Services/WorkflowService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

namespace api.Services;

public class WorkflowTriggerFailedException(string message, Exception? innerException = null)
: Exception(message, innerException);

public interface IWorkflowService
{
public Task TriggerWorkflow(Guid workflowId);
Expand Down Expand Up @@ -167,6 +170,11 @@ public async Task TriggerWorkflow(Guid workflowId)
);

await MarkWorkflowFailed(workflow, ex.Message);

throw new WorkflowTriggerFailedException(
$"Failed to trigger workflow '{workflow.WorkflowType}'",
ex
);
}
}

Expand Down Expand Up @@ -235,7 +243,14 @@ public async Task OnWorkflowCompleted(Guid workflowId)
nextWorkflow.StepNumber
);

await TriggerWorkflow(nextWorkflow.Id);
try
{
await TriggerWorkflow(nextWorkflow.Id);
}
catch (WorkflowTriggerFailedException)
{
// Already logged and persisted inside TriggerWorkflow.
}
}

private async Task MarkWorkflowFailed(Workflow workflow, string errorMessage)
Expand Down
Loading