diff --git a/backend/api.test/EventHandlers/TestMissionEventHandler.cs b/backend/api.test/EventHandlers/TestMissionEventHandler.cs index 3fb012a76..cc2352563 100644 --- a/backend/api.test/EventHandlers/TestMissionEventHandler.cs +++ b/backend/api.test/EventHandlers/TestMissionEventHandler.cs @@ -52,6 +52,7 @@ public TestMissionEventHandler(DatabaseFixture fixture) var returnToHomeServiceLogger = new Mock>().Object; var missionDefinitionServiceLogger = new Mock>().Object; var lastMissionRunServiceLogger = new Mock>().Object; + var sourceServiceLogger = new Mock>().Object; var configuration = WebApplication.CreateBuilder().Configuration; @@ -66,8 +67,8 @@ public TestMissionEventHandler(DatabaseFixture fixture) var echoServiceMock = new MockEchoService(); var stidServiceMock = new MockStidService(context); - var customMissionServiceMock = new MockCustomMissionService(); - var missionDefinitionService = new MissionDefinitionService(context, echoServiceMock, customMissionServiceMock, signalRService, accessRoleService, missionDefinitionServiceLogger, _missionRunService); + var sourceService = new SourceService(context, echoServiceMock, sourceServiceLogger); + var missionDefinitionService = new MissionDefinitionService(context, echoServiceMock, sourceService, signalRService, accessRoleService, missionDefinitionServiceLogger, _missionRunService); var robotModelService = new RobotModelService(context); var taskDurationServiceMock = new MockTaskDurationService(); var isarServiceMock = new MockIsarService(); diff --git a/backend/api.test/Mocks/CustomMissionServiceMock.cs b/backend/api.test/Mocks/CustomMissionServiceMock.cs deleted file mode 100644 index d76ead6fa..000000000 --- a/backend/api.test/Mocks/CustomMissionServiceMock.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Api.Controllers.Models; -using Api.Database.Models; -namespace Api.Services -{ - public class MockCustomMissionService : ICustomMissionService - { - private static readonly Dictionary> mockBlobStore = []; - - public Task UploadSource(List tasks) - { - string hash = CalculateHashFromTasks(tasks); - mockBlobStore.Add(hash, tasks); - return Task.FromResult(hash); - } - - public async Task?> GetMissionTasksFromSourceId(string id) - { - if (mockBlobStore.TryGetValue(id, out var value)) - { - var content = value; - foreach (var task in content) - { - task.Id = Guid.NewGuid().ToString(); // This is needed as tasks are owned by mission runs - } - return content; - } - await Task.CompletedTask; - return null; - } - - public string CalculateHashFromTasks(IList tasks) - { - var genericTasks = new List(); - foreach (var task in tasks) - { - var taskCopy = new MissionTask(task) - { - Id = "", - IsarTaskId = "" - }; - taskCopy.Inspections = taskCopy.Inspections.Select(i => new Inspection(i, useEmptyIDs: true)).ToList(); - genericTasks.Add(taskCopy); - } - - string json = JsonSerializer.Serialize(genericTasks); - byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(json)); - return BitConverter.ToString(hash).Replace("-", "", StringComparison.CurrentCulture).ToUpperInvariant(); - } - -#pragma warning disable IDE0060, CA1822 // Remove unused parameter - public async Task QueueCustomMissionRun(CustomMissionQuery customMissionQuery, MissionDefinition customMissionDefinition, Robot robot, IList missionTasks) - { - await Task.CompletedTask; - return new MissionRun(); - } -#pragma warning restore IDE0060, CA1822 // Remove unused parameter - } -} diff --git a/backend/api.test/TestWebApplicationFactory.cs b/backend/api.test/TestWebApplicationFactory.cs index ed4d805b5..b5eb2730d 100644 --- a/backend/api.test/TestWebApplicationFactory.cs +++ b/backend/api.test/TestWebApplicationFactory.cs @@ -36,7 +36,6 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddAuthorizationBuilder().AddFallbackPolicy( TestAuthHandler.AuthenticationScheme, policy => policy.RequireAuthenticatedUser() ); diff --git a/backend/api/Controllers/MissionSchedulingController.cs b/backend/api/Controllers/MissionSchedulingController.cs index 80add502b..a7781baab 100644 --- a/backend/api/Controllers/MissionSchedulingController.cs +++ b/backend/api/Controllers/MissionSchedulingController.cs @@ -2,7 +2,6 @@ using Api.Controllers.Models; using Api.Database.Models; using Api.Services; -using Api.Services.ActionServices; using Api.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -14,7 +13,6 @@ namespace Api.Controllers [Route("missions")] public class MissionSchedulingController( IMissionDefinitionService missionDefinitionService, - ICustomMissionSchedulingService customMissionSchedulingService, IMissionRunService missionRunService, IInstallationService installationService, IEchoService echoService, @@ -23,7 +21,8 @@ public class MissionSchedulingController( IStidService stidService, ILocalizationService localizationService, IRobotService robotService, - ISourceService sourceService + ISourceService sourceService, + IAreaService areaService ) : ControllerBase { @@ -371,7 +370,41 @@ [FromBody] CustomMissionQuery customMissionQuery var missionTasks = customMissionQuery.Tasks.Select(task => new MissionTask(task)).ToList(); MissionDefinition? customMissionDefinition; - try { customMissionDefinition = await customMissionSchedulingService.FindExistingOrCreateCustomMissionDefinition(customMissionQuery, missionTasks); } + try + { + Area? area = null; + if (customMissionQuery.AreaName != null) { area = await areaService.ReadByInstallationAndName(customMissionQuery.InstallationCode, customMissionQuery.AreaName); } + + if (area == null) + { + throw new AreaNotFoundException($"No area with name {customMissionQuery.AreaName} in installation {customMissionQuery.InstallationCode} was found"); + } + + var source = await sourceService.CheckForExistingCustomSource(missionTasks); + + MissionDefinition? existingMissionDefinition = null; + if (source == null) + { + source = await sourceService.CreateSourceIfDoesNotExist(missionTasks); + } + else + { + var missionDefinitions = await missionDefinitionService.ReadBySourceId(source.SourceId); + if (missionDefinitions.Count > 0) { existingMissionDefinition = missionDefinitions.First(); } + } + + customMissionDefinition = existingMissionDefinition ?? new MissionDefinition + { + Id = Guid.NewGuid().ToString(), + Source = source, + Name = customMissionQuery.Name, + InspectionFrequency = customMissionQuery.InspectionFrequency, + InstallationCode = customMissionQuery.InstallationCode, + Area = area + }; + + if (existingMissionDefinition == null) { await missionDefinitionService.Create(customMissionDefinition); } + } catch (SourceException e) { return StatusCode(StatusCodes.Status502BadGateway, e.Message); } catch (AreaNotFoundException) { return NotFound($"No area with name {customMissionQuery.AreaName} in installation {customMissionQuery.InstallationCode} was found"); } @@ -380,7 +413,30 @@ [FromBody] CustomMissionQuery customMissionQuery catch (RobotNotInSameInstallationAsMissionException e) { return Conflict(e.Message); } MissionRun? newMissionRun; - try { newMissionRun = await customMissionSchedulingService.QueueCustomMissionRun(customMissionQuery, customMissionDefinition.Id, robot.Id, missionTasks); } + try + { + var scheduledMission = new MissionRun + { + Name = customMissionQuery.Name, + Description = customMissionQuery.Description, + MissionId = customMissionDefinition.Id, + Comment = customMissionQuery.Comment, + Robot = robot, + Status = MissionStatus.Pending, + MissionRunType = MissionRunType.Normal, + DesiredStartTime = customMissionQuery.DesiredStartTime ?? DateTime.UtcNow, + Tasks = missionTasks, + InstallationCode = customMissionQuery.InstallationCode, + Area = customMissionDefinition.Area, + Map = new MapMetadata() + }; + + await mapService.AssignMapToMission(scheduledMission); + + if (scheduledMission.Tasks.Any()) { scheduledMission.CalculateEstimatedDuration(); } + + newMissionRun = await missionRunService.Create(scheduledMission); + } catch (Exception e) when (e is UnsupportedRobotCapabilityException) { return BadRequest(e.Message); } catch (Exception e) when (e is MissionNotFoundException) { return NotFound(e.Message); } catch (Exception e) when (e is RobotNotFoundException) { return NotFound(e.Message); } diff --git a/backend/api/Database/Models/Source.cs b/backend/api/Database/Models/Source.cs index 0168c2623..07942ef03 100644 --- a/backend/api/Database/Models/Source.cs +++ b/backend/api/Database/Models/Source.cs @@ -15,6 +15,8 @@ public class Source [Required] public MissionSourceType Type { get; set; } + + public string? CustomMissionTasks { get; set; } } public enum MissionSourceType diff --git a/backend/api/Migrations/20240726090053_MoveCustomMissionTasksToSourceTable.Designer.cs b/backend/api/Migrations/20240726090053_MoveCustomMissionTasksToSourceTable.Designer.cs new file mode 100644 index 000000000..880f0878d --- /dev/null +++ b/backend/api/Migrations/20240726090053_MoveCustomMissionTasksToSourceTable.Designer.cs @@ -0,0 +1,1360 @@ +// +using System; +using Api.Database.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Api.Migrations +{ + [DbContext(typeof(FlotillaDbContext))] + [Migration("20240726090053_MoveCustomMissionTasksToSourceTable")] + partial class MoveCustomMissionTasksToSourceTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Api.Database.Models.AccessRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AccessLevel") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstallationId") + .HasColumnType("text"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.ToTable("AccessRoles"); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DeckId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DefaultLocalizationPoseId") + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DeckId"); + + b.HasIndex("DefaultLocalizationPoseId"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("Areas"); + }); + + modelBuilder.Entity("Api.Database.Models.Deck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DefaultLocalizationPoseId") + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DefaultLocalizationPoseId"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("Decks"); + }); + + modelBuilder.Entity("Api.Database.Models.DefaultLocalizationPose", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DockingEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("DefaultLocalizationPoses"); + }); + + modelBuilder.Entity("Api.Database.Models.Inspection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AnalysisType") + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("InspectionType") + .IsRequired() + .HasColumnType("text"); + + b.Property("InspectionUrl") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("IsarStepId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionTaskId") + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoDuration") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("MissionTaskId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Api.Database.Models.InspectionFinding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Finding") + .IsRequired() + .HasColumnType("text"); + + b.Property("InspectionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InspectionId") + .HasColumnType("text"); + + b.Property("IsarStepId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InspectionId"); + + b.ToTable("InspectionFindings"); + }); + + modelBuilder.Entity("Api.Database.Models.Installation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationCode") + .IsUnique(); + + b.ToTable("Installations"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AreaId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("InspectionFrequency") + .HasColumnType("bigint"); + + b.Property("InstallationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("LastSuccessfulRunId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SourceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("LastSuccessfulRunId"); + + b.HasIndex("SourceId"); + + b.ToTable("MissionDefinitions"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AreaId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Description") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("DesiredStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EstimatedDuration") + .HasColumnType("bigint"); + + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("IsarMissionId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionId") + .HasColumnType("text"); + + b.Property("MissionRunType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatusReason") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("RobotId"); + + b.ToTable("MissionRuns"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("EchoPoseId") + .HasColumnType("integer"); + + b.Property("EchoTagLink") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsarTaskId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionRunId") + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TagId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TaskOrder") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MissionRunId"); + + b.ToTable("MissionTasks"); + }); + + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantCode") + .IsUnique(); + + b.ToTable("Plants"); + }); + + modelBuilder.Entity("Api.Database.Models.Robot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("BatteryLevel") + .HasColumnType("real"); + + b.Property("CurrentAreaId") + .HasColumnType("text"); + + b.Property("CurrentInstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentMissionId") + .HasColumnType("text"); + + b.Property("Deprecated") + .HasColumnType("boolean"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsarConnected") + .HasColumnType("boolean"); + + b.Property("IsarId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionQueueFrozen") + .HasColumnType("boolean"); + + b.Property("ModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("PressureLevel") + .HasColumnType("real"); + + b.Property("RobotCapabilities") + .HasColumnType("text"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrentAreaId"); + + b.HasIndex("CurrentInstallationId"); + + b.HasIndex("ModelId"); + + b.ToTable("Robots"); + }); + + modelBuilder.Entity("Api.Database.Models.RobotBatteryTimeseries", b => + { + b.Property("BatteryLevel") + .HasColumnType("real"); + + b.Property("MissionId") + .HasColumnType("text"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.ToTable("RobotBatteryTimeseries"); + }); + + modelBuilder.Entity("Api.Database.Models.RobotModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AverageDurationPerTag") + .HasColumnType("real"); + + b.Property("BatteryWarningThreshold") + .HasColumnType("real"); + + b.Property("LowerPressureWarningThreshold") + .HasColumnType("real"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpperPressureWarningThreshold") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("Type") + .IsUnique(); + + b.ToTable("RobotModels"); + }); + + modelBuilder.Entity("Api.Database.Models.RobotPoseTimeseries", b => + { + b.Property("MissionId") + .HasColumnType("text"); + + b.Property("OrientationW") + .HasColumnType("real"); + + b.Property("OrientationX") + .HasColumnType("real"); + + b.Property("OrientationY") + .HasColumnType("real"); + + b.Property("OrientationZ") + .HasColumnType("real"); + + b.Property("PositionX") + .HasColumnType("real"); + + b.Property("PositionY") + .HasColumnType("real"); + + b.Property("PositionZ") + .HasColumnType("real"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.ToTable("RobotPoseTimeseries"); + }); + + modelBuilder.Entity("Api.Database.Models.RobotPressureTimeseries", b => + { + b.Property("MissionId") + .HasColumnType("text"); + + b.Property("Pressure") + .HasColumnType("real"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.ToTable("RobotPressureTimeseries"); + }); + + modelBuilder.Entity("Api.Database.Models.SafePosition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AreaId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.ToTable("SafePositions"); + }); + + modelBuilder.Entity("Api.Database.Models.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("CustomMissionTasks") + .HasColumnType("text"); + + b.Property("SourceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("Api.Database.Models.AccessRole", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId"); + + b.Navigation("Installation"); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.HasOne("Api.Database.Models.Deck", "Deck") + .WithMany() + .HasForeignKey("DeckId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Api.Database.Models.DefaultLocalizationPose", "DefaultLocalizationPose") + .WithMany() + .HasForeignKey("DefaultLocalizationPoseId"); + + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.MapMetadata", "MapMetadata", b1 => + { + b1.Property("AreaId") + .HasColumnType("text"); + + b1.Property("MapName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.HasKey("AreaId"); + + b1.ToTable("Areas"); + + b1.WithOwner() + .HasForeignKey("AreaId"); + + b1.OwnsOne("Api.Database.Models.Boundary", "Boundary", b2 => + { + b2.Property("MapMetadataAreaId") + .HasColumnType("text"); + + b2.Property("X1") + .HasColumnType("double precision"); + + b2.Property("X2") + .HasColumnType("double precision"); + + b2.Property("Y1") + .HasColumnType("double precision"); + + b2.Property("Y2") + .HasColumnType("double precision"); + + b2.Property("Z1") + .HasColumnType("double precision"); + + b2.Property("Z2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("MapMetadataAreaId"); + }); + + b1.OwnsOne("Api.Database.Models.TransformationMatrices", "TransformationMatrices", b2 => + { + b2.Property("MapMetadataAreaId") + .HasColumnType("text"); + + b2.Property("C1") + .HasColumnType("double precision"); + + b2.Property("C2") + .HasColumnType("double precision"); + + b2.Property("D1") + .HasColumnType("double precision"); + + b2.Property("D2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("MapMetadataAreaId"); + }); + + b1.Navigation("Boundary") + .IsRequired(); + + b1.Navigation("TransformationMatrices") + .IsRequired(); + }); + + b.Navigation("Deck"); + + b.Navigation("DefaultLocalizationPose"); + + b.Navigation("Installation"); + + b.Navigation("MapMetadata") + .IsRequired(); + + b.Navigation("Plant"); + }); + + modelBuilder.Entity("Api.Database.Models.Deck", b => + { + b.HasOne("Api.Database.Models.DefaultLocalizationPose", "DefaultLocalizationPose") + .WithMany() + .HasForeignKey("DefaultLocalizationPoseId"); + + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DefaultLocalizationPose"); + + b.Navigation("Installation"); + + b.Navigation("Plant"); + }); + + modelBuilder.Entity("Api.Database.Models.DefaultLocalizationPose", b => + { + b.OwnsOne("Api.Database.Models.Pose", "Pose", b1 => + { + b1.Property("DefaultLocalizationPoseId") + .HasColumnType("text"); + + b1.HasKey("DefaultLocalizationPoseId"); + + b1.ToTable("DefaultLocalizationPoses"); + + b1.WithOwner() + .HasForeignKey("DefaultLocalizationPoseId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseDefaultLocalizationPoseId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseDefaultLocalizationPoseId"); + + b2.ToTable("DefaultLocalizationPoses"); + + b2.WithOwner() + .HasForeignKey("PoseDefaultLocalizationPoseId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseDefaultLocalizationPoseId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseDefaultLocalizationPoseId"); + + b2.ToTable("DefaultLocalizationPoses"); + + b2.WithOwner() + .HasForeignKey("PoseDefaultLocalizationPoseId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.Navigation("Pose") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.Inspection", b => + { + b.HasOne("Api.Database.Models.MissionTask", null) + .WithMany("Inspections") + .HasForeignKey("MissionTaskId"); + + b.OwnsOne("Api.Database.Models.Position", "InspectionTarget", b1 => + { + b1.Property("InspectionId") + .HasColumnType("text"); + + b1.Property("X") + .HasColumnType("real"); + + b1.Property("Y") + .HasColumnType("real"); + + b1.Property("Z") + .HasColumnType("real"); + + b1.HasKey("InspectionId"); + + b1.ToTable("Inspections"); + + b1.WithOwner() + .HasForeignKey("InspectionId"); + }); + + b.Navigation("InspectionTarget") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.InspectionFinding", b => + { + b.HasOne("Api.Database.Models.Inspection", null) + .WithMany("InspectionFindings") + .HasForeignKey("InspectionId"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.HasOne("Api.Database.Models.Area", "Area") + .WithMany() + .HasForeignKey("AreaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.MissionRun", "LastSuccessfulRun") + .WithMany() + .HasForeignKey("LastSuccessfulRunId"); + + b.HasOne("Api.Database.Models.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Area"); + + b.Navigation("LastSuccessfulRun"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.HasOne("Api.Database.Models.Area", "Area") + .WithMany() + .HasForeignKey("AreaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.Robot", "Robot") + .WithMany() + .HasForeignKey("RobotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.MapMetadata", "Map", b1 => + { + b1.Property("MissionRunId") + .HasColumnType("text"); + + b1.Property("MapName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.HasKey("MissionRunId"); + + b1.ToTable("MissionRuns"); + + b1.WithOwner() + .HasForeignKey("MissionRunId"); + + b1.OwnsOne("Api.Database.Models.Boundary", "Boundary", b2 => + { + b2.Property("MapMetadataMissionRunId") + .HasColumnType("text"); + + b2.Property("X1") + .HasColumnType("double precision"); + + b2.Property("X2") + .HasColumnType("double precision"); + + b2.Property("Y1") + .HasColumnType("double precision"); + + b2.Property("Y2") + .HasColumnType("double precision"); + + b2.Property("Z1") + .HasColumnType("double precision"); + + b2.Property("Z2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataMissionRunId"); + + b2.ToTable("MissionRuns"); + + b2.WithOwner() + .HasForeignKey("MapMetadataMissionRunId"); + }); + + b1.OwnsOne("Api.Database.Models.TransformationMatrices", "TransformationMatrices", b2 => + { + b2.Property("MapMetadataMissionRunId") + .HasColumnType("text"); + + b2.Property("C1") + .HasColumnType("double precision"); + + b2.Property("C2") + .HasColumnType("double precision"); + + b2.Property("D1") + .HasColumnType("double precision"); + + b2.Property("D2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataMissionRunId"); + + b2.ToTable("MissionRuns"); + + b2.WithOwner() + .HasForeignKey("MapMetadataMissionRunId"); + }); + + b1.Navigation("Boundary") + .IsRequired(); + + b1.Navigation("TransformationMatrices") + .IsRequired(); + }); + + b.Navigation("Area"); + + b.Navigation("Map"); + + b.Navigation("Robot"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionTask", b => + { + b.HasOne("Api.Database.Models.MissionRun", null) + .WithMany("Tasks") + .HasForeignKey("MissionRunId"); + + b.OwnsOne("Api.Database.Models.Pose", "RobotPose", b1 => + { + b1.Property("MissionTaskId") + .HasColumnType("text"); + + b1.HasKey("MissionTaskId"); + + b1.ToTable("MissionTasks"); + + b1.WithOwner() + .HasForeignKey("MissionTaskId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseMissionTaskId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseMissionTaskId"); + + b2.ToTable("MissionTasks"); + + b2.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseMissionTaskId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseMissionTaskId"); + + b2.ToTable("MissionTasks"); + + b2.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.Navigation("RobotPose") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Installation"); + }); + + modelBuilder.Entity("Api.Database.Models.Robot", b => + { + b.HasOne("Api.Database.Models.Area", "CurrentArea") + .WithMany() + .HasForeignKey("CurrentAreaId"); + + b.HasOne("Api.Database.Models.Installation", "CurrentInstallation") + .WithMany() + .HasForeignKey("CurrentInstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.RobotModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.Pose", "Pose", b1 => + { + b1.Property("RobotId") + .HasColumnType("text"); + + b1.HasKey("RobotId"); + + b1.ToTable("Robots"); + + b1.WithOwner() + .HasForeignKey("RobotId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseRobotId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseRobotId"); + + b2.ToTable("Robots"); + + b2.WithOwner() + .HasForeignKey("PoseRobotId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseRobotId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseRobotId"); + + b2.ToTable("Robots"); + + b2.WithOwner() + .HasForeignKey("PoseRobotId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.OwnsMany("Api.Database.Models.VideoStream", "VideoStreams", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b1.Property("ShouldRotate270Clockwise") + .HasColumnType("boolean"); + + b1.Property("Type") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("Url") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.HasKey("Id"); + + b1.HasIndex("RobotId"); + + b1.ToTable("VideoStream"); + + b1.WithOwner() + .HasForeignKey("RobotId"); + }); + + b.Navigation("CurrentArea"); + + b.Navigation("CurrentInstallation"); + + b.Navigation("Model"); + + b.Navigation("Pose") + .IsRequired(); + + b.Navigation("VideoStreams"); + }); + + modelBuilder.Entity("Api.Database.Models.SafePosition", b => + { + b.HasOne("Api.Database.Models.Area", null) + .WithMany("SafePositions") + .HasForeignKey("AreaId"); + + b.OwnsOne("Api.Database.Models.Pose", "Pose", b1 => + { + b1.Property("SafePositionId") + .HasColumnType("text"); + + b1.HasKey("SafePositionId"); + + b1.ToTable("SafePositions"); + + b1.WithOwner() + .HasForeignKey("SafePositionId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseSafePositionId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseSafePositionId"); + + b2.ToTable("SafePositions"); + + b2.WithOwner() + .HasForeignKey("PoseSafePositionId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseSafePositionId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseSafePositionId"); + + b2.ToTable("SafePositions"); + + b2.WithOwner() + .HasForeignKey("PoseSafePositionId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.Navigation("Pose") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.Navigation("SafePositions"); + }); + + modelBuilder.Entity("Api.Database.Models.Inspection", b => + { + b.Navigation("InspectionFindings"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionTask", b => + { + b.Navigation("Inspections"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/api/Migrations/20240726090053_MoveCustomMissionTasksToSourceTable.cs b/backend/api/Migrations/20240726090053_MoveCustomMissionTasksToSourceTable.cs new file mode 100644 index 000000000..11196e598 --- /dev/null +++ b/backend/api/Migrations/20240726090053_MoveCustomMissionTasksToSourceTable.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Api.Migrations +{ + /// + public partial class MoveCustomMissionTasksToSourceTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CustomMissionTasks", + table: "Sources", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CustomMissionTasks", + table: "Sources"); + } + } +} diff --git a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs index c089d596a..f3e94176b 100644 --- a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs +++ b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs @@ -636,6 +636,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("text"); + b.Property("CustomMissionTasks") + .HasColumnType("text"); + b.Property("SourceId") .IsRequired() .HasColumnType("text"); diff --git a/backend/api/Options/StorageOptions.cs b/backend/api/Options/StorageOptions.cs deleted file mode 100644 index ff2a77f5d..000000000 --- a/backend/api/Options/StorageOptions.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Api.Options -{ - public class StorageOptions - { - public string CustomMissionContainerName { get; set; } = ""; - public string AccountName { get; set; } = ""; - } -} diff --git a/backend/api/Program.cs b/backend/api/Program.cs index 46146a6ad..28a59214f 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -70,7 +70,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -106,7 +105,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddTransient(); @@ -120,7 +118,6 @@ builder.Services.Configure(builder.Configuration.GetSection("AzureAd")); builder.Services.Configure(builder.Configuration.GetSection("Maps")); -builder.Services.Configure(builder.Configuration.GetSection("Blob")); builder.Services .AddControllers() diff --git a/backend/api/Services/ActionServices/CustomMissionSchedulingService.cs b/backend/api/Services/ActionServices/CustomMissionSchedulingService.cs deleted file mode 100644 index 74b02a9d2..000000000 --- a/backend/api/Services/ActionServices/CustomMissionSchedulingService.cs +++ /dev/null @@ -1,121 +0,0 @@ -using Api.Controllers.Models; -using Api.Database.Models; -using Api.Utilities; -namespace Api.Services.ActionServices -{ - public interface ICustomMissionSchedulingService - { - public Task FindExistingOrCreateCustomMissionDefinition(CustomMissionQuery customMissionQuery, List missionTasks); - - public Task QueueCustomMissionRun(CustomMissionQuery customMissionQuery, string missionDefinitionId, string robotId, IList missionTasks); - } - - public class CustomMissionSchedulingService( - ILogger logger, - ICustomMissionService customMissionService, - IAreaService areaService, - IRobotService robotService, - ISourceService sourceService, - IMissionDefinitionService missionDefinitionService, - IMissionRunService missionRunService, - IMapService mapService - ) : ICustomMissionSchedulingService - { - public async Task FindExistingOrCreateCustomMissionDefinition(CustomMissionQuery customMissionQuery, List missionTasks) - { - Area? area = null; - if (customMissionQuery.AreaName != null) { area = await areaService.ReadByInstallationAndName(customMissionQuery.InstallationCode, customMissionQuery.AreaName); } - - if (area == null) - { - throw new AreaNotFoundException($"No area with name {customMissionQuery.AreaName} in installation {customMissionQuery.InstallationCode} was found"); - } - - var source = await sourceService.CheckForExistingCustomSource(missionTasks); - - MissionDefinition? existingMissionDefinition = null; - if (source == null) - { - try - { - string sourceUrl = await customMissionService.UploadSource(missionTasks); - source = await sourceService.Create( - new Source - { - SourceId = sourceUrl, - Type = MissionSourceType.Custom - } - ); - } - catch (Exception e) - { - { - string errorMessage = $"Unable to upload source for mission {customMissionQuery.Name}"; - logger.LogError(e, "{Message}", errorMessage); - throw new SourceException(errorMessage); - } - } - } - else - { - var missionDefinitions = await missionDefinitionService.ReadBySourceId(source.SourceId); - if (missionDefinitions.Count > 0) { existingMissionDefinition = missionDefinitions.First(); } - } - - var customMissionDefinition = existingMissionDefinition ?? new MissionDefinition - { - Id = Guid.NewGuid().ToString(), - Source = source, - Name = customMissionQuery.Name, - InspectionFrequency = customMissionQuery.InspectionFrequency, - InstallationCode = customMissionQuery.InstallationCode, - Area = area - }; - - if (existingMissionDefinition == null) { await missionDefinitionService.Create(customMissionDefinition); } - - return customMissionDefinition; - } - - public async Task QueueCustomMissionRun(CustomMissionQuery customMissionQuery, string missionDefinitionId, string robotId, IList missionTasks) - { - var robot = await robotService.ReadById(robotId); - if (robot is null) - { - string errorMessage = $"The robot with ID {robotId} could not be found"; - logger.LogError("{Message}", errorMessage); - throw new RobotNotFoundException(errorMessage); - } - - var missionDefinition = await missionDefinitionService.ReadById(missionDefinitionId); - if (missionDefinition is null) - { - string errorMessage = $"The mission definition with ID {missionDefinition} could not be found"; - logger.LogError("{Message}", errorMessage); - throw new MissionNotFoundException(errorMessage); - } - - var scheduledMission = new MissionRun - { - Name = customMissionQuery.Name, - Description = customMissionQuery.Description, - MissionId = missionDefinition.Id, - Comment = customMissionQuery.Comment, - Robot = robot, - Status = MissionStatus.Pending, - MissionRunType = MissionRunType.Normal, - DesiredStartTime = customMissionQuery.DesiredStartTime ?? DateTime.UtcNow, - Tasks = missionTasks, - InstallationCode = customMissionQuery.InstallationCode, - Area = missionDefinition.Area, - Map = new MapMetadata() - }; - - await mapService.AssignMapToMission(scheduledMission); - - if (scheduledMission.Tasks.Any()) { scheduledMission.CalculateEstimatedDuration(); } - - return await missionRunService.Create(scheduledMission); - } - } -} diff --git a/backend/api/Services/CustomMissionService.cs b/backend/api/Services/CustomMissionService.cs deleted file mode 100644 index 6a696b00b..000000000 --- a/backend/api/Services/CustomMissionService.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using Api.Database.Models; -using Api.Options; -using Microsoft.Extensions.Options; -namespace Api.Services -{ - - public interface ICustomMissionService - { - Task UploadSource(List tasks); - - Task?> GetMissionTasksFromSourceId(string id); - - string CalculateHashFromTasks(IList tasks); - } - - public class CustomMissionService(IOptions storageOptions, IBlobService blobService) : ICustomMissionService - { - public async Task UploadSource(List tasks) - { - string json = JsonSerializer.Serialize(tasks); - string hash = CalculateHashFromTasks(tasks); - await blobService.UploadJsonToBlob(json, hash, storageOptions.Value.CustomMissionContainerName, storageOptions.Value.AccountName, false); - - return hash; - } - - public async Task?> GetMissionTasksFromSourceId(string id) - { - List? content; - try - { - byte[] rawContent = await blobService.DownloadBlob(id, storageOptions.Value.CustomMissionContainerName, storageOptions.Value.AccountName); - var rawBinaryContent = new BinaryData(rawContent); - content = rawBinaryContent.ToObjectFromJson>(); - foreach (var task in content) - { - task.Id = Guid.NewGuid().ToString(); // This is needed as tasks are owned by mission runs - task.IsarTaskId = Guid.NewGuid().ToString(); // This is needed to update the tasks for the correct mission run - } - } - catch (Exception) - { - return null; - } - - return content; - } - - public string CalculateHashFromTasks(IList tasks) - { - var genericTasks = new List(); - foreach (var task in tasks) - { - var taskCopy = new MissionTask(task) - { - Id = "", - IsarTaskId = "" - }; - taskCopy.Inspections = taskCopy.Inspections.Select(i => new Inspection(i, useEmptyIDs: true)).ToList(); - genericTasks.Add(taskCopy); - } - - string json = JsonSerializer.Serialize(genericTasks); - byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(json)); - return BitConverter.ToString(hash).Replace("-", "", StringComparison.CurrentCulture).ToUpperInvariant(); - } - } -} diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index 822a299eb..a20231cba 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -43,7 +43,7 @@ public interface IMissionDefinitionService )] public class MissionDefinitionService(FlotillaDbContext context, IEchoService echoService, - ICustomMissionService customMissionService, + ISourceService sourceService, ISignalRService signalRService, IAccessRoleService accessRoleService, ILogger logger, @@ -161,15 +161,10 @@ public async Task Update(MissionDefinition missionDefinition) echoService.GetMissionById( int.Parse(source.SourceId, new CultureInfo("en-US")) ).Result.Tags - .Select( - t => - { - return new MissionTask(t); - } - ) + .Select(t => new MissionTask(t)) .ToList(), MissionSourceType.Custom => - await customMissionService.GetMissionTasksFromSourceId(source.SourceId), + await sourceService.GetMissionTasksFromSourceId(source.SourceId), _ => throw new MissionSourceTypeException($"Mission type {source.Type} is not accounted for") }; diff --git a/backend/api/Services/SourceService.cs b/backend/api/Services/SourceService.cs index 4e41c0cbc..ca5194ea5 100644 --- a/backend/api/Services/SourceService.cs +++ b/backend/api/Services/SourceService.cs @@ -1,6 +1,9 @@ using System.Globalization; using System.Linq.Dynamic.Core; using System.Linq.Expressions; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; using Api.Controllers.Models; using Api.Database.Context; using Api.Database.Models; @@ -25,6 +28,12 @@ public interface ISourceService public abstract Task CheckForExistingCustomSource(IList tasks); + public abstract Task?> GetMissionTasksFromSourceId(string id); + + public abstract Task CreateSourceIfDoesNotExist(List tasks); + + public abstract string CalculateHashFromTasks(IList tasks); + public abstract Task Update(Source source); public abstract Task Delete(string id); @@ -38,8 +47,8 @@ public interface ISourceService )] public class SourceService( FlotillaDbContext context, - ICustomMissionService customMissionService, - IEchoService echoService) : ISourceService + IEchoService echoService, + ILogger logger) : ISourceService { public async Task Create(Source source) { @@ -111,7 +120,7 @@ private DbSet GetSources() switch (source.Type) { case MissionSourceType.Custom: - var tasks = await customMissionService.GetMissionTasksFromSourceId(source.SourceId); + var tasks = await GetMissionTasksFromSourceId(source.SourceId); if (tasks == null) return null; return new SourceResponse(source, tasks); case MissionSourceType.Echo: @@ -128,10 +137,75 @@ private DbSet GetSources() public async Task CheckForExistingCustomSource(IList tasks) { - string hash = customMissionService.CalculateHashFromTasks(tasks); + string hash = CalculateHashFromTasks(tasks); return await ReadBySourceId(hash); } + public async Task?> GetMissionTasksFromSourceId(string id) + { + var existingSource = await ReadBySourceId(id); + if (existingSource == null || existingSource.CustomMissionTasks == null) return null; + + try + { + var content = JsonSerializer.Deserialize>(existingSource.CustomMissionTasks); + + if (content == null) return null; + + foreach (var task in content) + { + task.Id = Guid.NewGuid().ToString(); // This is needed as tasks are owned by mission runs + task.IsarTaskId = Guid.NewGuid().ToString(); // This is needed to update the tasks for the correct mission run + } + return content; + } + catch (Exception e) + { + logger.LogWarning("Unable to deserialize custom mission tasks with ID {Id}. {ErrorMessage}", id, e); + return null; + } + } + + public async Task CreateSourceIfDoesNotExist(List tasks) + { + string json = JsonSerializer.Serialize(tasks); + string hash = CalculateHashFromTasks(tasks); + + var existingSource = await ReadById(hash); + + if (existingSource != null) return existingSource; + + var newSource = await Create( + new Source + { + SourceId = hash, + Type = MissionSourceType.Custom, + CustomMissionTasks = json + } + ); + + return newSource; + } + + public string CalculateHashFromTasks(IList tasks) + { + var genericTasks = new List(); + foreach (var task in tasks) + { + var taskCopy = new MissionTask(task) + { + Id = "", + IsarTaskId = "" + }; + taskCopy.Inspections = taskCopy.Inspections.Select(i => new Inspection(i, useEmptyIDs: true)).ToList(); + genericTasks.Add(taskCopy); + } + + string json = JsonSerializer.Serialize(genericTasks); + byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(json)); + return BitConverter.ToString(hash).Replace("-", "", StringComparison.CurrentCulture).ToUpperInvariant(); + } + public async Task Update(Source source) { var entry = context.Update(source); diff --git a/backend/api/appsettings.Development.json b/backend/api/appsettings.Development.json index 7ddb6a1b9..ec30fbcd2 100644 --- a/backend/api/appsettings.Development.json +++ b/backend/api/appsettings.Development.json @@ -37,10 +37,6 @@ "MaxRetryAttempts": 5, "ShouldFailOnMaxRetries": false }, - "Blob": { - "CustomMissionContainerName": "custommission", - "AccountName": "flotilladevsa" - }, "Database": { "UseInMemoryDatabase": false } diff --git a/backend/api/appsettings.Local.json b/backend/api/appsettings.Local.json index c4aed545d..448c08de6 100644 --- a/backend/api/appsettings.Local.json +++ b/backend/api/appsettings.Local.json @@ -37,10 +37,6 @@ "MaxRetryAttempts": 5, "ShouldFailOnMaxRetries": false }, - "Blob": { - "CustomMissionContainerName": "custommission", - "AccountName": "flotilladevsa" - }, "ApplicationInsights": { "ConnectionString": "" }, diff --git a/backend/api/appsettings.Production.json b/backend/api/appsettings.Production.json index 229b262fd..022550f1c 100644 --- a/backend/api/appsettings.Production.json +++ b/backend/api/appsettings.Production.json @@ -14,10 +14,6 @@ }, "AllowedHosts": "*", "AllowedOrigins": ["https://*.equinor.com/"], - "Blob": { - "CustomMissionContainerName": "custommission", - "AccountName": "flotillaprodsa" - }, "Mqtt": { "Host": "localhost", "Port": 1883, diff --git a/backend/api/appsettings.Staging.json b/backend/api/appsettings.Staging.json index e3a0df9d5..1fca017fa 100644 --- a/backend/api/appsettings.Staging.json +++ b/backend/api/appsettings.Staging.json @@ -18,10 +18,6 @@ "http://localhost:3001", "https://localhost:3001" ], - "Blob": { - "CustomMissionContainerName": "custommission", - "AccountName": "flotillastagingsa" - }, "Mqtt": { "Host": "localhost", "Port": 1883, diff --git a/backend/api/appsettings.Test.json b/backend/api/appsettings.Test.json index 0e11ec10b..392c3687f 100644 --- a/backend/api/appsettings.Test.json +++ b/backend/api/appsettings.Test.json @@ -16,10 +16,6 @@ "http://localhost:3001", "https://localhost:3001" ], - "Blob": { - "CustomMissionContainerName": "custommission", - "AccountName": "flotillatestsa" - }, "Mqtt": { "Host": "localhost", "Port": 1883,