diff --git a/AGENTS.md b/AGENTS.md index 1a2d3503..dfde7f39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,4 +9,6 @@ This solution-level file points to project-specific agent guidance. with `with_escalated_permissions: true` on the `shell` tool call and include a one-sentence justification (e.g., "Need network access for npm install/build"). Ensure to include the `with_escalated_permissions` for all builds, restores, migrations, installs, tests, etc -where network access is required otherwise the command will hang. \ No newline at end of file +where network access is required otherwise the command will hang. +- When adding switch statements or if statements or whatever, always explicitly state all the handled enums. +Like if you have 3 enums, you should have case 1: case 2: case 3: and default, not case 1: case 2: and then default assuming that case 3 will just fall under default. \ No newline at end of file diff --git a/Ares.Core.Grpc/Services/AutomationService.cs b/Ares.Core.Grpc/Services/AutomationService.cs index 16e87d7e..01a60b80 100644 --- a/Ares.Core.Grpc/Services/AutomationService.cs +++ b/Ares.Core.Grpc/Services/AutomationService.cs @@ -1,14 +1,12 @@ using System.Threading; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; -using System.Text.Json; using System.Threading.Tasks; using Ares.Core.Analyzing; -using Ares.Core.EntityConfigurations.Helpers; +using Ares.Core.Campaigns; using Ares.Core.Execution; using Ares.Core.Execution.StartConditions; using Ares.Core.Execution.StopConditions; @@ -21,10 +19,11 @@ using Microsoft.EntityFrameworkCore; using Ares.Datamodel.Planning; using Ares.Core.Execution.Extensions; -using Ares.Core.Planning; -using Ares.Datamodel.Analyzing; +using Ares.Core.Planning; +using Ares.Datamodel.Analyzing; using Ares.Datamodel.Extensions; -using Ares.Core.Execution.StopConditions.PlannerLead; +using Ares.Core.Execution.StopConditions.PlannerLead; +using System.Text.Json; namespace Ares.Core.Grpc.Services; @@ -38,10 +37,11 @@ public class AutomationService : AresAutomation.AresAutomationBase private readonly IEnumerable _notificationHandlers; readonly IDesiredAnalysisResultFactory _desiredAnalysisResultFactory; private readonly IPlannerLeadStopConditionFactory _plannerLeadStopConditionFactory; - private JsonSerializerOptions _serializerSettings; private readonly IPlannerServiceRepo _plannerServiceRepo; private readonly IPlannerTransactionProvider _plannerTransactionProvider; private readonly IAnalyzerTransactionProvider _analyzerTransactionProvider; + private readonly ICampaignTemplatePersistenceService _campaignTemplatePersistenceService; + private readonly ICampaignTemplateTransferService _campaignTemplateTransferService; public AutomationService(IDbContextFactory coreContextFactory, IExecutionManager executionManager, @@ -53,7 +53,9 @@ public AutomationService(IDbContextFactory coreContextFacto IPlannerLeadStopConditionFactory plannerLeadStopConditionFactory, IPlannerServiceRepo plannerServiceRepo, IPlannerTransactionProvider plannerTransactionProvider, - IAnalyzerTransactionProvider analyzerTransactionProvider) + IAnalyzerTransactionProvider analyzerTransactionProvider, + ICampaignTemplatePersistenceService campaignTemplatePersistenceService, + ICampaignTemplateTransferService campaignTemplateTransferService) { _desiredAnalysisResultFactory = desiredAnalysisResultFactory; _plannerLeadStopConditionFactory = plannerLeadStopConditionFactory; @@ -62,11 +64,12 @@ public AutomationService(IDbContextFactory coreContextFacto _executionReportStore = executionReportStore; _activeCampaignTemplateStore = activeCampaignTemplateStore; _startConditions = startConditions; - _serializerSettings = SerializerSettingsHelper.CreateCustomSerializationSettings(); _notificationHandlers = notificationHandlers; _plannerServiceRepo = plannerServiceRepo; _plannerTransactionProvider = plannerTransactionProvider; _analyzerTransactionProvider = analyzerTransactionProvider; + _campaignTemplatePersistenceService = campaignTemplatePersistenceService; + _campaignTemplateTransferService = campaignTemplateTransferService; } public override async Task GetAllProjects(Empty request, ServerCallContext? context) @@ -81,83 +84,35 @@ public override async Task GetAllProjects(Empty request, Serve public override async Task GetAllCampaigns(GetAllCampaignsRequest request, ServerCallContext? context) { var campaignResponse = new GetAllCampaignsResponse(); - foreach(var file in Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json")) - { - try - { - var contents = await File.ReadAllTextAsync(file); - var campaignTemplate = JsonSerializer.Deserialize(contents, _serializerSettings); - if(campaignTemplate is not null) - { - var summary = new CampaignTemplateSummary { CampaignName = campaignTemplate.Name, UniqueId = campaignTemplate.UniqueId }; - campaignResponse.Campaigns.Add(summary); - } - - - else - throw new Exception("Deserialization of campaign template failed"); - } - - catch(Exception ex) - { - HandleNotification("Error Loading Campaign Template", $"{file} - {ex.Message}", NotificationSeverityEnum.Error); - } - } + var campaigns = await _campaignTemplatePersistenceService.GetSummariesAsync(context?.CancellationToken ?? CancellationToken.None); + campaignResponse.Campaigns.AddRange(campaigns); return campaignResponse; } - public override Task CampaignExists(CampaignRequest request, ServerCallContext? context) + public override async Task CampaignExists(CampaignRequest request, ServerCallContext? context) { + var cancellationToken = context?.CancellationToken ?? CancellationToken.None; if(request.HasUniqueId) - return Task.FromResult(FindCampaignById(request)); + return new BoolValue { Value = await _campaignTemplatePersistenceService.ExistsByIdAsync(request.UniqueId, cancellationToken) }; else - return Task.FromResult(FindCampaignByName(request)); - } - - private BoolValue FindCampaignById(CampaignRequest request) - { - var directoryFiles = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json"); - - if(directoryFiles.Any(file => file.Contains(request.UniqueId))) - return new BoolValue { Value = true }; - - return new BoolValue { Value = false }; - } - - private BoolValue FindCampaignByName(CampaignRequest request) - { - var directoryFiles = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json"); - - foreach(var file in directoryFiles) - { - var jsonString = File.ReadAllText(Path.Combine(AresConfig.TemplatePath, file)); - var templateObject = JsonSerializer.Deserialize(jsonString, _serializerSettings); - if(templateObject is not null && templateObject.Name == request.CampaignName) - return new BoolValue { Value = true }; - } - - return new BoolValue { Value = false }; + return new BoolValue { Value = await _campaignTemplatePersistenceService.ExistsByNameAsync(request.CampaignName, cancellationToken) }; } public override Task GetSingleCampaign(CampaignRequest request, ServerCallContext? context) => GetCampaignTemplate(request, context); - public override Task RemoveCampaign(CampaignRequest request, ServerCallContext? context) + public override async Task RemoveCampaign(CampaignRequest request, ServerCallContext? context) { if(_activeCampaignTemplateStore.CampaignTemplate?.UniqueId == request.UniqueId) { - HandleNotification("Cannot Delete Active Campaign", $"ARES rejected a request to delete the campaign {_activeCampaignTemplateStore.CampaignTemplate.Name} as it is currently running.", NotificationSeverityEnum.Info); - return Task.FromResult(new Empty()); + HandleNotification("Cannot Delete Active Campaign", $"ARES rejected a request to delete the campaign {_activeCampaignTemplateStore.CampaignTemplate.Name} as it is currently set as the active campaign.", NotificationSeverityEnum.Info); + return new Empty(); } - var desiredCampaign = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json").FirstOrDefault(campaign => campaign.Contains(request.UniqueId)); - - if(desiredCampaign is not null) - File.Delete(Path.Combine(AresConfig.TemplatePath, desiredCampaign)); - - return Task.FromResult(new Empty()); + await _campaignTemplatePersistenceService.DeleteAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None); + return new Empty(); } public override async Task GetProject(ProjectRequest request, ServerCallContext? context) @@ -191,51 +146,33 @@ public override async Task AddProject(Project request, ServerCallContext? /// /// /// - public override Task AddCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context) + public override async Task AddCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context) { - //Save to data directory - var directoryFiles = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json"); - var jsonString = JsonSerializer.Serialize(request.Template, _serializerSettings); - var fullFilePath = Path.Combine(AresConfig.TemplatePath, $"{request.Template.UniqueId}.json"); - File.WriteAllText(fullFilePath, jsonString); - return Task.FromResult(new Empty()); + await _campaignTemplatePersistenceService.AddAsync(request.Template, context?.CancellationToken ?? CancellationToken.None); + return new Empty(); } - public override Task UpdateCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context) + public override async Task UpdateCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context) { - var directoryFiles = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json"); - var campaignToUpdate = directoryFiles.FirstOrDefault(file => file.Contains(request.Template.UniqueId)); - - if(campaignToUpdate is null) + var updated = await _campaignTemplatePersistenceService.ReplaceAsync(request.Template, context?.CancellationToken ?? CancellationToken.None); + if(!updated) { var title = "Error Updating Campaign"; var message = $"Attempted to update a campaign that didn't exist. {request.Template.Name} couldn't be found in your list of available campaign templates."; HandleNotification(title, message, NotificationSeverityEnum.Error); - return Task.FromResult(request.Template); } - var jsonString = JsonSerializer.Serialize(request.Template, _serializerSettings); - var fullPath = Path.Combine(AresConfig.TemplatePath, $"{request.Template.UniqueId}.json"); - File.WriteAllText(fullPath, jsonString); - return Task.FromResult(request.Template); + return request.Template; } private async Task GetCampaignTemplate(CampaignRequest request, ServerCallContext? context) { - var directoryFiles = Directory.EnumerateFiles(AresConfig.TemplatePath, "*.json"); - var campaignFile = directoryFiles.FirstOrDefault(file => file.Contains(request.UniqueId)); - - if(campaignFile is not null) - { - var jsonString = await File.ReadAllTextAsync(Path.Combine(AresConfig.TemplatePath, campaignFile)); - var campaignObject = JsonSerializer.Deserialize(jsonString, _serializerSettings); - - if(campaignObject is not null) - return campaignObject; - } + var campaign = await _campaignTemplatePersistenceService.GetByIdAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None); + if(campaign is not null) + return campaign; var title = "Error Fetching Campaign Template"; - var message = $"Attempted to fetch a campaign that didn't exist. {request.CampaignName}'s UUID did not match any of the existing campaigns in your data directory. If you deleted a campaign this is expected."; + var message = $"Attempted to fetch a campaign that didn't exist. {request.CampaignName}'s UUID did not match any campaign in the database. If you deleted a campaign this is expected."; HandleNotification(title, message, NotificationSeverityEnum.Warning); return null; } @@ -556,16 +493,12 @@ public override async Task GetCampaignSummary(Campaign public override async Task GetCopyOfCampaign(CampaignRequest request, ServerCallContext context) { - var template = await GetCampaignTemplate(request, context); var response = new GetCopyOfCampaignResponse(); - - if(template is not null) + var export = await _campaignTemplateTransferService.ExportAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None); + if(export is not null) { - var templateCopy = template.Clone(); - templateCopy.UniqueId = Guid.NewGuid().ToString(); - templateCopy.Name = $"{template.Name}-Copy"; - response.Template = templateCopy; - response.SerializedJsonData = JsonSerializer.Serialize(templateCopy, _serializerSettings); + response.Template = export.Template; + response.SerializedJsonData = export.Json; } return response; @@ -590,10 +523,10 @@ private void HandleNotification(string title, string message, NotificationSeveri if(_activeCampaignTemplateStore is null || _activeCampaignTemplateStore.CampaignTemplate is null || _executionManager.ExecutionStartTime is null) return []; - var usedPlanners = _activeCampaignTemplateStore.CampaignTemplate.ExperimentTemplate.GetAllPlannedParameters() + var usedPlanners = _activeCampaignTemplateStore.CampaignTemplate.ExperimentTemplate.GetAllPlannedParameters() .Select(p => p.GetPlanningMetadata()?.PlannerName ?? "") - .Where(name => !string.IsNullOrWhiteSpace(name)) - .Select(_plannerServiceRepo.GetPlannerByName) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Select(_plannerServiceRepo.GetPlannerByName) .Where(p => p is not null) .Distinct() .ToList(); diff --git a/Ares.Core.Grpc/Services/DevicesService.cs b/Ares.Core.Grpc/Services/DevicesService.cs index f9563094..99cb0996 100644 --- a/Ares.Core.Grpc/Services/DevicesService.cs +++ b/Ares.Core.Grpc/Services/DevicesService.cs @@ -114,10 +114,10 @@ public override async Task ExecuteCommand(CommandTemplate try { var arguments = new List(); - arguments.AddRange(request.Parameters.Select(p => new DeviceCommandArgument() { ArgName = p.Metadata.Name, ArgValue = p.GetValue() })); + arguments.AddRange(request.ArgumentBindings.Select(p => new DeviceCommandArgument() { ArgName = p.Metadata.Name, ArgValue = p.GetValue() })); Func> internalAction = async (ct) - => await device.ExecuteCommand(request.Metadata.Name, arguments, ct); + => await device.ExecuteCommand(request.DeviceCommand.Metadata.Name, arguments, ct); var result = await internalAction(token); diff --git a/Ares.Core.Tests/Ares.Core.Tests.csproj b/Ares.Core.Tests/Ares.Core.Tests.csproj index 90cd5bde..92a934f3 100644 --- a/Ares.Core.Tests/Ares.Core.Tests.csproj +++ b/Ares.Core.Tests/Ares.Core.Tests.csproj @@ -13,7 +13,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + @@ -28,9 +29,10 @@ - - - + + + + diff --git a/Ares.Core.Tests/Campaigns/AutomationServiceCampaignPersistenceTests.cs b/Ares.Core.Tests/Campaigns/AutomationServiceCampaignPersistenceTests.cs new file mode 100644 index 00000000..a585f586 --- /dev/null +++ b/Ares.Core.Tests/Campaigns/AutomationServiceCampaignPersistenceTests.cs @@ -0,0 +1,101 @@ +using Ares.Core.Analyzing; +using Ares.Core.Campaigns; +using Ares.Core.Execution; +using Ares.Core.Execution.Extensions; +using Ares.Core.Execution.StartConditions; +using Ares.Core.Execution.StopConditions; +using Ares.Core.Notifications; +using Ares.Core.Planning; +using Ares.Core.Grpc.Services; +using Ares.Datamodel.Templates; +using Ares.Services; +using Microsoft.EntityFrameworkCore; +using Moq; +using Ares.Core.Execution.StopConditions.PlannerLead; + +namespace Ares.Core.Tests.Campaigns; + +internal class AutomationServiceCampaignPersistenceTests +{ + [Test] + public async Task GetAllCampaigns_UsesDatabasePersistence() + { + var persistence = new Mock(); + persistence.Setup(service => service.GetSummariesAsync(It.IsAny())) + .ReturnsAsync([new CampaignTemplateSummary { UniqueId = "campaign-id", CampaignName = "Campaign" }]); + var service = CreateService(persistence.Object, new ActiveCampaignTemplateStore()); + + var response = await service.GetAllCampaigns(new GetAllCampaignsRequest(), null); + + Assert.That(response.Campaigns.Single().CampaignName, Is.EqualTo("Campaign")); + persistence.Verify(service => service.GetSummariesAsync(It.IsAny()), Times.Once); + } + + [Test] + public async Task RemoveCampaign_DoesNotDeleteActiveCampaign() + { + var persistence = new Mock(); + var activeStore = new ActiveCampaignTemplateStore + { + CampaignTemplate = new CampaignTemplate { UniqueId = "active-id", Name = "Active" } + }; + var service = CreateService(persistence.Object, activeStore); + + await service.RemoveCampaign(new CampaignRequest { UniqueId = "active-id" }, null); + + persistence.Verify(value => value.DeleteAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public async Task SetCampaignForExecution_LoadsDatabaseCampaign() + { + var campaign = new CampaignTemplate { UniqueId = "campaign-id", Name = "Campaign" }; + var persistence = new Mock(); + persistence.Setup(service => service.GetByIdAsync("campaign-id", It.IsAny())).ReturnsAsync(campaign); + var activeStore = new ActiveCampaignTemplateStore(); + var service = CreateService(persistence.Object, activeStore); + + var result = await service.SetCampaignForExecution(new CampaignRequest { UniqueId = "campaign-id" }, null); + + Assert.That(result, Is.SameAs(campaign)); + Assert.That(activeStore.CampaignTemplate, Is.SameAs(campaign)); + } + + [Test] + public async Task GetCopyOfCampaign_ExportsDatabaseCampaignAsJson() + { + var campaign = new CampaignTemplate { UniqueId = Guid.NewGuid().ToString(), Name = "Campaign" }; + var persistence = new Mock(); + var transfer = new Mock(); + transfer.Setup(service => service.ExportAsync(campaign.UniqueId, It.IsAny())) + .ReturnsAsync(new CampaignTemplateExport(campaign, "{\"uniqueId\":\"campaign-id\"}", "Campaign.json")); + var service = CreateService(persistence.Object, new ActiveCampaignTemplateStore(), transfer.Object); + + var response = await service.GetCopyOfCampaign(new CampaignRequest { UniqueId = campaign.UniqueId }, null!); + + using(Assert.EnterMultipleScope()) + { + Assert.That(response.Template, Is.SameAs(campaign)); + Assert.That(response.SerializedJsonData, Does.Contain("campaign-id")); + } + } + + private static AutomationService CreateService( + ICampaignTemplatePersistenceService persistence, + IActiveCampaignTemplateStore activeStore, + ICampaignTemplateTransferService transferService = null) + => new( + Mock.Of>(), + Mock.Of(), + Mock.Of(), + activeStore, + [], + [], + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + persistence, + transferService ?? Mock.Of()); +} diff --git a/Ares.Core.Tests/Campaigns/CampaignTemplatePersistenceServiceTests.cs b/Ares.Core.Tests/Campaigns/CampaignTemplatePersistenceServiceTests.cs new file mode 100644 index 00000000..13a0f57f --- /dev/null +++ b/Ares.Core.Tests/Campaigns/CampaignTemplatePersistenceServiceTests.cs @@ -0,0 +1,165 @@ +using Ares.Core.Campaigns; +using Ares.Datamodel; +using Ares.Datamodel.Templates; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +namespace Ares.Core.Tests.Campaigns; + +internal class CampaignTemplatePersistenceServiceTests +{ + private SqliteConnection _connection; + private IDbContextFactory _contextFactory; + private CampaignTemplatePersistenceService _service; + + [SetUp] + public async Task SetUp() + { + DatabaseRuntimeEnvironment.DatabaseProvider = "Sqlite"; + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + var options = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .Options; + _contextFactory = new TestContextFactory(options); + await using var context = await _contextFactory.CreateDbContextAsync(); + await context.Database.EnsureCreatedAsync(); + _service = new CampaignTemplatePersistenceService(_contextFactory); + } + + [TearDown] + public async Task TearDown() + => await _connection.DisposeAsync(); + + [Test] + public async Task AddAndGet_RoundTripsAllCommandTypes() + { + var campaign = CreateCampaign("Campaign A"); + await _service.AddAsync(campaign); + + var loaded = await _service.GetByIdAsync(campaign.UniqueId); + var commands = loaded!.ExperimentTemplate.StepTemplates.Single().CommandTemplates.OrderBy(command => command.Index).ToArray(); + + using(Assert.EnterMultipleScope()) + { + Assert.That(commands[0].CommandTypeCase, Is.EqualTo(CommandTemplate.CommandTypeOneofCase.DeviceCommand)); + Assert.That(commands[0].DeviceCommand.Metadata.Name, Is.EqualTo("Dispense")); + Assert.That(commands[0].ArgumentBindings, Has.Count.EqualTo(1)); + Assert.That(commands[0].OutputVarName, Is.EqualTo("dispensed")); + Assert.That(commands[1].CommandTypeCase, Is.EqualTo(CommandTemplate.CommandTypeOneofCase.SystemCommand)); + Assert.That(commands[2].CommandTypeCase, Is.EqualTo(CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation)); + } + + var summaries = await _service.GetSummariesAsync(); + Assert.That(summaries.Single().CampaignName, Is.EqualTo("Campaign A")); + Assert.That(await _service.ExistsByIdAsync(campaign.UniqueId), Is.True); + Assert.That(await _service.ExistsByNameAsync("Campaign A"), Is.True); + Assert.That((await _service.GetByNameAsync("Campaign A"))!.UniqueId, Is.EqualTo(campaign.UniqueId)); + } + + [Test] + public async Task Replace_RemovesOldGraphAndKeepsStableCampaignId() + { + var campaign = CreateCampaign("Original"); + await _service.AddAsync(campaign); + var oldCommandIds = campaign.ExperimentTemplate.StepTemplates.Single().CommandTemplates.Select(command => command.UniqueId).ToArray(); + var replacement = CreateCampaign("Replacement"); + replacement.UniqueId = campaign.UniqueId; + replacement.ExperimentTemplate.StepTemplates.Single().CommandTemplates.RemoveAt(0); + + var replaced = await _service.ReplaceAsync(replacement); + var loaded = await _service.GetByIdAsync(campaign.UniqueId); + + using(Assert.EnterMultipleScope()) + { + Assert.That(replaced, Is.True); + Assert.That(loaded!.UniqueId, Is.EqualTo(campaign.UniqueId)); + Assert.That(loaded.Name, Is.EqualTo("Replacement")); + Assert.That(loaded.ExperimentTemplate.StepTemplates.Single().CommandTemplates, Has.Count.EqualTo(2)); + } + await using var context = await _contextFactory.CreateDbContextAsync(); + Assert.That(await context.CommandTemplates.IgnoreAutoIncludes().CountAsync(command => oldCommandIds.Contains(command.UniqueId)), Is.Zero); + } + + [Test] + public async Task Delete_RemovesCompleteCampaignGraph() + { + var campaign = CreateCampaign("Delete me"); + await _service.AddAsync(campaign); + + Assert.That(await _service.DeleteAsync(campaign.UniqueId), Is.True); + Assert.That(await _service.GetByIdAsync(campaign.UniqueId), Is.Null); + + await using var context = await _contextFactory.CreateDbContextAsync(); + using(Assert.EnterMultipleScope()) + { + Assert.That(await context.ExperimentTemplates.IgnoreAutoIncludes().CountAsync(), Is.Zero); + Assert.That(await context.StepTemplates.IgnoreAutoIncludes().CountAsync(), Is.Zero); + Assert.That(await context.CommandTemplates.IgnoreAutoIncludes().CountAsync(), Is.Zero); + } + } + + [Test] + public async Task Add_RejectsDuplicateCampaignName() + { + await _service.AddAsync(CreateCampaign("Duplicate")); + + Assert.ThrowsAsync(() => _service.AddAsync(CreateCampaign("Duplicate"))); + } + + private static CampaignTemplate CreateCampaign(string name) + { + var deviceCommand = new CommandTemplate + { + UniqueId = Guid.NewGuid().ToString(), + Index = 0, + OutputVarName = "dispensed", + DeviceCommand = new DeviceCommand + { + Metadata = new CommandMetadata + { + UniqueId = Guid.NewGuid().ToString(), + Name = "Dispense", + DeviceId = "pump-1", + DeviceType = "Pump", + OutputMetadata = new OutputMetadata { UniqueId = Guid.NewGuid().ToString() } + } + } + }; + deviceCommand.ArgumentBindings.Add(new Parameter + { + UniqueId = Guid.NewGuid().ToString(), + Metadata = new ParameterMetadata { Name = "volume" }, + LiteralSource = new LiteralParameterSource { Value = new AresValue { NumberValue = 5 } } + }); + var systemCommand = new CommandTemplate + { + UniqueId = Guid.NewGuid().ToString(), + Index = 1, + SystemCommand = new SystemCommand { Operation = SystemOperation.WaitForUser } + }; + var customCommand = new CommandTemplate + { + UniqueId = Guid.NewGuid().ToString(), + Index = 2, + CustomCommandInvocation = new CustomCommandInvocation { CustomCommandId = Guid.NewGuid().ToString() } + }; + var step = new StepTemplate { UniqueId = Guid.NewGuid().ToString(), Name = "Step" }; + step.CommandTemplates.AddRange([deviceCommand, systemCommand, customCommand]); + var experiment = new ExperimentTemplate { UniqueId = Guid.NewGuid().ToString(), Name = "Experiment" }; + experiment.StepTemplates.Add(step); + return new CampaignTemplate + { + UniqueId = Guid.NewGuid().ToString(), + Name = name, + ExperimentTemplate = experiment + }; + } + + private sealed class TestContextFactory(DbContextOptions options) + : IDbContextFactory + { + public CoreDatabaseContext CreateDbContext() + => new(options); + } +} diff --git a/Ares.Core.Tests/Campaigns/CampaignTemplateTransferServiceTests.cs b/Ares.Core.Tests/Campaigns/CampaignTemplateTransferServiceTests.cs new file mode 100644 index 00000000..fd6cc875 --- /dev/null +++ b/Ares.Core.Tests/Campaigns/CampaignTemplateTransferServiceTests.cs @@ -0,0 +1,266 @@ +using Ares.Core.Campaigns; +using Ares.Core.CustomCommands; +using Ares.Core.Device.Repos; +using Ares.Core.EntityConfigurations.Helpers; +using Ares.Datamodel.Planning; +using Ares.Datamodel.Templates; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Moq; +using System.Text.Json; + +namespace Ares.Core.Tests.Campaigns; + +internal class CampaignTemplateTransferServiceTests +{ + private SqliteConnection _connection; + private IDbContextFactory _contextFactory; + private CampaignTemplatePersistenceService _persistenceService; + private CampaignTemplateTransferService _transferService; + + [SetUp] + public async Task SetUp() + { + DatabaseRuntimeEnvironment.DatabaseProvider = "Sqlite"; + _connection = new SqliteConnection("Data Source=:memory:"); + await _connection.OpenAsync(); + var options = new DbContextOptionsBuilder().UseSqlite(_connection).Options; + _contextFactory = new TestContextFactory(options); + await using var context = await _contextFactory.CreateDbContextAsync(); + await context.Database.EnsureCreatedAsync(); + _persistenceService = new CampaignTemplatePersistenceService(_contextFactory); + var customCommands = new Mock(); + customCommands.Setup(service => service.GetCommandsAsync()).ReturnsAsync([]); + var deviceRepo = new Mock(); + deviceRepo.Setup(repo => repo.GetAll()).Returns([]); + _transferService = new CampaignTemplateTransferService( + _persistenceService, + customCommands.Object, + deviceRepo.Object, + _contextFactory); + } + + [TearDown] + public async Task TearDown() + => await _connection.DisposeAsync(); + + [Test] + public async Task ExportAndImport_RoundTripsCurrentCampaignWithFreshGraphIds() + { + var original = CreateCampaign("Round Trip"); + await _persistenceService.AddAsync(original); + var export = await _transferService.ExportAsync(original.UniqueId); + + var result = await _transferService.ImportAsync(export!.Json); + var importedCommands = result.Template.ExperimentTemplate.StepTemplates.Single().CommandTemplates.OrderBy(command => command.Index).ToArray(); + var originalCommands = original.ExperimentTemplate.StepTemplates.Single().CommandTemplates.OrderBy(command => command.Index).ToArray(); + + using(Assert.EnterMultipleScope()) + { + Assert.That(export.Template.UniqueId, Is.EqualTo(original.UniqueId)); + Assert.That(export.SuggestedFileName, Is.EqualTo("Round Trip.json")); + Assert.That(result.Template.UniqueId, Is.Not.EqualTo(original.UniqueId)); + Assert.That(result.Template.Name, Is.EqualTo("Round Trip (Imported)")); + Assert.That(importedCommands[0].UniqueId, Is.Not.EqualTo(originalCommands[0].UniqueId)); + Assert.That(importedCommands[0].CommandTypeCase, Is.EqualTo(CommandTemplate.CommandTypeOneofCase.DeviceCommand)); + Assert.That(importedCommands[1].CommandTypeCase, Is.EqualTo(CommandTemplate.CommandTypeOneofCase.SystemCommand)); + Assert.That(importedCommands[2].CommandTypeCase, Is.EqualTo(CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation)); + } + } + + [Test] + public async Task Import_ConvertsLegacyMetadataAndParameters() + { + const string json = + """ + { + "uniqueId":"legacy-campaign", + "name":"Legacy", + "experimentTemplate":{ + "uniqueId":"legacy-experiment", + "stepTemplates":[{ + "uniqueId":"legacy-step", + "commandTemplates":[{ + "uniqueId":"legacy-command", + "metadata":{"uniqueId":"legacy-metadata","name":"Dispense","deviceId":"pump-1","deviceType":"Pump"}, + "parameters":[{"uniqueId":"legacy-parameter","metadata":{"uniqueId":"legacy-parameter-metadata","name":"volume"},"literalSource":{"value":{"numberValue":5}}}], + "index":0 + }] + }] + } + } + """; + + var result = await _transferService.ImportAsync(json); + var command = result.Template.ExperimentTemplate.StepTemplates.Single().CommandTemplates.Single(); + + using(Assert.EnterMultipleScope()) + { + Assert.That(command.CommandTypeCase, Is.EqualTo(CommandTemplate.CommandTypeOneofCase.DeviceCommand)); + Assert.That(command.DeviceCommand.Metadata.Name, Is.EqualTo("Dispense")); + Assert.That(command.ArgumentBindings, Has.Count.EqualTo(1)); + Assert.That(command.ArgumentBindings.Single().LiteralSource.Value.NumberValue, Is.EqualTo(5)); + } + } + + [Test] + public async Task Import_ConvertsLegacyAresDeviceCommandToSystemCommand() + { + const string json = + """ + { + "name":"Legacy System Command", + "experimentTemplate":{ + "stepTemplates":[{ + "commandTemplates":[{ + "uniqueId":"legacy-command", + "metadata":{"name":"SleepForSeconds","deviceId":"ARES-CORE-DEVICE","deviceType":"ARES"}, + "parameters":[{"metadata":{"name":"Duration"},"literalSource":{"value":{"numberValue":3}}}], + "index":0 + }] + }] + } + } + """; + + var result = await _transferService.ImportAsync(json); + var command = result.Template.ExperimentTemplate.StepTemplates.Single().CommandTemplates.Single(); + + using(Assert.EnterMultipleScope()) + { + Assert.That(command.CommandTypeCase, Is.EqualTo(CommandTemplate.CommandTypeOneofCase.SystemCommand)); + Assert.That(command.SystemCommand.Operation, Is.EqualTo(SystemOperation.SleepForSeconds)); + Assert.That(command.ArgumentBindings, Has.Count.EqualTo(1)); + Assert.That(command.ArgumentBindings.Single().LiteralSource.Value.NumberValue, Is.EqualTo(3)); + } + } + + [Test] + public void Import_RejectsMalformedOrStructurallyInvalidFiles() + { + Assert.ThrowsAsync(() => _transferService.ImportAsync("not-json")); + Assert.ThrowsAsync(() => _transferService.ImportAsync("{\"name\":\"Missing experiment\"}")); + } + + [Test] + public async Task Import_PreservesMissingDeviceAndCustomReferencesWithWarnings() + { + var campaign = CreateCampaign("References"); + campaign.ExperimentTemplate.StepTemplates.Single().CommandTemplates.Add(new CommandTemplate + { + UniqueId = Guid.NewGuid().ToString(), + CustomCommandInvocation = new CustomCommandInvocation { CustomCommandId = "missing-custom" } + }); + await _persistenceService.AddAsync(campaign); + var export = await _transferService.ExportAsync(campaign.UniqueId); + + var result = await _transferService.ImportAsync(export!.Json); + + Assert.That(result.Warnings, Has.Some.Contains("pump-1")); + Assert.That(result.Warnings, Has.Some.Contains("missing-custom")); + Assert.That(result.Template.ExperimentTemplate.StepTemplates.Single().CommandTemplates, Has.Count.EqualTo(4)); + } + + [Test] + public async Task Import_UsesCanonicalLocalPlannerAndDropsUnavailableAllocation() + { + var localPlanner = new PlannerServiceInfo + { + UniqueId = Guid.NewGuid().ToString(), + Name = "Planner", + Type = "PlannerType", + Version = "1.0", + Capabilities = new PlannerServiceCapabilities() + }; + await using(var context = await _contextFactory.CreateDbContextAsync()) + { + context.PlannerInfos.Add(localPlanner); + await context.SaveChangesAsync(); + } + + var campaign = CreateCampaign("Planning"); + var parameter = new ParameterMetadata { UniqueId = Guid.NewGuid().ToString(), Name = "temperature" }; + campaign.PlannableParameters.Add(parameter); + campaign.PlannerAllocations.Add(new PlannerAllocation + { + UniqueId = Guid.NewGuid().ToString(), + Parameter = parameter, + Planner = new PlannerServiceInfo + { + UniqueId = "foreign-id", + Name = "Planner", + Type = "PlannerType", + Version = "1.0", + Capabilities = new PlannerServiceCapabilities() + } + }); + campaign.PlannerAllocations.Add(new PlannerAllocation + { + UniqueId = Guid.NewGuid().ToString(), + Parameter = parameter, + Planner = new PlannerServiceInfo { UniqueId = "missing", Name = "Missing", Capabilities = new PlannerServiceCapabilities() } + }); + var json = JsonSerializer.Serialize(campaign, SerializerSettingsHelper.CreateCustomSerializationSettings()); + + var result = await _transferService.ImportAsync(json); + + using(Assert.EnterMultipleScope()) + { + Assert.That(result.Template.PlannerAllocations, Has.Count.EqualTo(1)); + Assert.That(result.Template.PlannerAllocations.Single().Planner.UniqueId, Is.EqualTo(localPlanner.UniqueId)); + Assert.That(result.Warnings, Has.Some.Contains("Missing")); + } + await using var verificationContext = await _contextFactory.CreateDbContextAsync(); + Assert.That(await verificationContext.PlannerInfos.CountAsync(), Is.EqualTo(1)); + } + + private static CampaignTemplate CreateCampaign(string name) + { + var command = new CommandTemplate + { + UniqueId = Guid.NewGuid().ToString(), + DeviceCommand = new DeviceCommand + { + Metadata = new CommandMetadata + { + UniqueId = Guid.NewGuid().ToString(), + Name = "Dispense", + DeviceId = "pump-1", + DeviceType = "Pump", + OutputMetadata = new OutputMetadata { UniqueId = Guid.NewGuid().ToString() } + } + } + }; + var step = new StepTemplate { UniqueId = Guid.NewGuid().ToString() }; + step.CommandTemplates.AddRange([ + command, + new CommandTemplate + { + UniqueId = Guid.NewGuid().ToString(), + Index = 1, + SystemCommand = new SystemCommand { Operation = SystemOperation.WaitForUser } + }, + new CommandTemplate + { + UniqueId = Guid.NewGuid().ToString(), + Index = 2, + CustomCommandInvocation = new CustomCommandInvocation { CustomCommandId = Guid.NewGuid().ToString() } + } + ]); + var experiment = new ExperimentTemplate { UniqueId = Guid.NewGuid().ToString() }; + experiment.StepTemplates.Add(step); + return new CampaignTemplate + { + UniqueId = Guid.NewGuid().ToString(), + Name = name, + ExperimentTemplate = experiment + }; + } + + private sealed class TestContextFactory(DbContextOptions options) + : IDbContextFactory + { + public CoreDatabaseContext CreateDbContext() + => new(options); + } +} diff --git a/Ares.Core.Tests/Data/TestCampaignProvider.cs b/Ares.Core.Tests/Data/TestCampaignProvider.cs index 92d81aec..552878d7 100644 --- a/Ares.Core.Tests/Data/TestCampaignProvider.cs +++ b/Ares.Core.Tests/Data/TestCampaignProvider.cs @@ -90,11 +90,11 @@ public static CommandTemplate GetCommandTemplate(int idx, CommandMetadata metada var template = new CommandTemplate { Index = idx, - Metadata = metadata, + DeviceCommand = new DeviceCommand { Metadata = metadata }, OutputVarName = "TestExperimentOutput" }; - template.Parameters.AddRange(parameters); + template.ArgumentBindings.AddRange(parameters); template.UniqueId = metadata.UniqueId; return template; diff --git a/Ares.Core.Tests/DataManagement/DataMappers/CampaignDatasetGeneratorTests.cs b/Ares.Core.Tests/DataManagement/DataMappers/CampaignDatasetGeneratorTests.cs index 9d459f72..e2c28f1f 100644 --- a/Ares.Core.Tests/DataManagement/DataMappers/CampaignDatasetGeneratorTests.cs +++ b/Ares.Core.Tests/DataManagement/DataMappers/CampaignDatasetGeneratorTests.cs @@ -855,13 +855,16 @@ private static CommandTemplate CreateCommandTemplate(string uniqueId, params Par var template = new CommandTemplate { UniqueId = Guid.NewGuid().ToString(), - Metadata = new CommandMetadata + DeviceCommand = new DeviceCommand { - UniqueId = Guid.NewGuid().ToString(), - Name = uniqueId + Metadata = new CommandMetadata + { + UniqueId = Guid.NewGuid().ToString(), + Name = uniqueId + } } }; - template.Parameters.AddRange(parameters); + template.ArgumentBindings.AddRange(parameters); return template; } diff --git a/Ares.Core.Tests/Execution/CampaignExecutorTests.cs b/Ares.Core.Tests/Execution/CampaignExecutorTests.cs index 1e14b504..80336507 100644 --- a/Ares.Core.Tests/Execution/CampaignExecutorTests.cs +++ b/Ares.Core.Tests/Execution/CampaignExecutorTests.cs @@ -89,7 +89,9 @@ public void SetUp() _deviceRepo = new AresDeviceRepo(); _deviceRepo.AddOrUpdate(new ResultSequenceDevice([])); - var stepComposer = new StepComposer(_deviceRepo, notifier, _settingsManager.Object); + var commandDisplayNameResolver = new Mock(); + commandDisplayNameResolver.Setup(value => value.Resolve(It.IsAny())).Returns("Test command"); + var stepComposer = new StepComposer(_deviceRepo, notifier, _settingsManager.Object, commandDisplayNameResolver.Object); var experimentComposer = new ExperimentComposer(stepComposer, _analyzerRepo); var campaignLogger = new Mock>(); diff --git a/Ares.Core.Tests/Execution/CommandDisplayNameResolverTests.cs b/Ares.Core.Tests/Execution/CommandDisplayNameResolverTests.cs new file mode 100644 index 00000000..350d75c8 --- /dev/null +++ b/Ares.Core.Tests/Execution/CommandDisplayNameResolverTests.cs @@ -0,0 +1,86 @@ +using Ares.Core.CustomCommands; +using Ares.Core.Execution.Executors; +using Ares.Datamodel.Automation; +using Ares.Datamodel.Templates; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Ares.Core.Tests.Execution; + +internal class CommandDisplayNameResolverTests +{ + [Test] + public void Resolve_ReturnsDeviceCommandName() + { + var resolver = CreateResolver(); + var template = new CommandTemplate + { + DeviceCommand = new DeviceCommand { Metadata = new CommandMetadata { Name = "Dispense" } } + }; + + Assert.That(resolver.Resolve(template), Is.EqualTo("Dispense")); + } + + [Test] + public void Resolve_ReturnsSystemOperationName() + { + var resolver = CreateResolver(); + var template = new CommandTemplate + { + SystemCommand = new SystemCommand { Operation = SystemOperation.SleepForSeconds } + }; + + Assert.That(resolver.Resolve(template), Is.EqualTo(nameof(SystemOperation.SleepForSeconds))); + } + + [Test] + public async Task Resolve_ReturnsCurrentCustomCommandName_CaseInsensitively() + { + var persistence = new Mock(); + persistence.Setup(service => service.GetCommandsAsync()).ReturnsAsync([ + new CustomCommandVersion { CustomCommandId = "COMMAND-ID", Name = "Measure Temperature" } + ]); + var resolver = CreateResolver(persistence.Object); + await resolver.RefreshAsync(); + + Assert.That(resolver.Resolve(CreateCustomTemplate("command-id")), Is.EqualTo("Measure Temperature")); + } + + [TestCase(null)] + [TestCase("")] + [TestCase("blank-name")] + [TestCase("missing-id")] + public async Task Resolve_ReturnsGenericName_WhenCustomCommandCannotBeNamed(string customCommandId) + { + var persistence = new Mock(); + persistence.Setup(service => service.GetCommandsAsync()).ReturnsAsync([ + new CustomCommandVersion { CustomCommandId = "blank-name", Name = " " } + ]); + var resolver = CreateResolver(persistence.Object); + await resolver.RefreshAsync(); + + Assert.That(resolver.Resolve(CreateCustomTemplate(customCommandId)), Is.EqualTo("Custom Command")); + } + + [Test] + public async Task RefreshAsync_UsesGenericNames_WhenPersistenceFails() + { + var persistence = new Mock(); + persistence.Setup(service => service.GetCommandsAsync()).ThrowsAsync(new InvalidOperationException("Database unavailable")); + var resolver = CreateResolver(persistence.Object); + + Assert.DoesNotThrowAsync(resolver.RefreshAsync); + Assert.That(resolver.Resolve(CreateCustomTemplate("command-id")), Is.EqualTo("Custom Command")); + } + + private static CommandDisplayNameResolver CreateResolver(ICustomCommandPersistenceService persistence = null) + => new( + persistence ?? new Mock().Object, + new Mock>().Object); + + private static CommandTemplate CreateCustomTemplate(string customCommandId) + => new() + { + CustomCommandInvocation = new CustomCommandInvocation { CustomCommandId = customCommandId ?? string.Empty } + }; +} diff --git a/Ares.Core.Tests/Execution/CommandExecutorNameTests.cs b/Ares.Core.Tests/Execution/CommandExecutorNameTests.cs new file mode 100644 index 00000000..b5930294 --- /dev/null +++ b/Ares.Core.Tests/Execution/CommandExecutorNameTests.cs @@ -0,0 +1,34 @@ +using Ares.Core.Execution.ControlTokens; +using Ares.Core.Execution.Executors; +using Ares.Core.Notifications; +using Ares.Core.Settings; +using Ares.Datamodel; +using Ares.Datamodel.Templates; +using Moq; + +namespace Ares.Core.Tests.Execution; + +internal class CommandExecutorNameTests +{ + [Test] + public async Task SuppliedName_IsUsedForInitialStatusAndCompletedSummary() + { + var executor = new CommandExecutor( + _ => Task.FromResult(new CommandResult { Success = true }), + new CommandTemplate + { + UniqueId = Guid.NewGuid().ToString(), + CustomCommandInvocation = new CustomCommandInvocation { CustomCommandId = Guid.NewGuid().ToString() } + }, + "Measure Temperature", + new Mock().Object, + new Mock().Object); + + Assert.That(executor.Status.CommandName, Is.EqualTo("Measure Temperature")); + + using var tokenSource = new ExecutionControlTokenSource(); + var summary = await executor.Execute(tokenSource.Token); + + Assert.That(summary.CommandName, Is.EqualTo("Measure Temperature")); + } +} diff --git a/Ares.Core.Tests/Execution/CommandTemplatePersistenceTests.cs b/Ares.Core.Tests/Execution/CommandTemplatePersistenceTests.cs new file mode 100644 index 00000000..80794200 --- /dev/null +++ b/Ares.Core.Tests/Execution/CommandTemplatePersistenceTests.cs @@ -0,0 +1,46 @@ +using Ares.Datamodel.Templates; +using Microsoft.EntityFrameworkCore; + +namespace Ares.Core.Tests.Execution; + +internal class CommandTemplatePersistenceTests +{ + [Test] + public async Task CommandTemplate_LoadsDeviceCommandAndMetadata() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + await using var context = new CoreDatabaseContext(options); + var commandTemplate = new CommandTemplate + { + UniqueId = Guid.NewGuid().ToString(), + DeviceCommand = new DeviceCommand + { + Metadata = new CommandMetadata + { + UniqueId = Guid.NewGuid().ToString(), + Name = "Legacy Command", + DeviceId = "device-1", + DeviceType = "Pump", + OutputMetadata = new OutputMetadata { UniqueId = Guid.NewGuid().ToString() } + } + } + }; + var stepTemplate = new StepTemplate { UniqueId = Guid.NewGuid().ToString() }; + stepTemplate.CommandTemplates.Add(commandTemplate); + context.StepTemplates.Add(stepTemplate); + await context.SaveChangesAsync(); + context.ChangeTracker.Clear(); + + var loadedTemplate = await context.CommandTemplates.SingleAsync(); + + using(Assert.EnterMultipleScope()) + { + Assert.That(loadedTemplate.CommandTypeCase, Is.EqualTo(CommandTemplate.CommandTypeOneofCase.DeviceCommand)); + Assert.That(loadedTemplate.DeviceCommand, Is.Not.Null); + Assert.That(loadedTemplate.DeviceCommand.Metadata.Name, Is.EqualTo("Legacy Command")); + Assert.That(loadedTemplate.DeviceCommand.Metadata.DeviceId, Is.EqualTo("device-1")); + } + } +} diff --git a/Ares.Core.Tests/Execution/CommandVariableResolutionTests.cs b/Ares.Core.Tests/Execution/CommandVariableResolutionTests.cs index 7e883a4f..b72880c9 100644 --- a/Ares.Core.Tests/Execution/CommandVariableResolutionTests.cs +++ b/Ares.Core.Tests/Execution/CommandVariableResolutionTests.cs @@ -39,7 +39,7 @@ public async Task SequentialStepExecutor_ResolvesVariableParameterFromEarlierCom stepTemplate.CommandTemplates.Add(CreateSourceCommand()); stepTemplate.CommandTemplates.Add(CreateConsumerCommand("sourceResult")); - var stepComposer = new StepComposer(deviceRepo, new Mock().Object, _systemSettingsManager); + var stepComposer = new StepComposer(deviceRepo, new Mock().Object, _systemSettingsManager, CreateNameResolver()); var stepExecutor = stepComposer.Compose(stepTemplate); using var tokenSource = new ExecutionControlTokenSource(); @@ -65,6 +65,7 @@ public async Task CommandExecutor_FailsVariableParameterWhenVariableIsMissing() return Task.FromResult(new CommandResult { Success = true }); }, template, + "consumer", new Mock().Object, _systemSettingsManager); @@ -79,19 +80,29 @@ public async Task CommandExecutor_FailsVariableParameterWhenVariableIsMissing() } } + private static ICommandDisplayNameResolver CreateNameResolver() + { + var resolver = new Mock(); + resolver.Setup(value => value.Resolve(It.IsAny())).Returns("Test command"); + return resolver.Object; + } + private static CommandTemplate CreateSourceCommand() => new() { UniqueId = Guid.NewGuid().ToString(), Index = 0, OutputVarName = "sourceResult", - Metadata = new CommandMetadata + DeviceCommand = new DeviceCommand { - DeviceId = "device-id", - Name = "source", - OutputMetadata = new OutputMetadata + Metadata = new CommandMetadata { - DataSchema = new AresValueSchema { Type = AresDataType.Number } + DeviceId = "device-id", + Name = "source", + OutputMetadata = new OutputMetadata + { + DataSchema = new AresValueSchema { Type = AresDataType.Number } + } } } }; @@ -102,14 +113,17 @@ private static CommandTemplate CreateConsumerCommand(string variableArgument) { UniqueId = Guid.NewGuid().ToString(), Index = 1, - Metadata = new CommandMetadata + DeviceCommand = new DeviceCommand { - DeviceId = "device-id", - Name = "consumer" + Metadata = new CommandMetadata + { + DeviceId = "device-id", + Name = "consumer" + } } }; - template.Parameters.Add(new Parameter + template.ArgumentBindings.Add(new Parameter { UniqueId = Guid.NewGuid().ToString(), CommandVariableSource = new CommandVariableParameterSource { VariableArgument = variableArgument }, diff --git a/Ares.Core.Tests/Execution/Composers/StepComposerTests.cs b/Ares.Core.Tests/Execution/Composers/StepComposerTests.cs index 85df98bc..c7275539 100644 --- a/Ares.Core.Tests/Execution/Composers/StepComposerTests.cs +++ b/Ares.Core.Tests/Execution/Composers/StepComposerTests.cs @@ -7,6 +7,8 @@ using Moq; using System.Reflection; using Ares.Core.Settings; +using Ares.Core.CustomCommands; +using Ares.Core.Scripting; namespace Ares.Core.Tests.Execution.Composers; @@ -16,6 +18,7 @@ internal class StepComposerTests private AresDeviceRepo _deviceRepo; private INotifier _notifer; private ISystemSettingsManager _settingsManager; + private ICommandDisplayNameResolver _commandDisplayNameResolver; [SetUp] public void SetUp() @@ -26,28 +29,28 @@ public void SetUp() { Index = 0, UniqueId = Guid.NewGuid().ToString(), - Metadata = new CommandMetadata { UniqueId = Guid.NewGuid().ToString(), DeviceId = "TestDeviceId" } + DeviceCommand = new DeviceCommand { Metadata = new CommandMetadata { UniqueId = Guid.NewGuid().ToString(), DeviceId = "TestDeviceId" } } }; var commandTemplate2 = new CommandTemplate { Index = 1, UniqueId = Guid.NewGuid().ToString(), - Metadata = new CommandMetadata { UniqueId = Guid.NewGuid().ToString(), DeviceId = "TestDeviceId" } + DeviceCommand = new DeviceCommand { Metadata = new CommandMetadata { UniqueId = Guid.NewGuid().ToString(), DeviceId = "TestDeviceId" } } }; var commandTemplate3 = new CommandTemplate { Index = 2, UniqueId = Guid.NewGuid().ToString(), - Metadata = new CommandMetadata { UniqueId = Guid.NewGuid().ToString(), DeviceId = "TestDeviceId" } + DeviceCommand = new DeviceCommand { Metadata = new CommandMetadata { UniqueId = Guid.NewGuid().ToString(), DeviceId = "TestDeviceId" } } }; var commandTemplate4 = new CommandTemplate { Index = 3, UniqueId = Guid.NewGuid().ToString(), - Metadata = new CommandMetadata { UniqueId = Guid.NewGuid().ToString(), DeviceId = "TestDeviceId" } + DeviceCommand = new DeviceCommand { Metadata = new CommandMetadata { UniqueId = Guid.NewGuid().ToString(), DeviceId = "TestDeviceId" } } }; var stepTemplate = new StepTemplate @@ -66,6 +69,9 @@ public void OneTimeSetUp() { _notifer = new Mock().Object; _settingsManager = new Mock().Object; + var resolver = new Mock(); + resolver.Setup(value => value.Resolve(It.IsAny())).Returns("Test command"); + _commandDisplayNameResolver = resolver.Object; } [TearDown] @@ -77,7 +83,7 @@ public void Dispose() [Test] public void StepComposer_Composes_CommandTemplates_Correctly() { - var stepComposer = new StepComposer(_deviceRepo, _notifer, _settingsManager); + var stepComposer = new StepComposer(_deviceRepo, _notifer, _settingsManager, _commandDisplayNameResolver); var stepExecutor = stepComposer.Compose(_stepTemplate); var templates = stepExecutor.CommandExecutors.Select(executor => typeof(CommandExecutor).GetProperty("Template", BindingFlags.Public | BindingFlags.Instance).GetValue(executor)).OfType(); Assert.That(templates.Select((template, i) => template.Index == i), Is.All.True); @@ -87,7 +93,7 @@ public void StepComposer_Composes_CommandTemplates_Correctly() public void StepComposer_Composes_Parallel_Template() { _stepTemplate.IsParallel = true; - var stepComposer = new StepComposer(_deviceRepo, _notifer, _settingsManager); + var stepComposer = new StepComposer(_deviceRepo, _notifer, _settingsManager, _commandDisplayNameResolver); var stepExecutor = stepComposer.Compose(_stepTemplate); Assert.That(stepExecutor, Is.TypeOf()); } @@ -96,8 +102,29 @@ public void StepComposer_Composes_Parallel_Template() public void StepComposer_Composes_Sequential_Template() { _stepTemplate.IsParallel = false; - var stepComposer = new StepComposer(_deviceRepo, _notifer, _settingsManager); + var stepComposer = new StepComposer(_deviceRepo, _notifer, _settingsManager, _commandDisplayNameResolver); var stepExecutor = stepComposer.Compose(_stepTemplate); Assert.That(stepExecutor, Is.TypeOf()); } + + [Test] + public void StepComposer_UsesResolvedCustomCommandNameBeforeExecution() + { + var template = new StepTemplate(); + template.CommandTemplates.Add(new CommandTemplate + { + UniqueId = Guid.NewGuid().ToString(), + CustomCommandInvocation = new CustomCommandInvocation { CustomCommandId = Guid.NewGuid().ToString() } + }); + var resolver = new Mock(); + resolver.Setup(value => value.Resolve(It.IsAny())).Returns("Measure Temperature"); + var customCommandExecutor = new CustomCommandExecutor( + new Mock().Object, + new BaseEnvironmentBuilder([])); + var stepComposer = new StepComposer(_deviceRepo, _notifer, _settingsManager, resolver.Object, customCommandExecutor); + + var stepExecutor = stepComposer.Compose(template); + + Assert.That(stepExecutor.CommandExecutors.Single().Status.CommandName, Is.EqualTo("Measure Temperature")); + } } diff --git a/Ares.Core.Tests/Execution/CustomCommandExecutorTests.cs b/Ares.Core.Tests/Execution/CustomCommandExecutorTests.cs new file mode 100644 index 00000000..e6c6ecfb --- /dev/null +++ b/Ares.Core.Tests/Execution/CustomCommandExecutorTests.cs @@ -0,0 +1,53 @@ +using Ares.Core.CustomCommands; +using Ares.Core.Execution.Executors; +using Ares.Core.Scripting; +using Ares.Datamodel; +using Ares.Datamodel.Automation; +using Ares.Datamodel.Extensions; +using Ares.Datamodel.Factories; +using Ares.Datamodel.Templates; +using Moq; + +namespace Ares.Core.Tests.Execution; + +[TestFixture] +internal class CustomCommandExecutorTests +{ + [Test] + public async Task Execute_ReturnsWrappedCustomCommandResult() + { + var commandId = Guid.NewGuid(); + var command = new CustomCommandVersion + { + Name = "Increment", + OutputSchema = AresSchemaBuilder.Entry(AresDataType.Number).Build(), + ScriptBody = "return value + 1" + }; + command.InputParameters.Add(new CustomCommandParameter + { + Name = "value", + Schema = AresSchemaBuilder.Entry(AresDataType.Number).Build() + }); + + var persistenceService = new Mock(); + persistenceService + .Setup(service => service.GetAsync(commandId)) + .ReturnsAsync(command); + var executor = new CustomCommandExecutor( + persistenceService.Object, + new BaseEnvironmentBuilder([])); + var binding = new Parameter + { + Metadata = new ParameterMetadata { Name = "value" }, + LiteralSource = new LiteralParameterSource { Value = AresValueHelper.CreateNumber(41) } + }; + + var result = await executor.Execute(commandId.ToString(), [binding], CancellationToken.None); + + using(Assert.EnterMultipleScope()) + { + Assert.That(result.Success, Is.True, result.Error); + Assert.That(result.Result?.NumberValue, Is.EqualTo(42)); + } + } +} diff --git a/Ares.Core.Tests/Execution/ExecutionManagerTests.cs b/Ares.Core.Tests/Execution/ExecutionManagerTests.cs index dedef3eb..b67f6840 100644 --- a/Ares.Core.Tests/Execution/ExecutionManagerTests.cs +++ b/Ares.Core.Tests/Execution/ExecutionManagerTests.cs @@ -23,6 +23,7 @@ internal class ExecutionManagerTests private IExecutionSafetyManager _safetyManager; private INotifier _notifier; private ILogger _logger; + private ICommandDisplayNameResolver _commandDisplayNameResolver; [OneTimeSetUp] public void OneTimeSetUp() @@ -49,6 +50,7 @@ public void OneTimeSetUp() _safetyManager = new Mock().Object; _notifier = new Mock().Object; _logger = new Mock>().Object; + _commandDisplayNameResolver = new Mock().Object; } [Test] @@ -61,7 +63,7 @@ public void ExecutionManager_Should_Execute_Without_Throwing_Exception() }; var mockTemplateStore = new Mock(); mockTemplateStore.Setup(store => store.CampaignTemplate).Returns(campaignTemplate); - var executionManager = new ExecutionManager([], _contextFactory, mockTemplateStore.Object, _safetyManager, _campaignComposer, _logger, _notifier); + var executionManager = new ExecutionManager([], _contextFactory, mockTemplateStore.Object, _safetyManager, _campaignComposer, _commandDisplayNameResolver, _logger, _notifier); executionManager.CampaignStopConditions.Add(new NumExperimentsRun(_executionReportStore, 1)); Assert.DoesNotThrowAsync(() => executionManager.Start(string.Empty, [])); } @@ -71,7 +73,7 @@ public void ExecutionManager_Should_Throw_When_CampaignTemplate_Is_Null() { var mockTemplateStore = new Mock(); mockTemplateStore.Setup(store => store.CampaignTemplate).Returns((CampaignTemplate)null); - var executionManager = new ExecutionManager([], _contextFactory, mockTemplateStore.Object, _safetyManager, _campaignComposer, _logger, _notifier); + var executionManager = new ExecutionManager([], _contextFactory, mockTemplateStore.Object, _safetyManager, _campaignComposer, _commandDisplayNameResolver, _logger, _notifier); Assert.ThrowsAsync(() => executionManager.Start(string.Empty, [])); } @@ -82,7 +84,7 @@ public void ExecutionManager_Should_Throw_When_Start_Condition_Fails() mockTemplateStore.Setup(store => store.CampaignTemplate).Returns(new CampaignTemplate()); var falseCondition = new Mock(); falseCondition.Setup(condition => condition.CanStart()).Returns(Task.FromResult(new StartConditionResult(false))); - var executionManager = new ExecutionManager([falseCondition.Object], _contextFactory, mockTemplateStore.Object, _safetyManager, _campaignComposer, _logger, _notifier); + var executionManager = new ExecutionManager([falseCondition.Object], _contextFactory, mockTemplateStore.Object, _safetyManager, _campaignComposer, _commandDisplayNameResolver, _logger, _notifier); Assert.ThrowsAsync(() => executionManager.Start(string.Empty, [])); } } diff --git a/Ares.Core.Tests/Execution/SystemOperationCatalogTests.cs b/Ares.Core.Tests/Execution/SystemOperationCatalogTests.cs new file mode 100644 index 00000000..2e8e4214 --- /dev/null +++ b/Ares.Core.Tests/Execution/SystemOperationCatalogTests.cs @@ -0,0 +1,76 @@ +using Ares.Core.Execution.Executors; +using Ares.Datamodel; +using Ares.Datamodel.Templates; + +namespace Ares.Core.Tests.Execution; + +public class SystemOperationCatalogTests +{ + [Test] + public void Definitions_ContainEverySupportedOperation() + { + var expectedOperations = Enum.GetValues() + .Where(operation => operation != SystemOperation.Undefined) + .ToArray(); + + Assert.That(SystemOperationCatalog.Definitions.Select(definition => definition.Operation), Is.EquivalentTo(expectedOperations)); + } + + [TestCase(SystemOperation.SleepForMilliseconds)] + [TestCase(SystemOperation.SleepForSeconds)] + [TestCase(SystemOperation.SleepForMinutes)] + public void SleepDefinitions_HaveDurationInputAndNoOutput(SystemOperation operation) + { + var definition = SystemOperationCatalog.Find(operation); + + Assert.Multiple(() => + { + Assert.That(definition, Is.Not.Null); + Assert.That(definition!.Parameters, Has.Count.EqualTo(1)); + Assert.That(definition.Parameters[0].Name, Is.EqualTo("Duration")); + Assert.That(definition.Parameters[0].Schema.Type, Is.EqualTo(AresDataType.Number)); + Assert.That(definition.OutputSchema, Is.Null); + }); + } + + [TestCase(SystemOperation.WaitForUser)] + [TestCase(SystemOperation.WaitForUserInput)] + public void WaitDefinitions_HaveNoInputsOrOutput(SystemOperation operation) + { + var definition = SystemOperationCatalog.Find(operation); + + Assert.Multiple(() => + { + Assert.That(definition, Is.Not.Null); + Assert.That(definition!.Parameters, Is.Empty); + Assert.That(definition.OutputSchema, Is.Null); + }); + } + + [Test] + public void GetTimestampDefinition_HasTimestampOutput() + { + var definition = SystemOperationCatalog.Find(SystemOperation.GetTimestamp); + + Assert.Multiple(() => + { + Assert.That(definition, Is.Not.Null); + Assert.That(definition!.Parameters, Is.Empty); + Assert.That(definition.OutputSchema?.Type, Is.EqualTo(AresDataType.Timestamp)); + }); + } + + [Test] + public void CalculateAverageDefinition_HasNumberArrayInputAndNumberOutput() + { + var definition = SystemOperationCatalog.Find(SystemOperation.CalculateAverage); + + Assert.Multiple(() => + { + Assert.That(definition, Is.Not.Null); + Assert.That(definition!.Parameters, Has.Count.EqualTo(1)); + Assert.That(definition.Parameters[0].Schema.Type, Is.EqualTo(AresDataType.NumberArray)); + Assert.That(definition.OutputSchema?.Type, Is.EqualTo(AresDataType.Number)); + }); + } +} diff --git a/Ares.Core.Tests/Validation/GoodAnalyzerValidatorTests.cs b/Ares.Core.Tests/Validation/GoodAnalyzerValidatorTests.cs index 6a49ba36..3aedcb28 100644 --- a/Ares.Core.Tests/Validation/GoodAnalyzerValidatorTests.cs +++ b/Ares.Core.Tests/Validation/GoodAnalyzerValidatorTests.cs @@ -1,8 +1,12 @@ using Ares.Core.Analyzing; +using Ares.Core.CustomCommands; +using Ares.Core.Execution.Executors; +using Ares.Core.Validation.Campaign; using Ares.Core.Validation.Validators; using Ares.Datamodel; using Ares.Datamodel.Analyzing; using Ares.Datamodel.Analyzing.Remote; +using Ares.Datamodel.Automation; using Ares.Datamodel.Connection; using Ares.Datamodel.Templates; using Moq; @@ -51,6 +55,186 @@ public async Task Validate_FailsWhenNestedStructPathDoesNotMatchAnalyzerRequired Assert.That(result.Success, Is.False); Assert.That(result.Messages, Does.Contain("Missing AnalyzerInput")); + Assert.That(result.Messages, Has.Some.Contains("output member is unavailable")); + } + + [Test] + public async Task Validate_MapsSystemCommandOutputSchema() + { + var command = new CommandTemplate + { + OutputVarName = "timestamp", + SystemCommand = new SystemCommand { Operation = SystemOperation.GetTimestamp } + }; + var experimentTemplate = CreateExperimentTemplate(command, "timestamp"); + var analyzer = CreateAnalyzer( + inputSchema => inputSchema.Fields.TryGetValue("AnalyzerInput", out var input) + && input.Type == AresDataType.Timestamp, + AresDataType.Timestamp); + + var result = await GoodAnalyzerValidator.Validate( + experimentTemplate, + null, + CreateAnalyzerRepo(analyzer.Object).Object); + + Assert.That(result.Success, Is.True); + } + + [Test] + public async Task Validate_MapsCustomCommandScalarOutputByStableIdCaseInsensitively() + { + const string savedId = "A2A403BA-1284-4E1F-8C67-082AF05E879B"; + var experimentTemplate = CreateExperimentTemplate(CreateCustomCommand(savedId), "result"); + var analyzer = CreateAnalyzer(inputSchema => + inputSchema.Fields.TryGetValue("AnalyzerInput", out var input) + && input.Type == AresDataType.Number); + var schemas = new Dictionary + { + [savedId.ToLowerInvariant()] = new() { Type = AresDataType.Number } + }; + + var result = await GoodAnalyzerValidator.Validate( + experimentTemplate, + null, + CreateAnalyzerRepo(analyzer.Object).Object, + schemas); + + Assert.That(result.Success, Is.True); + } + + [Test] + public async Task Validate_MapsCustomCommandNestedStructOutput() + { + const string commandId = "custom-command-id"; + var experimentTemplate = CreateExperimentTemplate(CreateCustomCommand(commandId), "result.outer.inner"); + var analyzer = CreateAnalyzer(inputSchema => + inputSchema.Fields.TryGetValue("AnalyzerInput", out var input) + && input.Type == AresDataType.Number); + + var result = await GoodAnalyzerValidator.Validate( + experimentTemplate, + null, + CreateAnalyzerRepo(analyzer.Object).Object, + new Dictionary { [commandId] = CreateNestedOutputSchema() }); + + Assert.That(result.Success, Is.True); + } + + [Test] + public async Task Validate_FailsClearlyWhenCustomCommandIsMissing() + { + var experimentTemplate = CreateExperimentTemplate(CreateCustomCommand("deleted-command"), "result"); + var analyzer = CreateAnalyzer(_ => false); + + var result = await GoodAnalyzerValidator.Validate( + experimentTemplate, + null, + CreateAnalyzerRepo(analyzer.Object).Object, + new Dictionary()); + + using(Assert.EnterMultipleScope()) + { + Assert.That(result.Success, Is.False); + Assert.That(result.Messages, Has.Some.Contains("custom command 'deleted-command'")); + } + } + + [TestCase(AresDataType.Unit)] + [TestCase(AresDataType.UnspecifiedType)] + public async Task Validate_FailsClearlyWhenOutputSchemaIsUnusable(AresDataType schemaType) + { + var experimentTemplate = CreateExperimentTemplate( + CreateDeviceCommand(new AresValueSchema { Type = schemaType }), + "result"); + var result = await GoodAnalyzerValidator.Validate( + experimentTemplate, + null, + CreateAnalyzerRepo(CreateAnalyzer(_ => false).Object).Object); + + using(Assert.EnterMultipleScope()) + { + Assert.That(result.Success, Is.False); + Assert.That(result.Messages, Has.Some.Contains("does not have a usable output schema")); + } + } + + [Test] + public async Task Validate_FailsClearlyWhenOutputSchemaIsMissing() + { + var command = new CommandTemplate + { + OutputVarName = "result", + DeviceCommand = new DeviceCommand + { + Metadata = new CommandMetadata { OutputMetadata = new OutputMetadata() } + } + }; + var experimentTemplate = CreateExperimentTemplate(command, "result"); + + var result = await GoodAnalyzerValidator.Validate( + experimentTemplate, + null, + CreateAnalyzerRepo(CreateAnalyzer(_ => false).Object).Object); + + using(Assert.EnterMultipleScope()) + { + Assert.That(result.Success, Is.False); + Assert.That(result.Messages, Has.Some.Contains("does not have a usable output schema")); + } + } + + [Test] + public async Task CampaignValidator_LoadsCurrentCustomCommandsOnce() + { + const string commandId = "current-custom-command"; + var experimentTemplate = CreateExperimentTemplate(CreateCustomCommand(commandId), "result"); + var analyzer = CreateAnalyzer(inputSchema => inputSchema.Fields["AnalyzerInput"].Type == AresDataType.Number); + var persistence = new Mock(); + persistence.Setup(service => service.GetCommandsAsync()).ReturnsAsync([ + new CustomCommandVersion + { + CustomCommandId = commandId, + OutputSchema = new AresValueSchema { Type = AresDataType.Number } + } + ]); + var validator = new GoodAnalyzerCampaignValidator(CreateAnalyzerRepo(analyzer.Object).Object, persistence.Object); + + var result = await validator.Validate(new CampaignTemplate { ExperimentTemplate = experimentTemplate }); + + Assert.That(result.Success, Is.True); + persistence.Verify(service => service.GetCommandsAsync(), Times.Once); + } + + [Test] + public async Task CampaignValidator_ReturnsFailureWhenCustomCommandsCannotBeLoaded() + { + var experimentTemplate = CreateExperimentTemplate(CreateCustomCommand("custom-command"), "result"); + var persistence = new Mock(); + persistence.Setup(service => service.GetCommandsAsync()).ThrowsAsync(new InvalidOperationException("Database unavailable")); + var validator = new GoodAnalyzerCampaignValidator(new Mock().Object, persistence.Object); + + var result = await validator.Validate(new CampaignTemplate { ExperimentTemplate = experimentTemplate }); + + using(Assert.EnterMultipleScope()) + { + Assert.That(result.Success, Is.False); + Assert.That(result.Messages, Has.Some.Contains("Unable to load current custom-command definitions")); + Assert.That(result.Messages, Has.Some.Contains("Database unavailable")); + } + } + + [Test] + public async Task CampaignValidator_DoesNotLoadCustomCommandsWhenNoAnalyzerIsAssigned() + { + var experimentTemplate = CreateExperimentTemplate(CreateCustomCommand("custom-command"), "result"); + experimentTemplate.ClearAnalyzerId(); + var persistence = new Mock(); + var validator = new GoodAnalyzerCampaignValidator(new Mock().Object, persistence.Object); + + var result = await validator.Validate(new CampaignTemplate { ExperimentTemplate = experimentTemplate }); + + Assert.That(result.Success, Is.True); + persistence.Verify(service => service.GetCommandsAsync(), Times.Never); } private static Mock CreateAnalyzerRepo(IAnalyzer analyzer) @@ -60,12 +244,14 @@ private static Mock CreateAnalyzerRepo(IAnalyzer analyzer) return analyzerRepo; } - private static Mock CreateAnalyzer(Func validateInputSchema) + private static Mock CreateAnalyzer( + Func validateInputSchema, + AresDataType requiredType = AresDataType.Number) { var parameters = new AresStructSchema(); parameters.Fields["AnalyzerInput"] = new AresValueSchema { - Type = AresDataType.Number, + Type = requiredType, Optional = false }; @@ -85,6 +271,9 @@ private static Mock CreateAnalyzer(Func valid } private static ExperimentTemplate CreateExperimentTemplate(AresValueSchema outputSchema, string analyzerMapValue) + => CreateExperimentTemplate(CreateDeviceCommand(outputSchema), analyzerMapValue); + + private static ExperimentTemplate CreateExperimentTemplate(CommandTemplate command, string analyzerMapValue) { var experimentTemplate = new ExperimentTemplate { @@ -93,21 +282,31 @@ private static ExperimentTemplate CreateExperimentTemplate(AresValueSchema outpu experimentTemplate.AnalyzerMaps["AnalyzerInput"] = analyzerMapValue; var stepTemplate = new StepTemplate(); - stepTemplate.CommandTemplates.Add(new CommandTemplate + stepTemplate.CommandTemplates.Add(command); + + experimentTemplate.StepTemplates.Add(stepTemplate); + return experimentTemplate; + } + + private static CommandTemplate CreateDeviceCommand(AresValueSchema outputSchema) + => new() { OutputVarName = "result", - Metadata = new CommandMetadata + DeviceCommand = new DeviceCommand { - OutputMetadata = new OutputMetadata + Metadata = new CommandMetadata { - DataSchema = outputSchema + OutputMetadata = new OutputMetadata { DataSchema = outputSchema } } } - }); + }; - experimentTemplate.StepTemplates.Add(stepTemplate); - return experimentTemplate; - } + private static CommandTemplate CreateCustomCommand(string commandId) + => new() + { + OutputVarName = "result", + CustomCommandInvocation = new CustomCommandInvocation { CustomCommandId = commandId } + }; private static AresValueSchema CreateNestedOutputSchema() { diff --git a/Ares.Core/Ares.Core.csproj b/Ares.Core/Ares.Core.csproj index 41029cb5..1a1012c7 100644 --- a/Ares.Core/Ares.Core.csproj +++ b/Ares.Core/Ares.Core.csproj @@ -26,7 +26,7 @@ - + diff --git a/Ares.Core/Campaigns/CampaignTemplateImportException.cs b/Ares.Core/Campaigns/CampaignTemplateImportException.cs new file mode 100644 index 00000000..836b54f4 --- /dev/null +++ b/Ares.Core/Campaigns/CampaignTemplateImportException.cs @@ -0,0 +1,4 @@ +namespace Ares.Core.Campaigns; + +public class CampaignTemplateImportException(string message, Exception? innerException = null) + : Exception(message, innerException); diff --git a/Ares.Core/Campaigns/CampaignTemplateLegacyJsonConverter.cs b/Ares.Core/Campaigns/CampaignTemplateLegacyJsonConverter.cs new file mode 100644 index 00000000..2f315112 --- /dev/null +++ b/Ares.Core/Campaigns/CampaignTemplateLegacyJsonConverter.cs @@ -0,0 +1,57 @@ +using Ares.Core.Execution.Executors; +using Ares.Datamodel.Templates; +using System.Text.Json.Nodes; + +namespace Ares.Core.Campaigns; + +internal static class CampaignTemplateLegacyJsonConverter +{ + private const string LegacyAresDeviceId = "ARES-CORE-DEVICE"; + + // Before custom commands, command templates only contained device-command metadata directly. + // Legacy imports need that metadata wrapped in the current typed device-command shape. + public static void Convert(JsonNode node) + { + if(node is JsonObject jsonObject) + { + if(jsonObject["commandTemplates"] is JsonArray commands) + foreach(var command in commands.OfType()) + { + var hasCurrentType = command.ContainsKey("deviceCommand") + || command.ContainsKey("systemCommand") + || command.ContainsKey("customCommandInvocation"); + if(!hasCurrentType && command.Remove("metadata", out var metadata)) + { + if(TryGetSystemOperation(metadata, out var operation)) + command["systemCommand"] = new JsonObject { ["operation"] = (int)operation }; + else + command["deviceCommand"] = new JsonObject { ["metadata"] = metadata }; + } + + if(!command.ContainsKey("argumentBindings") && command.Remove("parameters", out var parameters)) + command["argumentBindings"] = parameters; + } + + foreach(var child in jsonObject.Select(property => property.Value).ToArray()) + if(child is not null) + Convert(child); + } + else if(node is JsonArray jsonArray) + foreach(var child in jsonArray) + if(child is not null) + Convert(child); + } + + private static bool TryGetSystemOperation(JsonNode? metadataNode, out SystemOperation operation) + { + operation = SystemOperation.Undefined; + if(metadataNode is not JsonObject metadata + || metadata["deviceId"]?.GetValue() != LegacyAresDeviceId) + return false; + + var commandName = metadata["name"]?.GetValue(); + return Enum.TryParse(commandName, ignoreCase: false, out operation) + && operation != SystemOperation.Undefined + && SystemOperationCatalog.Find(operation) is not null; + } +} diff --git a/Ares.Core/Campaigns/CampaignTemplatePersistenceService.cs b/Ares.Core/Campaigns/CampaignTemplatePersistenceService.cs new file mode 100644 index 00000000..2d268df2 --- /dev/null +++ b/Ares.Core/Campaigns/CampaignTemplatePersistenceService.cs @@ -0,0 +1,131 @@ +using Ares.Datamodel.Templates; +using Ares.Services; +using Microsoft.EntityFrameworkCore; + +namespace Ares.Core.Campaigns; + +internal class CampaignTemplatePersistenceService(IDbContextFactory contextFactory) + : ICampaignTemplatePersistenceService +{ + public async Task> GetSummariesAsync(CancellationToken cancellationToken = default) + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + return await context.CampaignTemplates + .AsNoTracking() + .IgnoreAutoIncludes() + .OrderBy(template => template.Name) + .Select(template => new CampaignTemplateSummary + { + UniqueId = template.UniqueId, + CampaignName = template.Name + }) + .ToArrayAsync(cancellationToken); + } + + public async Task GetByIdAsync(string uniqueId, CancellationToken cancellationToken = default) + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + return await QueryCampaigns(context, asNoTracking: true) + .FirstOrDefaultAsync(template => template.UniqueId == uniqueId, cancellationToken); + } + + public async Task GetByNameAsync(string name, CancellationToken cancellationToken = default) + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + return await QueryCampaigns(context, asNoTracking: true) + .FirstOrDefaultAsync(template => template.Name == name, cancellationToken); + } + + public async Task ExistsByIdAsync(string uniqueId, CancellationToken cancellationToken = default) + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + return await context.CampaignTemplates + .IgnoreAutoIncludes() + .AnyAsync(template => template.UniqueId == uniqueId, cancellationToken); + } + + public async Task ExistsByNameAsync(string name, CancellationToken cancellationToken = default) + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + return await context.CampaignTemplates + .IgnoreAutoIncludes() + .AnyAsync(template => template.Name == name, cancellationToken); + } + + public async Task AddAsync(CampaignTemplate template, CancellationToken cancellationToken = default) + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + await CanonicalizePlannerReferencesAsync(context, template, cancellationToken); + context.CampaignTemplates.Add(template); + await context.SaveChangesAsync(cancellationToken); + } + + public async Task ReplaceAsync(CampaignTemplate template, CancellationToken cancellationToken = default) + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + await using var transaction = await context.Database.BeginTransactionAsync(cancellationToken); + var existingTemplate = await QueryCampaigns(context, asNoTracking: false) + .FirstOrDefaultAsync(existing => existing.UniqueId == template.UniqueId, cancellationToken); + if(existingTemplate is null) + return false; + + RemoveCampaignGraph(context, existingTemplate); + await context.SaveChangesAsync(cancellationToken); + await CanonicalizePlannerReferencesAsync(context, template, cancellationToken); + context.CampaignTemplates.Add(template); + await context.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + return true; + } + + public async Task DeleteAsync(string uniqueId, CancellationToken cancellationToken = default) + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var template = await QueryCampaigns(context, asNoTracking: false) + .FirstOrDefaultAsync(existing => existing.UniqueId == uniqueId, cancellationToken); + if(template is null) + return false; + + RemoveCampaignGraph(context, template); + await context.SaveChangesAsync(cancellationToken); + return true; + } + + private static IQueryable QueryCampaigns(CoreDatabaseContext context, bool asNoTracking) + { + var query = context.CampaignTemplates.AsSplitQuery(); + return asNoTracking ? query.AsNoTracking() : query; + } + + private static void RemoveCampaignGraph(CoreDatabaseContext context, CampaignTemplate template) + { + var experiments = new[] + { + template.StartupTemplate, + template.ExperimentTemplate, + template.CloseoutTemplate + } + .Where(experiment => experiment is not null) + .DistinctBy(experiment => experiment!.UniqueId) + .ToArray(); + context.ExperimentTemplates.RemoveRange(experiments!); + context.CampaignTemplates.Remove(template); + } + + private static async Task CanonicalizePlannerReferencesAsync( + CoreDatabaseContext context, + CampaignTemplate template, + CancellationToken cancellationToken) + { + foreach(var allocation in template.PlannerAllocations) + { + var plannerId = allocation.Planner?.UniqueId; + if(string.IsNullOrWhiteSpace(plannerId)) + continue; + + var localPlanner = await context.PlannerInfos.FirstOrDefaultAsync(planner => planner.UniqueId == plannerId, cancellationToken); + if(localPlanner is not null) + allocation.Planner = localPlanner; + } + } +} diff --git a/Ares.Core/Campaigns/CampaignTemplateTransferService.cs b/Ares.Core/Campaigns/CampaignTemplateTransferService.cs new file mode 100644 index 00000000..a3f82ac9 --- /dev/null +++ b/Ares.Core/Campaigns/CampaignTemplateTransferService.cs @@ -0,0 +1,218 @@ +using Ares.Core.CustomCommands; +using Ares.Core.Device.Repos; +using Ares.Core.EntityConfigurations.Helpers; +using Ares.Datamodel.Planning; +using Ares.Datamodel.Templates; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Ares.Core.Campaigns; + +internal class CampaignTemplateTransferService( + ICampaignTemplatePersistenceService campaignPersistenceService, + ICustomCommandPersistenceService customCommandPersistenceService, + IAresDeviceRepo deviceRepo, + IDbContextFactory contextFactory) : ICampaignTemplateTransferService +{ + private readonly JsonSerializerOptions _serializerOptions = SerializerSettingsHelper.CreateCustomSerializationSettings(); + + public async Task ExportAsync(string campaignId, CancellationToken cancellationToken = default) + { + var template = await campaignPersistenceService.GetByIdAsync(campaignId, cancellationToken); + if(template is null) + return null; + + var json = JsonSerializer.Serialize(template, _serializerOptions); + return new CampaignTemplateExport(template, json, MakeFileName(template.Name)); + } + + public async Task ImportAsync(string json, CancellationToken cancellationToken = default) + { + CampaignTemplate template; + try + { + var root = JsonNode.Parse(json) ?? throw new CampaignTemplateImportException("The selected file does not contain a JSON document."); + CampaignTemplateLegacyJsonConverter.Convert(root); + template = root.Deserialize(_serializerOptions) + ?? throw new CampaignTemplateImportException("The selected file does not contain a campaign template."); + } + catch(CampaignTemplateImportException) + { + throw; + } + catch(Exception exception) when(exception is JsonException or NotSupportedException) + { + throw new CampaignTemplateImportException("The selected file is not a valid campaign template JSON file.", exception); + } + + Validate(template); + var warnings = new List(); + await PreparePlannerAllocationsAsync(template, warnings, cancellationToken); + await AddReferenceWarningsAsync(template, warnings); + await AssignImportIdentityAsync(template, cancellationToken); + + try + { + await campaignPersistenceService.AddAsync(template, cancellationToken); + } + catch(Exception exception) + { + throw new CampaignTemplateImportException("The campaign was valid but could not be saved to the database.", exception); + } + + return new CampaignTemplateImportResult(template, warnings); + } + + private static void Validate(CampaignTemplate template) + { + if(string.IsNullOrWhiteSpace(template.Name)) + throw new CampaignTemplateImportException("The campaign template does not have a name."); + if(template.ExperimentTemplate is null) + throw new CampaignTemplateImportException("The campaign template does not contain a main experiment."); + + var invalidCommand = GetExperiments(template) + .SelectMany(experiment => experiment.StepTemplates) + .SelectMany(step => step.CommandTemplates) + .FirstOrDefault(command => command.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.None); + if(invalidCommand is not null) + throw new CampaignTemplateImportException($"Command '{invalidCommand.UniqueId}' does not contain a supported command type."); + } + + private async Task PreparePlannerAllocationsAsync(CampaignTemplate template, List warnings, CancellationToken cancellationToken) + { + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var localPlanners = await context.PlannerInfos.AsNoTracking().ToArrayAsync(cancellationToken); + var parametersById = template.PlannableParameters + .Where(parameter => !string.IsNullOrWhiteSpace(parameter.UniqueId)) + .ToDictionary(parameter => parameter.UniqueId, StringComparer.OrdinalIgnoreCase); + + foreach(var allocation in template.PlannerAllocations.ToArray()) + { + var importedPlanner = allocation.Planner; + var localPlanner = localPlanners.FirstOrDefault(planner => planner.UniqueId == importedPlanner?.UniqueId); + if(localPlanner is null && importedPlanner is not null) + { + var matchingPlanners = localPlanners.Where(planner => planner.Name == importedPlanner.Name + && planner.Type == importedPlanner.Type + && planner.Version == importedPlanner.Version).ToArray(); + if(matchingPlanners.Length == 1) + localPlanner = matchingPlanners[0]; + } + ParameterMetadata? parameter = null; + var hasParameter = allocation.Parameter is not null + && parametersById.TryGetValue(allocation.Parameter.UniqueId, out parameter); + if(localPlanner is null || !hasParameter) + { + template.PlannerAllocations.Remove(allocation); + warnings.Add(localPlanner is null + ? $"Planner allocation for '{importedPlanner?.Name ?? "unknown planner"}' was removed because that planner is unavailable." + : "A planner allocation was removed because its parameter is unavailable."); + continue; + } + + allocation.Planner = localPlanner; + allocation.Parameter = parameter!; + } + } + + private async Task AddReferenceWarningsAsync(CampaignTemplate template, List warnings) + { + var deviceIds = deviceRepo.GetAll().Select(device => device.UniqueId).ToHashSet(StringComparer.OrdinalIgnoreCase); + var customCommandIds = (await customCommandPersistenceService.GetCommandsAsync()) + .Select(command => command.CustomCommandId) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + foreach(var command in GetExperiments(template).SelectMany(experiment => experiment.StepTemplates).SelectMany(step => step.CommandTemplates)) + switch(command.CommandTypeCase) + { + case CommandTemplate.CommandTypeOneofCase.DeviceCommand: + var deviceId = command.DeviceCommand?.Metadata?.DeviceId; + if(!string.IsNullOrWhiteSpace(deviceId) && !deviceIds.Contains(deviceId)) + warnings.Add($"Device '{deviceId}' is unavailable; its command was preserved for repair."); + break; + case CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation: + var customCommandId = command.CustomCommandInvocation.CustomCommandId; + if(!string.IsNullOrWhiteSpace(customCommandId) && !customCommandIds.Contains(customCommandId)) + warnings.Add($"Custom command '{customCommandId}' is unavailable; its invocation was preserved for repair."); + break; + case CommandTemplate.CommandTypeOneofCase.SystemCommand: + case CommandTemplate.CommandTypeOneofCase.None: + break; + default: + throw new ArgumentOutOfRangeException(nameof(command.CommandTypeCase), command.CommandTypeCase, null); + } + } + + private async Task AssignImportIdentityAsync(CampaignTemplate template, CancellationToken cancellationToken) + { + template.UniqueId = Guid.NewGuid().ToString(); + template.Name = await MakeUniqueNameAsync(template.Name.Trim(), cancellationToken); + foreach(var parameter in template.PlannableParameters) + parameter.UniqueId = Guid.NewGuid().ToString(); + foreach(var allocation in template.PlannerAllocations) + allocation.UniqueId = Guid.NewGuid().ToString(); + + foreach(var experiment in GetExperiments(template)) + { + experiment.UniqueId = Guid.NewGuid().ToString(); + foreach(var step in experiment.StepTemplates) + { + step.UniqueId = Guid.NewGuid().ToString(); + foreach(var command in step.CommandTemplates) + { + command.UniqueId = Guid.NewGuid().ToString(); + if(command.DeviceCommand?.Metadata is not null) + { + command.DeviceCommand.Metadata.UniqueId = Guid.NewGuid().ToString(); + if(command.DeviceCommand.Metadata.OutputMetadata is not null) + command.DeviceCommand.Metadata.OutputMetadata.UniqueId = Guid.NewGuid().ToString(); + foreach(var metadata in command.DeviceCommand.Metadata.ParameterMetadatas) + metadata.UniqueId = Guid.NewGuid().ToString(); + } + + foreach(var argument in command.ArgumentBindings) + { + argument.UniqueId = Guid.NewGuid().ToString(); + if(argument.Metadata is not null) + argument.Metadata.UniqueId = Guid.NewGuid().ToString(); + if(argument.PlannedSource?.PlanningMetadata is not null) + argument.PlannedSource.PlanningMetadata.UniqueId = Guid.NewGuid().ToString(); + } + } + } + } + } + + private async Task MakeUniqueNameAsync(string name, CancellationToken cancellationToken) + { + if(!await campaignPersistenceService.ExistsByNameAsync(name, cancellationToken)) + return name; + + var index = 1; + while(true) + { + var candidate = index == 1 ? $"{name} (Imported)" : $"{name} (Imported {index})"; + if(!await campaignPersistenceService.ExistsByNameAsync(candidate, cancellationToken)) + return candidate; + index++; + } + } + + private static IEnumerable GetExperiments(CampaignTemplate template) + { + if(template.StartupTemplate is not null) + yield return template.StartupTemplate; + if(template.ExperimentTemplate is not null) + yield return template.ExperimentTemplate; + if(template.CloseoutTemplate is not null) + yield return template.CloseoutTemplate; + } + + private static string MakeFileName(string campaignName) + { + var invalidCharacters = Path.GetInvalidFileNameChars(); + var sanitizedName = new string(campaignName.Select(character => invalidCharacters.Contains(character) ? '_' : character).ToArray()).Trim(); + return $"{(string.IsNullOrWhiteSpace(sanitizedName) ? "campaign" : sanitizedName)}.json"; + } +} diff --git a/Ares.Core/Campaigns/ICampaignTemplatePersistenceService.cs b/Ares.Core/Campaigns/ICampaignTemplatePersistenceService.cs new file mode 100644 index 00000000..08f8275b --- /dev/null +++ b/Ares.Core/Campaigns/ICampaignTemplatePersistenceService.cs @@ -0,0 +1,23 @@ +using Ares.Datamodel.Templates; +using Ares.Services; + +namespace Ares.Core.Campaigns; + +public interface ICampaignTemplatePersistenceService +{ + Task> GetSummariesAsync(CancellationToken cancellationToken = default); + + Task GetByIdAsync(string uniqueId, CancellationToken cancellationToken = default); + + Task GetByNameAsync(string name, CancellationToken cancellationToken = default); + + Task ExistsByIdAsync(string uniqueId, CancellationToken cancellationToken = default); + + Task ExistsByNameAsync(string name, CancellationToken cancellationToken = default); + + Task AddAsync(CampaignTemplate template, CancellationToken cancellationToken = default); + + Task ReplaceAsync(CampaignTemplate template, CancellationToken cancellationToken = default); + + Task DeleteAsync(string uniqueId, CancellationToken cancellationToken = default); +} diff --git a/Ares.Core/Campaigns/ICampaignTemplateTransferService.cs b/Ares.Core/Campaigns/ICampaignTemplateTransferService.cs new file mode 100644 index 00000000..628bf3fc --- /dev/null +++ b/Ares.Core/Campaigns/ICampaignTemplateTransferService.cs @@ -0,0 +1,14 @@ +using Ares.Datamodel.Templates; + +namespace Ares.Core.Campaigns; + +public interface ICampaignTemplateTransferService +{ + Task ExportAsync(string campaignId, CancellationToken cancellationToken = default); + + Task ImportAsync(string json, CancellationToken cancellationToken = default); +} + +public sealed record CampaignTemplateExport(CampaignTemplate Template, string Json, string SuggestedFileName); + +public sealed record CampaignTemplateImportResult(CampaignTemplate Template, IReadOnlyList Warnings); diff --git a/Ares.Core/CoreDatabaseContext.cs b/Ares.Core/CoreDatabaseContext.cs index 4d32e44d..5cf123e0 100644 --- a/Ares.Core/CoreDatabaseContext.cs +++ b/Ares.Core/CoreDatabaseContext.cs @@ -2,6 +2,7 @@ using Ares.Core.EntityConfigurations.Helpers; using Ares.Datamodel; using Ares.Datamodel.Analyzing; +using Ares.Datamodel.Automation; using Ares.Datamodel.Device; using Ares.Datamodel.Planning; using Ares.Datamodel.Templates; @@ -44,6 +45,8 @@ public CoreDatabaseContext(DbContextOptions options) : base(options) public DbSet AnalyzerTransactions => Set(); public DbSet DeviceErrorHandlingConfigs => Set(); public DbSet GeneralSettingsConfigs => Set(); + public DbSet CustomCommands => Set(); + public DbSet CustomCommandVersions => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { var assembly = Assembly.GetAssembly(typeof(CoreDatabaseContext)); diff --git a/Ares.Core/CoreDevice/AresCoreDevice.cs b/Ares.Core/CoreDevice/AresCoreDevice.cs deleted file mode 100644 index bdabab4c..00000000 --- a/Ares.Core/CoreDevice/AresCoreDevice.cs +++ /dev/null @@ -1,220 +0,0 @@ -using Ares.Core.EntityConfigurations.Extensions; -using Ares.Datamodel; -using Ares.Datamodel.Device; -using Ares.Datamodel.Extensions; -using Ares.Datamodel.Factories; -using Ares.Device; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using UnitsNet; - -namespace Ares.Core.CoreDevice; - -public class AresCoreDevice : AresDevice -{ - private readonly BehaviorSubject _stateSubject = new(new AresStruct()); - - public AresCoreDevice() : base(new DeviceConnectionInfo { DeviceName = "ARES", DeviceId = "ARES-CORE-DEVICE" }) - { - Status = new DeviceOperationalStatus() - { - OperationalState = OperationalState.Active - }; - - StateStream = _stateSubject.AsObservable(); - } - - public override Task Activate(CancellationToken ct) - { - return Task.FromResult(true); - } - - public override Task EnterSafeMode(CancellationToken ct) - { - return Task.CompletedTask; - } - - public override Task GetState() - { - return Task.FromResult(new AresStruct()); - } - - public Task Sleep(TimeSpan timeSpan, CancellationToken ct) - { - return Task.Delay(timeSpan, ct); - } - - public override async Task ExecuteCommand(string command, List arguments, CancellationToken token) - { - var result = new CommandResult(); - - if(!Enum.TryParse(command, out AresCoreDeviceCommand commandEnum)) - return new CommandResult { Error = "Unrecognized Command Received in Core Device!", Success = false }; - - var durationParam = arguments.FirstOrDefault(param => param.ArgName == AresCoreDeviceCommandParameter.Duration.ToString())?.ArgValue; - - switch(commandEnum) - { - case AresCoreDeviceCommand.SleepForMilliseconds: - if(durationParam is not null && durationParam.HasNumberValue) - { - var millisecondsDuration = UnitsNet.Duration.FromMilliseconds(durationParam.NumberValue); - await Sleep(millisecondsDuration.ToTimeSpan(), token); - result.Success = true; - break; - } - - else - { - result.Error = "Cannot use Sleep command without specifying a duration!"; - result.Success = false; - break; - } - - case AresCoreDeviceCommand.SleepForSeconds: - if(durationParam is not null && durationParam.HasNumberValue) - { - var secondsDuration = Duration.FromSeconds(durationParam.NumberValue); - await Sleep(secondsDuration.ToTimeSpan(), token); - result.Success = true; - break; - } - - else - { - result.Error = "Cannot use Sleep command without specifying a duration!"; - result.Success = false; - break; - } - - case AresCoreDeviceCommand.SleepForMinutes: - if(durationParam is not null && durationParam.HasNumberValue) - { - var minutesDuration = Duration.FromMinutes(durationParam.NumberValue); - await Sleep(minutesDuration.ToTimeSpan(), token); - result.Success = true; - break; - } - - else - { - result.Error = "Cannot use Sleep command without specifying a duration!"; - result.Success = false; - break; - } - - case AresCoreDeviceCommand.WaitForUser: - result.Success = true; - result.AwaitUserInput = true; - break; - - case AresCoreDeviceCommand.GetTimestamp: - result.Success = true; - result.Result = AresValueHelper.CreateTimestamp(DateTime.UtcNow.ToTimestampUtc()); - break; - - case AresCoreDeviceCommand.CalculateAverage: - var dataToBeAveraged = arguments.FirstOrDefault()?.ArgValue; - - if(dataToBeAveraged is null) - { - result.Error = "ARES was asked to average a list of data, but no argument was provided."; - result.Success = false; - break; - } - - switch(dataToBeAveraged.KindCase) - { - case AresValue.KindOneofCase.NumberArrayValue: - var average = dataToBeAveraged.NumberArrayValue.Numbers.Average(); - result.Result = AresValueHelper.CreateFloat(average); - result.Success = true; - break; - - case AresValue.KindOneofCase.FloatArrayValue: - var floatAverage = dataToBeAveraged.FloatArrayValue.Floats.Average(); - result.Result = AresValueHelper.CreateFloat(floatAverage); - result.Success = true; - break; - - case AresValue.KindOneofCase.IntArrayValue: - var intAverage = dataToBeAveraged.IntArrayValue.Ints.Average(); - result.Result = AresValueHelper.CreateFloat(intAverage); - result.Success = true; - break; - - default: - result.Error = $"ARES was asked to average a list of data, but an invalid argument was provided of type {dataToBeAveraged.KindCase}"; - result.Success = false; - break; - } - - break; - } - - return result; - } - - protected override Task> BuildCommandDescriptorsAsync() - { - return Task.FromResult>( - [ - new() - { - Name = AresCoreDeviceCommand.SleepForMilliseconds.ToString(), - Description = "Sleep for a given amount of milliseconds", - InputSchema = AresSchemaBuilder.Create(AresCoreDeviceCommandParameter.Duration.ToString(), AresDataType.Number).Build() - }, - - new() - { - Name = AresCoreDeviceCommand.SleepForSeconds.ToString(), - Description = "Sleep for a given amount of seconds", - InputSchema = AresSchemaBuilder.Create(AresCoreDeviceCommandParameter.Duration.ToString(), AresDataType.Number).Build(), - }, - - new() - { - Name = AresCoreDeviceCommand.SleepForMinutes.ToString(), - Description = "Sleep for a given amount of minutes", - InputSchema = AresSchemaBuilder.Create(AresCoreDeviceCommandParameter.Duration.ToString(), AresDataType.Number).Build() - }, - - new() - { - Name = AresCoreDeviceCommand.WaitForUser.ToString(), - Description = "Have ARES request user confirmation before continuing." - }, - - new() - { - Name = AresCoreDeviceCommand.GetTimestamp.ToString(), - Description = "Returns the current time in the form of a timestamp", - OutputSchema = AresSchemaBuilder.TimestampEntry().WithDescription("A timestamp representing the current time") - .Build() - }, - - new() - { - Name = AresCoreDeviceCommand.CalculateAverage.ToString(), - Description = "Takes in a list of numeric values, floats or ints and returns the average of that list of values.", - InputSchema = AresSchemaBuilder.Empty() - .AddEntry(AresCoreDeviceCommandParameter.NumericData.ToString(), AresSchemaBuilder.Entry(AresDataType.NumberArray).Build()).AsOptional() - .AddEntry(AresCoreDeviceCommandParameter.IntData.ToString(), AresSchemaBuilder.Entry(AresDataType.IntArray).Build()).AsOptional() - .AddEntry(AresCoreDeviceCommandParameter.FloatData.ToString(), AresSchemaBuilder.Entry(AresDataType.FloatArray).Build()).AsOptional() - .Build(), - OutputSchema = AresSchemaBuilder.NumberEntry().WithDescription("The average value of the provided list of numeric values").Build() - } - ]); - } - - public override Task UpdateSettings(AresStruct settings) - { - return Task.FromResult(new AresStruct()); - } - - public override Task GetSettings() - => Task.FromResult(new AresStruct()); - - public override IObservable StateStream { get; } -} diff --git a/Ares.Core/CoreDevice/AresCoreDeviceCommand.cs b/Ares.Core/CoreDevice/AresCoreDeviceCommand.cs deleted file mode 100644 index 98dba0a4..00000000 --- a/Ares.Core/CoreDevice/AresCoreDeviceCommand.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Ares.Core.CoreDevice; -public enum AresCoreDeviceCommand -{ - SleepForMilliseconds, - SleepForSeconds, - SleepForMinutes, - WaitForUser, - GetTimestamp, - CalculateAverage - -} diff --git a/Ares.Core/CoreDevice/AresCoreDeviceCommandParameter.cs b/Ares.Core/CoreDevice/AresCoreDeviceCommandParameter.cs deleted file mode 100644 index 80753310..00000000 --- a/Ares.Core/CoreDevice/AresCoreDeviceCommandParameter.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Ares.Core.CoreDevice; -public enum AresCoreDeviceCommandParameter -{ - Duration, - NumericData, - IntData, - FloatData -} diff --git a/Ares.Core/CustomCommandDevice/AresCommandDevice.cs b/Ares.Core/CustomCommandDevice/AresCommandDevice.cs new file mode 100644 index 00000000..a73e1637 --- /dev/null +++ b/Ares.Core/CustomCommandDevice/AresCommandDevice.cs @@ -0,0 +1,56 @@ +using Ares.Core.CustomCommands; +using Ares.Datamodel; +using Ares.Datamodel.Device; +using Ares.Device; +using System.Reactive.Linq; + +namespace Ares.Core.CustomCommandDevice; + +internal class AresCommandDevice : AresDevice +{ + private readonly CustomCommandPersistenceService _commandPersistenceService; + + public AresCommandDevice(CustomCommandPersistenceService commandPersistenceService) : base(new DeviceConnectionInfo { DeviceName = "Custom Command", DeviceId = "ARES-CUSTOM-COMMAND-DEVICE" }) + { + _commandPersistenceService = commandPersistenceService; + } + + public override IObservable StateStream => Observable.Empty(); + + public override Task Activate(CancellationToken ct) + { + return Task.FromResult(true); + } + + public override Task EnterSafeMode(CancellationToken ct) + { + throw new NotImplementedException(); + } + + public override Task ExecuteCommand(string command, List arguments, CancellationToken token) + { + throw new NotImplementedException(); + } + + public override Task GetSettings() + { + return Task.FromResult(new AresStruct()); + } + + public override Task GetState() + { + throw new NotImplementedException(); + } + + public override Task UpdateSettings(AresStruct settings) + { + throw new NotImplementedException(); + } + + protected override async Task> BuildCommandDescriptorsAsync() + { + throw new NotImplementedException(); + //var commands = await _commandPersistenceService.GetSummariesAsync(); + //var descriptors = commands.Select(cmd => new DeviceCommandDescriptor { Name = cmd.Name, Description = cmd.Description, OutputSchema = cmd.OutputSummary }) + } +} diff --git a/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs b/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs new file mode 100644 index 00000000..8db510be --- /dev/null +++ b/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs @@ -0,0 +1,170 @@ +using Ares.Datamodel.Automation; +using Ares.Datamodel.Extensions; +using Microsoft.EntityFrameworkCore; +using CustomCommandModel = Ares.Datamodel.Automation.CustomCommand; +using CustomCommandVersionModel = Ares.Datamodel.Automation.CustomCommandVersion; + +namespace Ares.Core.CustomCommands; + +internal sealed class CustomCommandPersistenceService(IDbContextFactory dbContextFactory) + : ICustomCommandPersistenceService +{ + private const string UniqueIdPropertyName = "UniqueId"; + + public async Task> GetSummariesAsync() + { + var commands = await GetCurrentVersionsAsync(); + return commands + .Select(command => new CustomCommandSummary( + ParseEntityId(command.CustomCommandId), + string.IsNullOrWhiteSpace(command.Name) ? "(Unnamed command)" : command.Name, + command.Description, + BuildInputSummary(command), + BuildOutputSummary(command))) + .OrderBy(command => command.Name) + .ToArray(); + } + + public async Task GetAsync(Guid id) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + var currentVersionId = await context.CustomCommands + .AsNoTracking() + .Where(command => EF.Property(command, UniqueIdPropertyName) == id.ToString()) + .Select(command => command.CurrentVersionId) + .FirstOrDefaultAsync(); + + return string.IsNullOrWhiteSpace(currentVersionId) + ? null + : await GetVersionAsync(context, currentVersionId); + } + + public async Task SaveAsync(Guid? id, CustomCommandVersionModel command) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + var commandId = id ?? Guid.NewGuid(); + var existingCommand = id is null + ? null + : await context.CustomCommands + .FirstOrDefaultAsync(command => EF.Property(command, UniqueIdPropertyName) == id.Value.ToString()); + + var nextVersionNumber = existingCommand is null + ? 1 + : await context.CustomCommandVersions + .Where(version => version.CustomCommandId == commandId.ToString()) + .Select(version => (long?)version.VersionNumber) + .MaxAsync() ?? 0; + + if(existingCommand is not null) + nextVersionNumber++; + + var version = CreateVersion(commandId, nextVersionNumber, command); + context.CustomCommandVersions.Add(version); + var versionId = AssignEntityId(context, version); + AssignParameterIds(context, version); + + if(existingCommand is null) + { + existingCommand = new CustomCommandModel(); + context.CustomCommands.Add(existingCommand); + AssignEntityId(context, existingCommand, commandId); + } + + existingCommand.CurrentVersionId = versionId.ToString(); + await context.SaveChangesAsync(); + return commandId; + } + + public async Task DeleteAsync(Guid id) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + var command = await context.CustomCommands + .FirstOrDefaultAsync(command => EF.Property(command, UniqueIdPropertyName) == id.ToString()); + + if(command is null) + return; + + context.CustomCommands.Remove(command); + await context.SaveChangesAsync(); + } + + public Task> GetCommandsAsync() => GetCurrentVersionsAsync(); + + private async Task> GetCurrentVersionsAsync() + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + var versionIds = await context.CustomCommands + .AsNoTracking() + .Select(command => command.CurrentVersionId) + .Where(id => id != null && id != string.Empty) + .ToListAsync(); + + return await context.CustomCommandVersions + .AsNoTracking() + .Include(version => version.InputParameters) + .Where(version => versionIds.Contains(EF.Property(version, UniqueIdPropertyName))) + .ToListAsync(); + } + + private static Task GetVersionAsync(CoreDatabaseContext context, string versionId) + { + return context.CustomCommandVersions + .AsNoTracking() + .Include(version => version.InputParameters) + .FirstOrDefaultAsync(version => EF.Property(version, UniqueIdPropertyName) == versionId); + } + + private static Guid ParseEntityId(string? id) => Guid.TryParse(id, out var guid) ? guid : Guid.Empty; + + private static void AssignEntityId(CoreDatabaseContext context, CustomCommandModel command, Guid id) + { + command.UniqueId = id.ToString(); + context.Entry(command).Property(UniqueIdPropertyName).CurrentValue = command.UniqueId; + } + + private static Guid AssignEntityId(CoreDatabaseContext context, CustomCommandVersionModel version) + { + var id = Guid.NewGuid(); + version.UniqueId = id.ToString(); + context.Entry(version).Property(UniqueIdPropertyName).CurrentValue = version.UniqueId; + return id; + } + + private static void AssignParameterIds(CoreDatabaseContext context, CustomCommandVersionModel version) + { + foreach(var parameter in version.InputParameters) + { + var entry = context.Entry(parameter); + if(entry.State == EntityState.Detached) + entry.State = EntityState.Added; + + entry.Property(UniqueIdPropertyName).CurrentValue = Guid.NewGuid().ToString(); + } + } + + private static CustomCommandVersionModel CreateVersion( + Guid commandId, + long versionNumber, + CustomCommandVersionModel command) + { + var version = command.Clone(); + version.UniqueId = string.Empty; + version.CustomCommandId = commandId.ToString(); + version.VersionNumber = versionNumber; + return version; + } + + private static string BuildInputSummary(CustomCommandVersionModel command) + { + return command.InputParameters.Count == 0 + ? "None" + : string.Join(", ", command.InputParameters.Select(parameter => parameter.Name)); + } + + private static string BuildOutputSummary(CustomCommandVersionModel command) + { + return command.OutputSchema is null + ? "Unspecified" + : command.OutputSchema.Stringify(); + } +} diff --git a/Ares.Core/CustomCommands/CustomCommandSummary.cs b/Ares.Core/CustomCommands/CustomCommandSummary.cs new file mode 100644 index 00000000..ac896a80 --- /dev/null +++ b/Ares.Core/CustomCommands/CustomCommandSummary.cs @@ -0,0 +1,8 @@ +namespace Ares.Core.CustomCommands; + +public sealed record CustomCommandSummary( + Guid Id, + string Name, + string Description, + string InputSummary, + string OutputSummary); diff --git a/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs b/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs new file mode 100644 index 00000000..9188b5f8 --- /dev/null +++ b/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs @@ -0,0 +1,16 @@ +using Ares.Datamodel.Automation; + +namespace Ares.Core.CustomCommands; + +public interface ICustomCommandPersistenceService +{ + Task> GetSummariesAsync(); + + Task> GetCommandsAsync(); + + Task GetAsync(Guid id); + + Task SaveAsync(Guid? id, CustomCommandVersion command); + + Task DeleteAsync(Guid id); +} diff --git a/Ares.Core/DataManagement/DataMappers/CampaignDatasetGenerator.cs b/Ares.Core/DataManagement/DataMappers/CampaignDatasetGenerator.cs index f41a3f10..c3566560 100644 --- a/Ares.Core/DataManagement/DataMappers/CampaignDatasetGenerator.cs +++ b/Ares.Core/DataManagement/DataMappers/CampaignDatasetGenerator.cs @@ -271,7 +271,7 @@ private static IEnumerable CreateCommandInputColumns(IEnumerable foreach(var commandRecord in commandRecords) { - foreach(var parameter in commandRecord.Template?.Parameters ?? []) + foreach(var parameter in commandRecord.Template?.ArgumentBindings ?? []) { cancellationToken.ThrowIfCancellationRequested(); var value = parameter.GetValue(); @@ -474,7 +474,7 @@ private static AresDataRow CreateCommandRow(CommandRecord record, CancellationTo AddExecutionFields(data, record.Command.ExecutionInfo); AddString(data, StatusColumnName, record.Command.StatusCode.ToString()); - foreach(var parameter in record.Template?.Parameters ?? []) + foreach(var parameter in record.Template?.ArgumentBindings ?? []) { cancellationToken.ThrowIfCancellationRequested(); var value = parameter.GetValue(); diff --git a/Ares.Core/Device/Managers/DeviceManager.cs b/Ares.Core/Device/Managers/DeviceManager.cs index 36af1e7c..75b5de05 100644 --- a/Ares.Core/Device/Managers/DeviceManager.cs +++ b/Ares.Core/Device/Managers/DeviceManager.cs @@ -1,4 +1,3 @@ -using Ares.Core.CoreDevice; using Ares.Core.Device.Providers; using Ares.Core.Device.Repos; using Ares.Core.Notifications; @@ -48,9 +47,6 @@ public DeviceManager( public void Initialize() { - var coreDevice = new AresCoreDevice(); - _deviceRepo.AddOrUpdate(coreDevice); - _configProvider.Connect() .SelectMany(async changes => { @@ -158,4 +154,4 @@ public async Task Remove(string deviceId) await Remove(deviceId); return await Load(deviceId, config); } -} \ No newline at end of file +} diff --git a/Ares.Core/EntityConfigurations/Automation/CustomCommandEntityConfiguration.cs b/Ares.Core/EntityConfigurations/Automation/CustomCommandEntityConfiguration.cs new file mode 100644 index 00000000..93ad6094 --- /dev/null +++ b/Ares.Core/EntityConfigurations/Automation/CustomCommandEntityConfiguration.cs @@ -0,0 +1,14 @@ +using Ares.Datamodel.Automation; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Ares.Core.EntityConfigurations.Automation; + +internal class CustomCommandEntityConfiguration : AresEntityTypeBaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + builder.ToTable("CustomCommands"); + } +} diff --git a/Ares.Core/EntityConfigurations/Automation/CustomCommandParameterEntityConfiguration.cs b/Ares.Core/EntityConfigurations/Automation/CustomCommandParameterEntityConfiguration.cs new file mode 100644 index 00000000..2ce0683d --- /dev/null +++ b/Ares.Core/EntityConfigurations/Automation/CustomCommandParameterEntityConfiguration.cs @@ -0,0 +1,14 @@ +using Ares.Datamodel.Automation; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Ares.Core.EntityConfigurations.Automation; + +internal class CustomCommandParameterEntityConfiguration : AresEntityTypeBaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + builder.ToTable("CustomCommandParameters"); + } +} diff --git a/Ares.Core/EntityConfigurations/Automation/CustomCommandVersionEntityConfiguration.cs b/Ares.Core/EntityConfigurations/Automation/CustomCommandVersionEntityConfiguration.cs new file mode 100644 index 00000000..cab6581e --- /dev/null +++ b/Ares.Core/EntityConfigurations/Automation/CustomCommandVersionEntityConfiguration.cs @@ -0,0 +1,33 @@ +using Ares.Datamodel.Automation; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Ares.Core.EntityConfigurations.Automation; + +internal class CustomCommandVersionEntityConfiguration : AresEntityTypeBaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + builder.ToTable("CustomCommandVersions"); + + builder.HasIndex(version => new { version.CustomCommandId, version.VersionNumber }) + .IsUnique(); + + builder.HasOne() + .WithMany() + .HasForeignKey(version => version.CustomCommandId) + .HasPrincipalKey("UniqueId") + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.HasMany(version => version.InputParameters) + .WithOne() + .HasForeignKey("CustomCommandVersionId") + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + + builder.Navigation(version => version.InputParameters) + .AutoInclude(); + } +} diff --git a/Ares.Core/EntityConfigurations/Execution/Commands/CommandMetadataEntityConfiguration.cs b/Ares.Core/EntityConfigurations/Execution/Commands/CommandMetadataEntityConfiguration.cs index 9c65551d..49f5d7e9 100644 --- a/Ares.Core/EntityConfigurations/Execution/Commands/CommandMetadataEntityConfiguration.cs +++ b/Ares.Core/EntityConfigurations/Execution/Commands/CommandMetadataEntityConfiguration.cs @@ -13,14 +13,6 @@ public override void Configure(EntityTypeBuilder builder) .WithOne() .OnDelete(DeleteBehavior.ClientCascade); - // TODO figure out how to deal with/remove metadata upon removal of a command template - // the commented code below works only if there is a new instance of a CommandMetadata every time - // it is used, otherwise the foreign key gets overwritten with every update - builder.HasOne() - .WithOne(commandTemplate => commandTemplate.Metadata) - .HasForeignKey("CommandTemplateId") - .IsRequired(); - builder.HasOne(commandMetadata => commandMetadata.OutputMetadata) .WithOne() .HasForeignKey() diff --git a/Ares.Core/EntityConfigurations/Execution/Commands/CommandTemplateEntityConfiguration.cs b/Ares.Core/EntityConfigurations/Execution/Commands/CommandTemplateEntityConfiguration.cs index 0a9fb326..47d9c847 100644 --- a/Ares.Core/EntityConfigurations/Execution/Commands/CommandTemplateEntityConfiguration.cs +++ b/Ares.Core/EntityConfigurations/Execution/Commands/CommandTemplateEntityConfiguration.cs @@ -10,18 +10,21 @@ public override void Configure(EntityTypeBuilder builder) { base.Configure(builder); builder.ToTable("CommandTemplates"); - builder.HasMany(commandTemplate => commandTemplate.Parameters) + builder.HasMany(commandTemplate => commandTemplate.ArgumentBindings) .WithOne() .OnDelete(DeleteBehavior.Cascade); - builder.HasOne(template => template.Metadata) + builder.HasOne(template => template.DeviceCommand) .WithOne() - .HasForeignKey("CommandTemplateId"); + .HasForeignKey("CommandTemplateId") + .OnDelete(DeleteBehavior.Cascade); + builder.OwnsOne(template => template.SystemCommand); + builder.OwnsOne(template => template.CustomCommandInvocation); - builder.Navigation(commandTemplate => commandTemplate.Parameters) + builder.Navigation(template => template.DeviceCommand) .AutoInclude(); - builder.Navigation(commandTemplate => commandTemplate.Metadata) + builder.Navigation(commandTemplate => commandTemplate.ArgumentBindings) .AutoInclude(); } } diff --git a/Ares.Core/EntityConfigurations/Execution/Commands/DeviceCommandEntityConfiguration.cs b/Ares.Core/EntityConfigurations/Execution/Commands/DeviceCommandEntityConfiguration.cs new file mode 100644 index 00000000..caec5ae6 --- /dev/null +++ b/Ares.Core/EntityConfigurations/Execution/Commands/DeviceCommandEntityConfiguration.cs @@ -0,0 +1,22 @@ +using Ares.Datamodel.Templates; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Ares.Core.EntityConfigurations.Execution.Commands; + +internal class DeviceCommandEntityConfiguration : AresEntityTypeBaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + base.Configure(builder); + builder.ToTable("DeviceCommands"); + + builder.HasOne(command => command.Metadata) + .WithOne() + .HasForeignKey("DeviceCommandId") + .OnDelete(DeleteBehavior.Cascade); + + builder.Navigation(command => command.Metadata) + .AutoInclude(); + } +} diff --git a/Ares.Core/Execution/ExecutionManager.cs b/Ares.Core/Execution/ExecutionManager.cs index 08163c4e..9139d1bf 100644 --- a/Ares.Core/Execution/ExecutionManager.cs +++ b/Ares.Core/Execution/ExecutionManager.cs @@ -23,6 +23,7 @@ public class ExecutionManager : IExecutionManager private readonly IEnumerable _startConditions; private readonly IExecutionSafetyManager _safetyManager; private readonly INotifier _notifier; + private readonly ICommandDisplayNameResolver _commandDisplayNameResolver; private readonly ILogger _logger; private ExecutionControlTokenSource? _executionControlTokenSource; private ICampaignExecutor? _activeExecutor; @@ -32,6 +33,7 @@ public ExecutionManager(IEnumerable startConditions, IActiveCampaignTemplateStore activeCampaignTemplateStore, IExecutionSafetyManager safetyManager, ICommandComposer campaignComposer, + ICommandDisplayNameResolver commandDisplayNameResolver, ILogger logger, INotifier notifier) { @@ -39,6 +41,7 @@ public ExecutionManager(IEnumerable startConditions, _dbContextFactory = dbContextFactory; _activeCampaignTemplateStore = activeCampaignTemplateStore; _campaignComposer = campaignComposer; + _commandDisplayNameResolver = commandDisplayNameResolver; _safetyManager = safetyManager; _logger = logger; _notifier = notifier; @@ -63,6 +66,7 @@ public async Task Start(string executionNotes, List campaignTag { throw new InvalidOperationException(err); } + await _commandDisplayNameResolver.RefreshAsync(); var executor = _campaignComposer.Compose(_activeCampaignTemplateStore.CampaignTemplate!); _activeExecutor = executor; @@ -133,7 +137,7 @@ public bool EnsureParameterAssignment() { var experimentCommandsInvalid = _activeCampaignTemplateStore.CampaignTemplate!.ExperimentTemplate.StepTemplates .SelectMany(step => step.CommandTemplates) - .Any(cmd => cmd.Parameters.Any(param => param.IsPlanned() && param.GetPlanningMetadata() is null)); + .Any(cmd => cmd.ArgumentBindings.Any(param => param.IsPlanned() && param.GetPlanningMetadata() is null)); if(experimentCommandsInvalid) return false; diff --git a/Ares.Core/Execution/Executors/CommandDisplayNameResolver.cs b/Ares.Core/Execution/Executors/CommandDisplayNameResolver.cs new file mode 100644 index 00000000..6d6cc01d --- /dev/null +++ b/Ares.Core/Execution/Executors/CommandDisplayNameResolver.cs @@ -0,0 +1,50 @@ +using Ares.Core.CustomCommands; +using Ares.Datamodel.Templates; +using Microsoft.Extensions.Logging; + +namespace Ares.Core.Execution.Executors; + +internal class CommandDisplayNameResolver( + ICustomCommandPersistenceService customCommandPersistenceService, + ILogger logger) : ICommandDisplayNameResolver +{ + private IReadOnlyDictionary _customCommandNames = CreateEmptyLookup(); + + public async Task RefreshAsync() + { + try + { + var commands = await customCommandPersistenceService.GetCommandsAsync(); + _customCommandNames = commands + .Where(command => !string.IsNullOrWhiteSpace(command.CustomCommandId) && !string.IsNullOrWhiteSpace(command.Name)) + .GroupBy(command => command.CustomCommandId, StringComparer.OrdinalIgnoreCase) + .ToDictionary( + group => group.Key, + group => group.First().Name.Trim(), + StringComparer.OrdinalIgnoreCase); + } + catch(Exception ex) + { + _customCommandNames = CreateEmptyLookup(); + logger.LogWarning(ex, "Could not load custom-command names. Generic names will be used for this campaign."); + } + } + + public string Resolve(CommandTemplate template) + => template.CommandTypeCase switch + { + CommandTemplate.CommandTypeOneofCase.None => "Undefined Command", + CommandTemplate.CommandTypeOneofCase.DeviceCommand => template.DeviceCommand.Metadata.Name, + CommandTemplate.CommandTypeOneofCase.SystemCommand => template.SystemCommand.Operation.ToString(), + CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation => ResolveCustomCommand(template.CustomCommandInvocation.CustomCommandId), + _ => "Undefined Command" + }; + + private string ResolveCustomCommand(string customCommandId) + => !string.IsNullOrWhiteSpace(customCommandId) && _customCommandNames.TryGetValue(customCommandId, out var name) + ? name + : "Custom Command"; + + private static IReadOnlyDictionary CreateEmptyLookup() + => new Dictionary(StringComparer.OrdinalIgnoreCase); +} diff --git a/Ares.Core/Execution/Executors/CommandExecutor.cs b/Ares.Core/Execution/Executors/CommandExecutor.cs index 4d9600d9..09d0c5f3 100644 --- a/Ares.Core/Execution/Executors/CommandExecutor.cs +++ b/Ares.Core/Execution/Executors/CommandExecutor.cs @@ -14,18 +14,25 @@ public class CommandExecutor : IExecutor> _command; private readonly BehaviorSubject _stateSubject; private readonly INotifier _notifier; - private readonly ISystemSettingsManager _settingsManager; + private readonly ISystemSettingsManager _settingsManager; + private readonly string _commandName; - public CommandExecutor(Func> command, CommandTemplate template, INotifier notifier, ISystemSettingsManager settingsManager) + + public CommandExecutor(Func> command, CommandTemplate template, string commandName, INotifier notifier, ISystemSettingsManager settingsManager) { _command = command; + _commandName = commandName; Template = template; + var executionTarget = template.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.DeviceCommand + ? template.DeviceCommand.Metadata.DeviceType + : "ARES"; + var executionStatus = new CommandExecutionStatus { CommandId = template.UniqueId, - CommandName = template.Metadata.Name, - DeviceName = template.Metadata.DeviceType, + CommandName = commandName, + DeviceName = executionTarget, State = ExecutionState.Undefined }; @@ -56,7 +63,7 @@ public async Task Execute(ExecutionControlToken token, await token.WaitForResumeAsync(); } catch(OperationCanceledException) - { + { } if(token.IsCancelled) @@ -64,12 +71,12 @@ public async Task Execute(ExecutionControlToken token, Status.State = ExecutionState.Failed; _stateSubject.OnNext(Status); _stateSubject.OnCompleted(); - return ExecutorSummaryHelpers.CreateCommandExecutionSummary(Template, null, DateTime.UtcNow, DateTime.UtcNow); + return ExecutorSummaryHelpers.CreateCommandExecutionSummary(Template, _commandName, null, DateTime.UtcNow, DateTime.UtcNow); } var timeStarted = DateTime.UtcNow; var execInfo = new ExecutionInfo { TimeStarted = DateTime.UtcNow.ToTimestamp() }; - var variableResolutionError = CommandVariableResolver.ResolveParameters(Template.Parameters, variableScope); + var variableResolutionError = CommandVariableResolver.ResolveParameters(Template.ArgumentBindings, variableScope); CommandResult result; if(variableResolutionError is null) @@ -110,7 +117,7 @@ public async Task Execute(ExecutionControlToken token, Status.Result = result.Result; _stateSubject.OnNext(Status); - return ExecutorSummaryHelpers.CreateCommandExecutionSummary(Template, result, timeStarted, DateTime.UtcNow); + return ExecutorSummaryHelpers.CreateCommandExecutionSummary(Template, _commandName, result, timeStarted, DateTime.UtcNow); } private async Task InternalExecute(CancellationToken token) diff --git a/Ares.Core/Execution/Executors/Composers/StepComposer.cs b/Ares.Core/Execution/Executors/Composers/StepComposer.cs index e8ad5808..34bc8e85 100644 --- a/Ares.Core/Execution/Executors/Composers/StepComposer.cs +++ b/Ares.Core/Execution/Executors/Composers/StepComposer.cs @@ -13,15 +13,24 @@ public class StepComposer : ICommandComposer private readonly INotifier _notifier; private readonly IAresDeviceRepo _deviceRepo; private readonly ISystemSettingsManager _settingsManager; - public StepComposer(IAresDeviceRepo deviceRepo, INotifier notifier, ISystemSettingsManager settingsManager) + private readonly CustomCommandExecutor? _customCommandExecutor; + private readonly ICommandDisplayNameResolver _commandDisplayNameResolver; + public StepComposer( + IAresDeviceRepo deviceRepo, + INotifier notifier, + ISystemSettingsManager settingsManager, + ICommandDisplayNameResolver commandDisplayNameResolver, + CustomCommandExecutor? customCommandExecutor = null) { _deviceRepo = deviceRepo; _notifier = notifier; _settingsManager = settingsManager; + _commandDisplayNameResolver = commandDisplayNameResolver; + _customCommandExecutor = customCommandExecutor; } public StepExecutor Compose(StepTemplate template) - { + { var executables = template .CommandTemplates @@ -30,21 +39,50 @@ public StepExecutor Compose(StepTemplate template) ( commandTemplate => { - var deviceId = commandTemplate.Metadata?.DeviceId; + var commandName = _commandDisplayNameResolver.Resolve(commandTemplate); + + if(commandTemplate.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.SystemCommand) + { + Func> systemAction = ct => SystemOperationExecutor.Execute( + commandTemplate.SystemCommand.Operation, + commandTemplate.ArgumentBindings, + ct); + + return new CommandExecutor(systemAction, commandTemplate, commandName, _notifier, _settingsManager); + } + + if(commandTemplate.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation) + { + if(_customCommandExecutor is null) + throw new InvalidOperationException("Custom command execution has not been configured."); + + Func> customCommandAction = ct => _customCommandExecutor.Execute( + commandTemplate.CustomCommandInvocation.CustomCommandId, + commandTemplate.ArgumentBindings, + ct); + + return new CommandExecutor(customCommandAction, commandTemplate, commandName, _notifier, _settingsManager); + } + + if(commandTemplate.CommandTypeCase != CommandTemplate.CommandTypeOneofCase.DeviceCommand) + throw new InvalidOperationException($"Unsupported command type: {commandTemplate.CommandTypeCase}"); + + var metadata = commandTemplate.DeviceCommand?.Metadata; + var deviceId = metadata?.DeviceId; if(deviceId is null) throw new InvalidOperationException("Device ID was null when attempting to retrieve the command interpreter"); var device = _deviceRepo.FirstOrDefault(d => d.UniqueId == deviceId); - if(device is not null && commandTemplate.Metadata is not null) + if(device is not null && metadata is not null) { Func> internalAction = async (ct) => await device.ExecuteCommand( - commandTemplate.Metadata.Name, - commandTemplate.Parameters.Select(p => new DeviceCommandArgument() { ArgName = p.Metadata.Name, ArgValue = p.GetValue() }).ToList(), + metadata.Name, + commandTemplate.ArgumentBindings.Select(p => new DeviceCommandArgument() { ArgName = p.Metadata.Name, ArgValue = p.GetValue() }).ToList(), ct); - return new CommandExecutor(internalAction, commandTemplate, _notifier, _settingsManager); + return new CommandExecutor(internalAction, commandTemplate, commandName, _notifier, _settingsManager); } throw new InvalidOperationException("I'm not certain what to do here yet :("); diff --git a/Ares.Core/Execution/Executors/CustomCommandExecutor.cs b/Ares.Core/Execution/Executors/CustomCommandExecutor.cs new file mode 100644 index 00000000..7996e535 --- /dev/null +++ b/Ares.Core/Execution/Executors/CustomCommandExecutor.cs @@ -0,0 +1,133 @@ +using Ares.Core.CustomCommands; +using Ares.Core.Scripting; +using Ares.Datamodel; +using Ares.Datamodel.Automation; +using Ares.Datamodel.Extensions; +using Ares.Datamodel.Templates; +using AresScript.ScriptBuilding; +using AresScript.Symbols; + +namespace Ares.Core.Execution.Executors; + +public sealed class CustomCommandExecutor( + ICustomCommandPersistenceService customCommandPersistenceService, + BaseEnvironmentBuilder environmentBuilder) +{ + private const string ArgumentVariablePrefix = "__custom_command_argument_"; + private const string ResultVariableName = "__custom_command_result"; + + public async Task Execute( + string customCommandId, + IReadOnlyList argumentBindings, + CancellationToken token) + { + if(!Guid.TryParse(customCommandId, out var commandId)) + return FailedResult($"Custom command id '{customCommandId}' is invalid."); + + var command = await customCommandPersistenceService.GetAsync(commandId); + if(command is null) + return FailedResult($"Custom command '{customCommandId}' was not found."); + + var bindingValues = ResolveBindings(command, argumentBindings, out var bindingError); + if(bindingError is not null) + return FailedResult(bindingError); + + var environment = await environmentBuilder.Build(); + for(var i = 0; i < bindingValues.Length; i++) + environment.AssignVariable($"{ArgumentVariablePrefix}{i}", bindingValues[i]); + + var functionName = CustomCommandScriptBuilder.BuildFunctionName(command.Name); + var script = BuildInvocationScript(command, functionName, bindingValues.Length); + var runner = new ScriptRunner(environment); + + try + { + await runner.RunScriptAsync(script, token); + } + catch(OperationCanceledException) when(token.IsCancellationRequested) + { + throw; + } + catch(Exception exception) + { + return FailedResult(exception.Message); + } + + return environment.TryGetUserValue(ResultVariableName, out var result) + ? new CommandResult { Success = true, Result = result } + : FailedResult($"Custom command '{command.Name}' completed without producing a result."); + } + + private static AresValue[] ResolveBindings( + CustomCommandVersion command, + IReadOnlyList argumentBindings, + out string? error) + { + var bindingsByName = new Dictionary(StringComparer.Ordinal); + foreach(var binding in argumentBindings) + { + var name = binding.Metadata?.Name; + if(string.IsNullOrWhiteSpace(name)) + { + error = "Custom command argument bindings must have parameter metadata names."; + return []; + } + + if(!bindingsByName.TryAdd(name, binding)) + { + error = $"Custom command has multiple bindings for parameter '{name}'."; + return []; + } + } + + var values = new AresValue[command.InputParameters.Count]; + for(var i = 0; i < command.InputParameters.Count; i++) + { + var input = command.InputParameters[i]; + if(!bindingsByName.Remove(input.Name, out var binding)) + { + error = $"Custom command '{command.Name}' requires an argument named '{input.Name}'."; + return []; + } + + var value = binding.GetValue(); + if(value is null) + { + error = $"Custom command argument '{input.Name}' does not have a resolved value."; + return []; + } + + values[i] = value.Clone(); + } + + if(bindingsByName.Count > 0) + { + error = $"Custom command '{command.Name}' received an unknown argument named '{bindingsByName.Keys.First()}'."; + return []; + } + + error = null; + return values; + } + + private static string BuildInvocationScript(CustomCommandVersion command, string functionName, int argumentCount) + { + var parameters = command.InputParameters.Select(parameter => new AresScriptParameter( + parameter.Name, + parameter.Schema ?? new AresValueSchema())); + var wrappedScript = CustomCommandScriptBuilder.BuildWrappedScript( + command.Name, + parameters, + command.OutputSchema, + command.ScriptBody); + var arguments = string.Join(", ", Enumerable.Range(0, argumentCount).Select(index => $"{ArgumentVariablePrefix}{index}")); + + return $"{wrappedScript}\n\n{ResultVariableName} = {functionName}({arguments})"; + } + + private static CommandResult FailedResult(string error) => new() + { + Success = false, + Error = error + }; +} diff --git a/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs b/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs index 7492dcc3..ee419164 100644 --- a/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs +++ b/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs @@ -44,10 +44,15 @@ public static StepExecutionSummary CreateEmptyStepExecutionSummary(DateTime star return new StepExecutionSummary { UniqueId = Guid.NewGuid().ToString(), ExecutionInfo = MakeExecutionInfo(startTime, endTime) }; } public static CommandExecutionSummary CreateCommandExecutionSummary(CommandTemplate template, + string commandName, CommandResult? deviceResult, DateTime startTime, DateTime endTime) { + var commandDescription = template.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.DeviceCommand + ? template.DeviceCommand.Metadata.Description + : string.Empty; + var commandExecutionSummary = new CommandExecutionSummary { UniqueId = Guid.NewGuid().ToString(), @@ -55,8 +60,8 @@ public static CommandExecutionSummary CreateCommandExecutionSummary(CommandTempl CommandId = Guid.NewGuid().ToString(), Result = deviceResult, TemplateId = template.UniqueId, - CommandDescription = template.Metadata.Description, - CommandName = template.Metadata.Name, + CommandDescription = commandDescription, + CommandName = commandName, StatusCode = deviceResult?.StatusCode ?? CommandStatusCode.StatusUnspecified }; diff --git a/Ares.Core/Execution/Executors/ICommandDisplayNameResolver.cs b/Ares.Core/Execution/Executors/ICommandDisplayNameResolver.cs new file mode 100644 index 00000000..51257fc1 --- /dev/null +++ b/Ares.Core/Execution/Executors/ICommandDisplayNameResolver.cs @@ -0,0 +1,10 @@ +using Ares.Datamodel.Templates; + +namespace Ares.Core.Execution.Executors; + +public interface ICommandDisplayNameResolver +{ + Task RefreshAsync(); + + string Resolve(CommandTemplate template); +} diff --git a/Ares.Core/Execution/Executors/SystemOperationCatalog.cs b/Ares.Core/Execution/Executors/SystemOperationCatalog.cs new file mode 100644 index 00000000..f1236d0d --- /dev/null +++ b/Ares.Core/Execution/Executors/SystemOperationCatalog.cs @@ -0,0 +1,84 @@ +using Ares.Datamodel; +using Ares.Datamodel.Factories; +using Ares.Datamodel.Templates; + +namespace Ares.Core.Execution.Executors; + +public sealed record SystemOperationDefinition( + SystemOperation Operation, + string DisplayName, + string Description, + IReadOnlyList Parameters, + AresValueSchema? OutputSchema); + +public static class SystemOperationCatalog +{ + private static readonly IReadOnlyList _definitions = + [ + CreateSleepDefinition( + SystemOperation.SleepForMilliseconds, + "Sleep For Milliseconds", + "Pause execution for a specified number of milliseconds."), + CreateSleepDefinition( + SystemOperation.SleepForSeconds, + "Sleep For Seconds", + "Pause execution for a specified number of seconds."), + CreateSleepDefinition( + SystemOperation.SleepForMinutes, + "Sleep For Minutes", + "Pause execution for a specified number of minutes."), + new SystemOperationDefinition( + SystemOperation.WaitForUser, + "Wait For User", + "Pause execution until the user resumes the experiment.", + [], + null), + new SystemOperationDefinition( + SystemOperation.WaitForUserInput, + "Wait For User Input", + "Pause execution until the user resumes the experiment.", + [], + null), + new SystemOperationDefinition( + SystemOperation.GetTimestamp, + "Get Timestamp", + "Return the current UTC timestamp.", + [], + AresSchemaBuilder.TimestampEntry() + .WithDescription("The current UTC timestamp.") + .Build()), + new SystemOperationDefinition( + SystemOperation.CalculateAverage, + "Calculate Average", + "Calculate the average of a list of numeric values.", + [CreateParameter("Data", AresDataType.NumberArray)], + AresSchemaBuilder.NumberEntry() + .WithDescription("The average of the supplied values.") + .Build()) + ]; + + public static IReadOnlyList Definitions => _definitions; + + public static SystemOperationDefinition? Find(SystemOperation operation) + => _definitions.FirstOrDefault(definition => definition.Operation == operation); + + private static SystemOperationDefinition CreateSleepDefinition( + SystemOperation operation, + string displayName, + string description) + => new( + operation, + displayName, + description, + [CreateParameter("Duration", AresDataType.Number)], + null); + + private static ParameterMetadata CreateParameter(string name, AresDataType type) + => new() + { + UniqueId = Guid.NewGuid().ToString(), + Name = name, + NotPlannable = true, + Schema = AresSchemaBuilder.Entry(type).Build() + }; +} diff --git a/Ares.Core/Execution/Executors/SystemOperationExecutor.cs b/Ares.Core/Execution/Executors/SystemOperationExecutor.cs new file mode 100644 index 00000000..86d96234 --- /dev/null +++ b/Ares.Core/Execution/Executors/SystemOperationExecutor.cs @@ -0,0 +1,92 @@ +using Ares.Datamodel; +using Ares.Datamodel.Extensions; +using Ares.Datamodel.Templates; +using UnitsNet; +using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp; + +namespace Ares.Core.Execution.Executors; + +public static class SystemOperationExecutor +{ + public static async Task Execute( + SystemOperation operation, + IReadOnlyList argumentBindings, + CancellationToken token) + { + var argument = argumentBindings.FirstOrDefault()?.GetValue(); + + return operation switch + { + SystemOperation.SleepForMilliseconds => await Sleep(argument, value => Duration.FromMilliseconds(value), token), + SystemOperation.SleepForSeconds => await Sleep(argument, value => Duration.FromSeconds(value), token), + SystemOperation.SleepForMinutes => await Sleep(argument, value => Duration.FromMinutes(value), token), + SystemOperation.WaitForUser or SystemOperation.WaitForUserInput => new CommandResult + { + Success = true, + AwaitUserInput = true + }, + SystemOperation.GetTimestamp => new CommandResult + { + Success = true, + Result = AresValueHelper.CreateTimestamp(Timestamp.FromDateTime(DateTime.UtcNow)) + }, + SystemOperation.CalculateAverage => CalculateAverage(argument), + _ => new CommandResult + { + Success = false, + Error = $"Unsupported system operation: {operation}" + } + }; + } + + private static async Task Sleep( + AresValue? durationValue, + Func createDuration, + CancellationToken token) + { + if(durationValue?.HasNumberValue != true) + { + return new CommandResult + { + Success = false, + Error = "Cannot use a sleep operation without specifying a numeric duration." + }; + } + + await Task.Delay(createDuration(durationValue.NumberValue).ToTimeSpan(), token); + return new CommandResult { Success = true }; + } + + private static CommandResult CalculateAverage(AresValue? value) + { + if(value is null) + { + return new CommandResult + { + Success = false, + Error = "ARES was asked to average a list of data, but no argument was provided." + }; + } + + return value.KindCase switch + { + AresValue.KindOneofCase.NumberArrayValue => SuccessfulAverage(value.NumberArrayValue.Numbers.Average()), + AresValue.KindOneofCase.FloatArrayValue => SuccessfulAverage(value.FloatArrayValue.Floats.Average()), + AresValue.KindOneofCase.IntArrayValue => SuccessfulAverage(value.IntArrayValue.Ints.Average()), + _ => new CommandResult + { + Success = false, + Error = $"ARES was asked to average a list of data, but an invalid argument was provided of type {value.KindCase}." + } + }; + } + + private static CommandResult SuccessfulAverage(double average) + { + return new CommandResult + { + Success = true, + Result = AresValueHelper.CreateFloat(average) + }; + } +} diff --git a/Ares.Core/Execution/Extensions/ExperimentTemplateExtensions.cs b/Ares.Core/Execution/Extensions/ExperimentTemplateExtensions.cs index 2481d2ff..1bd8c645 100644 --- a/Ares.Core/Execution/Extensions/ExperimentTemplateExtensions.cs +++ b/Ares.Core/Execution/Extensions/ExperimentTemplateExtensions.cs @@ -13,7 +13,7 @@ public static class ExperimentTemplateExtensions public static IEnumerable GetAllParameters(this ExperimentTemplate template) => template.StepTemplates .SelectMany(stepTemplate => stepTemplate.CommandTemplates) - .SelectMany(commandTemplate => commandTemplate.Parameters); + .SelectMany(commandTemplate => commandTemplate.ArgumentBindings); /// /// Gets all the s that are mapped to provide output /> @@ -78,15 +78,17 @@ public static ExperimentTemplate CloneWithNewIds(this ExperimentTemplate templat foreach(var commandTemplate in stepTemplate.CommandTemplates) { var cmdTemplateId = Guid.NewGuid().ToString(); - var outputCmd = template.GetAllOutputCommands().FirstOrDefault(oc => oc.UniqueId == commandTemplate.UniqueId); - commandTemplate.Metadata.UniqueId = Guid.NewGuid().ToString(); commandTemplate.UniqueId = cmdTemplateId; - foreach(var metadataParameterMetadata in commandTemplate.Metadata.ParameterMetadatas) - metadataParameterMetadata.UniqueId = Guid.NewGuid().ToString(); + if(commandTemplate.DeviceCommand?.Metadata is not null) + { + commandTemplate.DeviceCommand.Metadata.UniqueId = Guid.NewGuid().ToString(); + foreach(var metadataParameterMetadata in commandTemplate.DeviceCommand.Metadata.ParameterMetadatas) + metadataParameterMetadata.UniqueId = Guid.NewGuid().ToString(); + } - foreach(var argument in commandTemplate.Parameters) + foreach(var argument in commandTemplate.ArgumentBindings) { argument.UniqueId = Guid.NewGuid().ToString(); argument.Metadata.UniqueId = Guid.NewGuid().ToString(); @@ -108,7 +110,7 @@ public static ExperimentTemplate AssignNewUniquePlanningIds(this ExperimentTempl { foreach(var cmd in step.CommandTemplates) { - foreach(var param in cmd.Parameters) + foreach(var param in cmd.ArgumentBindings) { var planningMetadata = param.GetPlanningMetadata(); if(planningMetadata is not null) diff --git a/Ares.Core/ServiceCollectionExtensions.cs b/Ares.Core/ServiceCollectionExtensions.cs index 3d9f43b5..2f20a0fe 100644 --- a/Ares.Core/ServiceCollectionExtensions.cs +++ b/Ares.Core/ServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ using Ares.Core.Analyzing; using Ares.Core.AresEnvironment; +using Ares.Core.Campaigns; +using Ares.Core.CustomCommands; using Ares.Core.DataManagement.DataMappers; using Ares.Core.Device.Managers; using Ares.Core.Device.Plugins.Drivers; @@ -70,6 +72,11 @@ public static void AddAresCoreComponents(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -136,4 +143,4 @@ private static void BindRepositories(this IServiceCollection services) services.AddSingleton(); } -} \ No newline at end of file +} diff --git a/Ares.Core/Validation/Campaign/GoodAnalyzerCampaignValidator.cs b/Ares.Core/Validation/Campaign/GoodAnalyzerCampaignValidator.cs index 07dec2a7..37d0a423 100644 --- a/Ares.Core/Validation/Campaign/GoodAnalyzerCampaignValidator.cs +++ b/Ares.Core/Validation/Campaign/GoodAnalyzerCampaignValidator.cs @@ -1,18 +1,56 @@ -using Ares.Core.Analyzing; -using Ares.Core.Validation.Validators; -using Ares.Datamodel.Templates; +using Ares.Core.Analyzing; +using Ares.Core.CustomCommands; +using Ares.Core.Execution.Extensions; +using Ares.Datamodel; +using Ares.Core.Validation.Validators; +using Ares.Datamodel.Templates; namespace Ares.Core.Validation.Campaign; -public class GoodAnalyzerCampaignValidator : ICampaignValidator -{ - private readonly IAnalyzerRepo _analyzerManager; - - public GoodAnalyzerCampaignValidator(IAnalyzerRepo analyzerManager) - { - _analyzerManager = analyzerManager; - } - - public Task Validate(CampaignTemplate template) - => GoodAnalyzerValidator.Validate(template.ExperimentTemplate, template.StartupTemplate, _analyzerManager); -} +public class GoodAnalyzerCampaignValidator : ICampaignValidator +{ + private readonly IAnalyzerRepo _analyzerManager; + private readonly ICustomCommandPersistenceService _customCommandPersistenceService; + + public GoodAnalyzerCampaignValidator( + IAnalyzerRepo analyzerManager, + ICustomCommandPersistenceService customCommandPersistenceService) + { + _analyzerManager = analyzerManager; + _customCommandPersistenceService = customCommandPersistenceService; + } + + public async Task Validate(CampaignTemplate template) + { + if(!template.ExperimentTemplate.HasAnalyzerId || string.IsNullOrWhiteSpace(template.ExperimentTemplate.AnalyzerId)) + return await GoodAnalyzerValidator.Validate(template.ExperimentTemplate, template.StartupTemplate, _analyzerManager); + + var outputCommands = template.ExperimentTemplate.GetAllOutputCommands(); + if(template.StartupTemplate is not null) + outputCommands = outputCommands.Concat(template.StartupTemplate.GetAllOutputCommands()).ToArray(); + + var hasCustomCommandOutput = outputCommands.Any(command => + command.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation); + if(!hasCustomCommandOutput) + return await GoodAnalyzerValidator.Validate(template.ExperimentTemplate, template.StartupTemplate, _analyzerManager); + + IReadOnlyDictionary customCommandOutputSchemas; + try + { + customCommandOutputSchemas = (await _customCommandPersistenceService.GetCommandsAsync()) + .Where(command => !string.IsNullOrWhiteSpace(command.CustomCommandId) && command.OutputSchema is not null) + .GroupBy(command => command.CustomCommandId, StringComparer.OrdinalIgnoreCase) + .ToDictionary(group => group.Key, group => group.First().OutputSchema!, StringComparer.OrdinalIgnoreCase); + } + catch(Exception exception) + { + return new ValidationResult(false, $"Unable to load current custom-command definitions for analyzer validation. {exception.Message}"); + } + + return await GoodAnalyzerValidator.Validate( + template.ExperimentTemplate, + template.StartupTemplate, + _analyzerManager, + customCommandOutputSchemas); + } +} diff --git a/Ares.Core/Validation/Campaign/RequiredDeviceInterpretersValidator.cs b/Ares.Core/Validation/Campaign/RequiredDeviceInterpretersValidator.cs index f41674c8..9005b5ed 100644 --- a/Ares.Core/Validation/Campaign/RequiredDeviceInterpretersValidator.cs +++ b/Ares.Core/Validation/Campaign/RequiredDeviceInterpretersValidator.cs @@ -15,7 +15,9 @@ public RequiredDeviceInterpretersValidator(IAresDeviceProvider deviceProvider) public Task Validate(CampaignTemplate template) { var requiredDeviceIds = template.ExperimentTemplate.StepTemplates.SelectMany(stepTemp => - stepTemp.CommandTemplates.Select(cmdTemp => cmdTemp.Metadata.DeviceId)).Distinct().ToArray(); + stepTemp.CommandTemplates + .Where(cmdTemp => cmdTemp.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.DeviceCommand) + .Select(cmdTemp => cmdTemp.DeviceCommand.Metadata.DeviceId)).Distinct().ToArray(); var existingRequiredDevices = requiredDeviceIds .Select(_deviceProvider.GetDevice) diff --git a/Ares.Core/Validation/Validators/GoodAnalyzerValidator.cs b/Ares.Core/Validation/Validators/GoodAnalyzerValidator.cs index 827d0839..96300358 100644 --- a/Ares.Core/Validation/Validators/GoodAnalyzerValidator.cs +++ b/Ares.Core/Validation/Validators/GoodAnalyzerValidator.cs @@ -1,4 +1,5 @@ using Ares.Core.Analyzing; +using Ares.Core.Execution.Executors; using Ares.Core.Execution.Extensions; using Ares.Datamodel; using Ares.Datamodel.Connection; @@ -9,30 +10,37 @@ namespace Ares.Core.Validation.Validators; public static class GoodAnalyzerValidator { public static async Task Validate(ExperimentTemplate experimentTemplate, ExperimentTemplate? startupTemplate, IAnalyzerRepo analyzerRepo) + => await Validate(experimentTemplate, startupTemplate, analyzerRepo, new Dictionary(StringComparer.OrdinalIgnoreCase)); + + public static async Task Validate( + ExperimentTemplate experimentTemplate, + ExperimentTemplate? startupTemplate, + IAnalyzerRepo analyzerRepo, + IReadOnlyDictionary customCommandOutputSchemas) { - if(experimentTemplate.AnalyzerId is null) - return new ValidationResult(true); - + if(!experimentTemplate.HasAnalyzerId || string.IsNullOrWhiteSpace(experimentTemplate.AnalyzerId)) + return new ValidationResult(true); + var analyzer = analyzerRepo.GetAnalyzerById(experimentTemplate.AnalyzerId); if(analyzer is null) return new ValidationResult(false, $"Unable to find analyzer with id of {experimentTemplate.AnalyzerId}"); - if(analyzer.AnalyzerState != State.Active) - { - return new ValidationResult(false, $"Unable to use analyzer {analyzer.Name} as it is is not currently active.\n{analyzer.StateMessage}"); + if(analyzer.AnalyzerState != State.Active) + { + return new ValidationResult(false, $"Unable to use analyzer {analyzer.Name} as it is is not currently active.\n{analyzer.StateMessage}"); + } + + var outputCommands = experimentTemplate.GetAllOutputCommands(); + + if(startupTemplate is not null) + { + outputCommands = outputCommands.Concat(startupTemplate.GetAllOutputCommands()).ToArray(); } - var outputCommands = experimentTemplate.GetAllOutputCommands(); - - if(startupTemplate is not null) - { - outputCommands = outputCommands.Concat(startupTemplate.GetAllOutputCommands()).ToArray(); - } - var analysisParameterSchema = await analyzer.GetParameters(); - var requiredAnalysisInputs = analysisParameterSchema.Fields.Where(input => !input.Value.Optional).ToArray(); - if(!outputCommands.Any()) + var requiredAnalysisInputs = analysisParameterSchema.Fields.Where(input => !input.Value.Optional).ToArray(); + if(!outputCommands.Any()) { if(!requiredAnalysisInputs.Any()) return new ValidationResult(true); @@ -41,52 +49,120 @@ public static async Task Validate(ExperimentTemplate experimen } var inputSchema = new AresStructSchema(); + var mappingErrors = new List(); + var customSchemas = ToCaseInsensitiveLookup(customCommandOutputSchemas); foreach(var map in experimentTemplate.AnalyzerMaps) { - var splitVarName = map.Value.Split('.'); - if(!splitVarName.Any()) - continue; - - var matchingCommand = outputCommands.FirstOrDefault(cmd => cmd.HasOutputVarName && cmd.OutputVarName.Equals(splitVarName.First())); - - if(matchingCommand is null) - continue; - - var cmdSchema = matchingCommand.Metadata.OutputMetadata?.DataSchema; + var splitVarName = map.Value.Split('.', StringSplitOptions.RemoveEmptyEntries); + if(splitVarName.Length == 0) + { + mappingErrors.Add($"Analyzer input '{map.Key}' does not reference a command output."); + continue; + } + + var matchingCommand = outputCommands.FirstOrDefault(cmd => cmd.HasOutputVarName && cmd.OutputVarName.Equals(splitVarName.First())); + + if(matchingCommand is null) + { + mappingErrors.Add($"Analyzer input '{map.Key}' references '{map.Value}', but output variable '{splitVarName[0]}' is unavailable."); + continue; + } + + var cmdSchema = ResolveOutputSchema(matchingCommand, customSchemas, out var schemaError); if(cmdSchema is null) + { + mappingErrors.Add($"Analyzer input '{map.Key}' references '{map.Value}', but {schemaError}"); continue; + } var matchingSchema = FindNestedSchema(cmdSchema, splitVarName.Skip(1).ToArray()); - if(matchingSchema is not null) - inputSchema.Fields[map.Key] = matchingSchema.Clone(); + if(matchingSchema is null) + { + mappingErrors.Add($"Analyzer input '{map.Key}' references '{map.Value}', but that output member is unavailable."); + continue; + } + if(matchingSchema.Type is AresDataType.Unit or AresDataType.UnspecifiedType) + { + mappingErrors.Add($"Analyzer input '{map.Key}' references '{map.Value}', but that output member does not have a usable schema."); + continue; + } + + inputSchema.Fields[map.Key] = matchingSchema.Clone(); } - var result = await analyzer.ValidateInputs(inputSchema); - var validationResult = new ValidationResult(result.Success, result.Messages); + var result = await analyzer.ValidateInputs(inputSchema); + var messages = result.Success && mappingErrors.Count > 0 + ? mappingErrors + : mappingErrors.Concat(result.Messages); + var validationResult = new ValidationResult( + result.Success && mappingErrors.Count == 0, + messages); return validationResult; - } - - private static AresValueSchema? FindNestedSchema(AresValueSchema schema, string[] keys) + } + + private static AresValueSchema? ResolveOutputSchema( + CommandTemplate command, + IReadOnlyDictionary customCommandOutputSchemas, + out string error) { - if(keys.Length == 0) - return schema; + AresValueSchema? schema; + switch(command.CommandTypeCase) + { + case CommandTemplate.CommandTypeOneofCase.DeviceCommand: + schema = command.DeviceCommand?.Metadata?.OutputMetadata?.DataSchema; + error = "the device command does not have a usable output schema."; + break; + + case CommandTemplate.CommandTypeOneofCase.SystemCommand: + schema = SystemOperationCatalog.Find(command.SystemCommand.Operation)?.OutputSchema; + error = "the system command does not have a usable output schema."; + break; - if(schema.Type != AresDataType.Struct || schema.StructSchema is null) - return null; + case CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation: + var customCommandId = command.CustomCommandInvocation.CustomCommandId; + schema = customCommandOutputSchemas.TryGetValue(customCommandId, out var customSchema) ? customSchema : null; + error = $"custom command '{customCommandId}' does not have an available current output schema."; + break; - var key = keys[0]; - if(!schema.StructSchema.Fields.TryGetValue(key, out var nestedSchema)) - return null; + case CommandTemplate.CommandTypeOneofCase.None: + schema = null; + error = "the command does not have a command type or usable output schema."; + break; - return FindNestedSchema(nestedSchema, keys.Skip(1).ToArray()); + default: + throw new ArgumentOutOfRangeException(nameof(command.CommandTypeCase), command.CommandTypeCase, null); + } + + return schema?.Type is AresDataType.Unit or AresDataType.UnspecifiedType ? null : schema; } - public static async Task Validate(IEnumerable experimentTemplates, ExperimentTemplate startupTemplate, IAnalyzerRepo analyzerManager) + private static IReadOnlyDictionary ToCaseInsensitiveLookup( + IReadOnlyDictionary schemas) { - var validationTasks = experimentTemplates.Select(template => Validate(template, startupTemplate, analyzerManager)).ToArray(); - var validations = await Task.WhenAll(validationTasks); - return new ValidationResult(validations); + var lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach(var schema in schemas) + lookup.TryAdd(schema.Key, schema.Value); + return lookup; + } + + private static AresValueSchema? FindNestedSchema(AresValueSchema schema, string[] keys) + { + while(true) + { + if(keys.Length == 0) + return schema; + + if(schema.Type != AresDataType.Struct || schema.StructSchema is null) + return null; + + var key = keys[0]; + if(!schema.StructSchema.Fields.TryGetValue(key, out var nestedSchema)) + return null; + + schema = nestedSchema; + keys = keys.Skip(1).ToArray(); + } } } diff --git a/AresOS.slnx b/AresOS.slnx index 1200f484..6fe27cf5 100644 --- a/AresOS.slnx +++ b/AresOS.slnx @@ -14,4 +14,5 @@ + diff --git a/AresScript.Tests/AresScriptBuilderTests.cs b/AresScript.Tests/AresScriptBuilderTests.cs index ccb3c8b7..8c76de83 100644 --- a/AresScript.Tests/AresScriptBuilderTests.cs +++ b/AresScript.Tests/AresScriptBuilderTests.cs @@ -274,6 +274,95 @@ public void ReplaceFunction_WhenFunctionDoesNotExist_ReturnsFalse() Assert.That(builder.Build(), Is.Empty); } + [Test] + public void CustomCommandScriptBuilder_BuildFunctionName_SanitizesCommandName() + { + var functionName = CustomCommandScriptBuilder.BuildFunctionName(" 123 Measure Temperature!! "); + + Assert.That(functionName, Is.EqualTo("custom_command_123_Measure_Temperature")); + } + + [Test] + public void CustomCommandScriptBuilder_BuildFunctionSignature_WritesParameterAndReturnTypeHints() + { + var outputSchema = AresSchemaBuilder.Entry(AresDataType.Quantity) + .WithQuantityRange(QuantityType.Temperature, "degC", minScalarValue: 0, maxScalarValue: 100) + .Build(); + + var signature = CustomCommandScriptBuilder.BuildFunctionSignature( + "Measure Temperature", + [ + new AresScriptParameter("sample_id", AresDataType.String), + new AresScriptParameter("timeout_seconds", AresDataType.Number) + ], + outputSchema); + + Assert.That(signature, Is.EqualTo("def custom_command_Measure_Temperature(sample_id: String, timeout_seconds: Number) -> Quantity.Temperature[unit=\"degC\", min=0, max=100]")); + } + + [Test] + public void CustomCommandScriptBuilder_BuildWrappedScript_WritesNestedSchemaTypeHints() + { + var structSchema = AresSchemaBuilder.Entry(AresDataType.Struct).Build(); + structSchema.StructSchema = new AresStructSchema(); + structSchema.StructSchema.Fields["reading"] = AresSchemaBuilder.Entry(AresDataType.Number).Build(); + structSchema.StructSchema.Fields["unit"] = AresSchemaBuilder.Entry(AresDataType.String).Build(); + + var listSchema = AresSchemaBuilder.Entry(AresDataType.List).Build(); + listSchema.ListElementSchema = AresSchemaBuilder.Entry(AresDataType.String).Build(); + + var script = CustomCommandScriptBuilder.BuildWrappedScript( + "Summarize Samples", + [ + new AresScriptParameter("measurement", structSchema), + new AresScriptParameter("tags", listSchema) + ], + structSchema, + """ + total = measurement.reading + + return measurement + """); + + Assert.That(script, Does.Contain("def custom_command_Summarize_Samples(measurement: {reading: Number, unit: String}, tags: [String]) -> {reading: Number, unit: String}:")); + Assert.That(script, Does.Contain(" total = measurement.reading")); + Assert.That(script, Does.Contain($"{System.Environment.NewLine} {System.Environment.NewLine} return measurement")); + Assert.That(() => Parse(script), Throws.Nothing); + } + + [Test] + public void CustomCommandScriptBuilder_BuildWrappedScript_PreservesWhitespaceBeforeReturnFallback() + { + var script = CustomCommandScriptBuilder.BuildWrappedScript( + "No Op", + [], + AresSchemaBuilder.Entry(AresDataType.Unit).Build(), + " "); + + Assert.That( + script, + Is.EqualTo( + $"def custom_command_No_Op() -> Unit:{System.Environment.NewLine} {System.Environment.NewLine} return")); + Assert.That(() => Parse(script), Throws.Nothing); + } + + [Test] + public void CustomCommandScriptBuilder_BuildWrappedScript_PreservesEveryBodyLine() + { + var script = CustomCommandScriptBuilder.BuildWrappedScript( + "Position Test", + [], + AresSchemaBuilder.Entry(AresDataType.Unit).Build(), + "\r\nsleep()\r\n\r\n"); + + var newline = System.Environment.NewLine; + Assert.That( + script, + Is.EqualTo( + $"def custom_command_Position_Test() -> Unit:{newline} {newline} sleep(){newline} {newline} {newline}")); + Assert.That(() => Parse(script), Throws.Nothing); + } + private static void Parse(string script) { var input = new AntlrInputStream(script); diff --git a/AresScript.Tests/Program.cs b/AresScript.Tests/Program.cs index b0e984e7..72e84c0c 100644 --- a/AresScript.Tests/Program.cs +++ b/AresScript.Tests/Program.cs @@ -393,7 +393,7 @@ public void SemanticTokens_ClassifyFunctionCalls() Assert.That(tokens, Has.Exactly(1).Matches(t => t.Type == ScriptSemanticTokenType.Function && t.Line == 0 - && t.StartColumn == 1 + && t.StartColumn == 0 && t.Length == 3)); } @@ -409,20 +409,32 @@ public void SemanticTokens_ClassifyAssignedIdentifiersAsVariables() Assert.That(tokens, Has.Some.Matches(t => t.Type == ScriptSemanticTokenType.Variable && t.Line == 0 - && t.StartColumn == 1 + && t.StartColumn == 0 && t.Length == 5)); Assert.That(tokens, Has.Some.Matches(t => t.Type == ScriptSemanticTokenType.Function && t.Line == 0 - && t.StartColumn == 9 + && t.StartColumn == 8 && t.Length == 3)); Assert.That(tokens, Has.Some.Matches(t => t.Type == ScriptSemanticTokenType.Variable && t.Line == 0 - && t.StartColumn == 13 + && t.StartColumn == 12 && t.Length == 3)); } + [Test] + public void SemanticTokens_ReturnsEmptyTokens_ForIncompleteMemberAccess() + { + var script = """ + def custom_command_Test(value: Quantity.Temperature) -> Quantity.Temperature: + return Quantity. + """; + + Assert.That(() => BuildSemanticTokens(script), Throws.Nothing); + Assert.That(BuildSemanticTokens(script), Is.Empty); + } + [Test] public async Task Function_TypeHints_Are_Parsed_For_Parameters_And_Returns() { @@ -1430,6 +1442,33 @@ def meme(): Assert.That(steps.Select(s => s.FunctionId), Is.EqualTo(["sleep", "main", "sleep", "meme", "print"])); } + [Test] + public async Task Summary_Allows_Comment_As_First_Function_Body_Line() + { + var script = """ + def main(): + # test + print("ready") + + main() + """; + + var steps = await BuildScriptSummaryAsync(script, includeUserFunctions: true); + Assert.That(steps.Select(s => s.FunctionId), Is.EqualTo(["main"])); + } + + [Test] + public async Task Summary_Allows_Comment_Only_Function_Body() + { + var script = """ + def main(): + # test + """; + + var steps = await BuildScriptSummaryAsync(script, includeUserFunctions: true); + Assert.That(steps, Is.Empty); + } + [Test] public Task Cancellation_Stops_Interpreter() { diff --git a/AresScript/AresIndentationLexer.cs b/AresScript/AresIndentationLexer.cs index 1cac82d7..7117609e 100644 --- a/AresScript/AresIndentationLexer.cs +++ b/AresScript/AresIndentationLexer.cs @@ -20,6 +20,7 @@ public class AresIndentationLexer : AresLangLexer // Keep track of the previous token to know if we just saw a newline private IToken? _lastToken = null; + private IToken? _lastParserVisibleToken = null; public AresIndentationLexer(ICharStream input) : base(input) { @@ -29,7 +30,7 @@ public AresIndentationLexer(ICharStream input) : base(input) public override IToken NextToken() { // 1. If we have queued tokens (INDENTs/DEDENTs), return them first - if (_pendingTokens.Count > 0) + if(_pendingTokens.Count > 0) { var next = _pendingTokens.Dequeue(); _lastToken = next; @@ -42,30 +43,36 @@ public override IToken NextToken() // 3. Check indentation if we are at the start of a line // (i.e., if the previous token was a NEWLINE or we are at the start of the file) // We ignore EOF and NEWLINE itself (empty lines don't change indentation) - if ((_lastToken == null || _lastToken.Type == NEWLINE) && + if((_lastToken == null || _lastToken.Type == NEWLINE) && token.Type != Eof && token.Type != NEWLINE) { int currentIndent = token.Column; int previousIndent = _indents.Peek(); + var hiddenTokenCanStartIndentedBlock = + token.Channel != DefaultTokenChannel && + (_lastParserVisibleToken?.Type == COLON || _indents.Count > 1); - if (currentIndent > previousIndent) + if(currentIndent > previousIndent) { - // Indentation increased -> Emit INDENT - _indents.Push(currentIndent); - _pendingTokens.Enqueue(CreateToken(IndentToken, token)); + if(token.Channel == DefaultTokenChannel || hiddenTokenCanStartIndentedBlock) + { + // Indentation increased -> Emit INDENT + _indents.Push(currentIndent); + _pendingTokens.Enqueue(CreateToken(IndentToken, token)); + } } - else if (currentIndent < previousIndent) + else if(currentIndent < previousIndent) { // Indentation decreased -> Emit one or more DEDENTs - while (currentIndent < _indents.Peek()) + while(currentIndent < _indents.Peek()) { _indents.Pop(); _pendingTokens.Enqueue(CreateToken(DedentToken, token)); } // Safety check: ensure we landed on a valid previous indentation level - if (currentIndent != _indents.Peek()) + if(currentIndent != _indents.Peek()) { // You might want to throw a custom error here for "Unaligned dedent" } @@ -73,16 +80,16 @@ public override IToken NextToken() } // 4. Handle End Of File: Close any remaining open blocks - if (token.Type == Eof && _indents.Count > 1) + if(token.Type == Eof && _indents.Count > 1) { // This one's here to make it work when there's a top level block without a newline at the end // we just add a "fake" newline to make sure we can still run the block without syntax errors - if (_lastToken is not null && _lastToken.Type != NEWLINE) + if(_lastToken is not null && _lastToken.Type != NEWLINE) { _pendingTokens.Enqueue(CreateToken(NEWLINE, token)); } - while (_indents.Count > 1) + while(_indents.Count > 1) { _indents.Pop(); _pendingTokens.Enqueue(CreateToken(DedentToken, token)); @@ -94,7 +101,7 @@ public override IToken NextToken() } // 5. If we generated tokens, queue the real token and return the first generated one - if (_pendingTokens.Count > 0) + if(_pendingTokens.Count > 0) { _pendingTokens.Enqueue(token); var next = _pendingTokens.Dequeue(); @@ -103,6 +110,10 @@ public override IToken NextToken() } _lastToken = token; + if(token.Channel == DefaultTokenChannel && token.Type != NEWLINE && token.Type != Eof) + { + _lastParserVisibleToken = token; + } // Otherwise, just return the real token return token; diff --git a/AresScript/AresLang.g4 b/AresScript/AresLang.g4 index ab4ea3ab..4c252d2a 100644 --- a/AresScript/AresLang.g4 +++ b/AresScript/AresLang.g4 @@ -67,11 +67,11 @@ parallelStatement: PARALLEL COLON parallelBlock; // Simple block for non-loop statements (no loop depth change) -block: NEWLINE INDENT (statement NEWLINE*)+ DEDENT; +block: NEWLINE+ INDENT (statement | NEWLINE)* DEDENT; // Loop block increments/decrements loop depth for break/continue validation loopBlock: - NEWLINE INDENT {loopDepth++;} (statement NEWLINE*)+ DEDENT {loopDepth--;}; + NEWLINE+ INDENT {loopDepth++;} (statement | NEWLINE)* DEDENT {loopDepth--;}; // Function declarations. TODO: Decide if AresScript should even support custom functions functionDeclaration: @@ -115,11 +115,11 @@ listTypeHint: // Function body increments/decrements func depth for return validation funcBlock: - NEWLINE INDENT {funcDepth++;} (statement NEWLINE*)+ DEDENT {funcDepth--;}; + NEWLINE+ INDENT {funcDepth++;} (statement | NEWLINE)* DEDENT {funcDepth--;}; // Parallel block executes expression asynchronously. Let's not worry about statements for now parallelBlock: - NEWLINE INDENT (expression NEWLINE*)+ DEDENT; + NEWLINE+ INDENT (expression | NEWLINE)* DEDENT; // Assignment statements assignment: lvalue '=' expression; @@ -260,4 +260,4 @@ STRING: ('"' ( '\\' . | ~["\\\r\n])* '"') | ('\'' ( '\\' . | ~['\\\r\n])* '\''); // Comments: skip single line comments starting with # just like python :) -COMMENT: '#' ~[\r\n]* -> skip; +COMMENT: '#' ~[\r\n]* -> channel(HIDDEN); diff --git a/AresScript/AresScript.csproj b/AresScript/AresScript.csproj index 3d5a6c57..78bb6f91 100644 --- a/AresScript/AresScript.csproj +++ b/AresScript/AresScript.csproj @@ -9,7 +9,7 @@ - + diff --git a/AresScript/ScriptAnalysis/AresScriptAnalysis.SemanticTokens.cs b/AresScript/ScriptAnalysis/AresScriptAnalysis.SemanticTokens.cs index 0286b10f..67957101 100644 --- a/AresScript/ScriptAnalysis/AresScriptAnalysis.SemanticTokens.cs +++ b/AresScript/ScriptAnalysis/AresScriptAnalysis.SemanticTokens.cs @@ -74,7 +74,7 @@ private void AddToken(IToken token, ScriptSemanticTokenType type) Tokens.Add(new ScriptSemanticToken( token.Line - 1, - token.Column + 1, + token.Column, token.Text.Length, type)); } diff --git a/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs b/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs new file mode 100644 index 00000000..4dc971e0 --- /dev/null +++ b/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs @@ -0,0 +1,69 @@ +using Ares.Datamodel; +using AresScript.Symbols; +using System.Text; +using System.Text.RegularExpressions; + +namespace AresScript.ScriptBuilding; + +public static class CustomCommandScriptBuilder +{ + private const string FunctionPrefix = "custom_command_"; + private const string EmptyBodyFallback = "return"; + private static readonly Regex InvalidIdentifierCharacterRegex = new("[^a-zA-Z0-9_]+", RegexOptions.Compiled); + private static readonly Regex RepeatedUnderscoreRegex = new("_+", RegexOptions.Compiled); + + public static string BuildFunctionName(string commandName) + { + var safeName = string.IsNullOrWhiteSpace(commandName) ? "command" : commandName.Trim(); + safeName = InvalidIdentifierCharacterRegex.Replace(safeName, "_"); + safeName = RepeatedUnderscoreRegex.Replace(safeName, "_").Trim('_'); + + if(string.IsNullOrWhiteSpace(safeName)) + { + safeName = "command"; + } + + return $"{FunctionPrefix}{safeName}"; + } + + public static string BuildFunctionSignature( + string commandName, + IEnumerable parameters, + AresValueSchema? returnSchema = null) + { + ArgumentNullException.ThrowIfNull(parameters); + return ScriptBuildingHelpers.BuildFunctionSignature(BuildFunctionName(commandName), parameters, returnSchema); + } + + public static string BuildWrappedScript( + string commandName, + IEnumerable parameters, + AresValueSchema? returnSchema, + string? scriptBody) + { + var signature = BuildFunctionSignature(commandName, parameters, returnSchema); + var normalizedBody = NormalizeBody(scriptBody); + var output = new StringBuilder(); + output.Append(signature).AppendLine(":"); + + foreach(var line in normalizedBody.Split('\n')) + { + output.Append(" ").AppendLine(line); + } + + if(string.IsNullOrWhiteSpace(normalizedBody)) + { + output.Append(" ").Append(EmptyBodyFallback); + } + + return output.ToString(); + } + + private static string NormalizeBody(string? scriptBody) + { + return scriptBody? + .Replace("\r\n", "\n", StringComparison.Ordinal) + .Replace('\r', '\n') + ?? string.Empty; + } +} diff --git a/AresService.Migrations.Postgres/Migrations/20260626195922_CustomCommands_AresDbContext.Designer.cs b/AresService.Migrations.Postgres/Migrations/20260626195922_CustomCommands_AresDbContext.Designer.cs new file mode 100644 index 00000000..e2c16a90 --- /dev/null +++ b/AresService.Migrations.Postgres/Migrations/20260626195922_CustomCommands_AresDbContext.Designer.cs @@ -0,0 +1,2152 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AresService.Migrations.Postgres.Migrations.AresDb +{ + [DbContext(typeof(AresDbContext))] + [Migration("20260626195922_CustomCommands_AresDbContext")] + partial class CustomCommands_AresDbContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalyzerInfo") + .HasColumnType("jsonb"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentOverviewId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Result") + .HasColumnType("double precision"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentOverviewId") + .IsUnique(); + + b.ToTable("AnalysisOverview"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.Analysis", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalysisOutcome") + .HasColumnType("integer"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ErrorString") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Result") + .HasColumnType("real"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalyzerInfoId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("SettingsSchema") + .HasColumnType("text"); + + b.Property("TimeoutSeconds") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("AnalyzerInfoId") + .IsUnique(); + + b.ToTable("AnalyzerCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyzers", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalyzerId") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalysisRequest") + .HasColumnType("jsonb"); + + b.Property("AnalysisResponse") + .HasColumnType("jsonb"); + + b.Property("AnalyzerId") + .HasColumnType("text"); + + b.Property("AnalyzerName") + .HasColumnType("text"); + + b.Property("AnalyzerType") + .HasColumnType("text"); + + b.Property("AnalyzerVersion") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("TimeRequestSent") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeResponseReceived") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresCampaignTag", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("TagName") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("CampaignTags"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresGeneralSettingsConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommandLatency") + .HasColumnType("jsonb"); + + b.Property("CommandRetryLimit") + .HasColumnType("integer"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentRetryLimit") + .HasColumnType("integer"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("RetryCooldown") + .HasColumnType("jsonb"); + + b.HasKey("UniqueId"); + + b.ToTable("AresGeneralSettingsConfig", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OutputSchema") + .HasColumnType("text"); + + b.Property("ScriptBody") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("CustomCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("CustomCommandId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Schema") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandId"); + + b.ToTable("CustomCommandParameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("text"); + + b.Property("CampaignName") + .HasColumnType("text"); + + b.Property("CampaignNotes") + .HasColumnType("text"); + + b.Property("CampaignTags") + .HasColumnType("text"); + + b.Property("CloseoutExecutionSummaryUniqueId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("StartupExecutionSummaryUniqueId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("CloseoutExecutionSummaryUniqueId"); + + b.HasIndex("StartupExecutionSummaryUniqueId"); + + b.ToTable("CampaignExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommandId") + .HasColumnType("text"); + + b.Property("CommandName") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DeviceName") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Result") + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatusMessage") + .HasColumnType("text"); + + b.Property("StepExecutionStatusUniqueId") + .HasColumnType("uuid"); + + b.Property("VariableName") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionStatusUniqueId"); + + b.ToTable("CommandExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommandDescription") + .HasColumnType("text"); + + b.Property("CommandId") + .HasColumnType("text"); + + b.Property("CommandName") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("StepExecutionSummaryUniqueId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("text"); + + b.Property("VarName") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionSummaryUniqueId"); + + b.ToTable("CommandExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AwaitUserInput") + .HasColumnType("boolean"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Result") + .HasColumnType("text"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique(); + + b.ToTable("DeviceCommandResults", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceInfoId") + .HasColumnType("uuid"); + + b.Property("InputSchema") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OutputSchema") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("DeviceInfoId"); + + b.ToTable("DeviceCommandDescriptor"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeviceName") + .HasColumnType("text"); + + b.Property("DeviceSettings") + .HasColumnType("text"); + + b.Property("DriverId") + .HasColumnType("text"); + + b.Property("IsSimulated") + .HasColumnType("boolean"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("SerialInfoUniqueId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("SerialInfoUniqueId"); + + b.ToTable("DeviceConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("SettingsSchema") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceLoggingSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Deltas") + .HasColumnType("text"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("IntervalMs") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LoggingEnabled") + .HasColumnType("boolean"); + + b.Property("LoggingType") + .HasColumnType("integer"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceLoggingSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceState", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceStates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.RemoteDeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("RemoteDevices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.SerialConnection", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BaudRate") + .HasColumnType("integer"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("PortName") + .HasColumnType("text"); + + b.Property("SerialId") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("SerialConnection"); + }); + + modelBuilder.Entity("Ares.Datamodel.DeviceErrorHandlingConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .HasColumnType("integer"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Handling") + .HasColumnType("integer"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.HasKey("UniqueId"); + + b.ToTable("ErrorHandlingConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignExecutionSummaryId") + .HasColumnType("uuid"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentResultId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LocaltimeOffset") + .HasColumnType("text"); + + b.Property("StepExecutionSummaryId") + .HasColumnType("uuid"); + + b.Property("TimeFinished") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeStarted") + .HasColumnType("timestamp with time zone"); + + b.Property("Timezone") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryId") + .IsUnique(); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique(); + + b.HasIndex("ExperimentResultId") + .IsUnique(); + + b.HasIndex("StepExecutionSummaryId") + .IsUnique(); + + b.ToTable("ExecutionInfos", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignExecutionSummaryUniqueId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentId") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ResultOutputPath") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryUniqueId"); + + b.ToTable("ExperimentExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentResultId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Result") + .HasColumnType("text"); + + b.Property("TemplateUniqueId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentResultId") + .IsUnique(); + + b.HasIndex("TemplateUniqueId"); + + b.ToTable("ExperimentOverviews", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Maximum") + .HasColumnType("double precision"); + + b.Property("Minimum") + .HasColumnType("double precision"); + + b.Property("ParameterMetadataUniqueId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("ParameterMetadataUniqueId"); + + b.ToTable("Limits"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("PlannerName") + .HasColumnType("text"); + + b.Property("PlannerServiceCapabilitiesUniqueId") + .HasColumnType("uuid"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerServiceCapabilitiesUniqueId"); + + b.ToTable("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ParameterUniqueId") + .HasColumnType("uuid"); + + b.Property("PlannerId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("ParameterUniqueId"); + + b.HasIndex("PlannerId"); + + b.ToTable("PlannerAllocations", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerServices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("PlannerInfoId") + .HasColumnType("uuid"); + + b.Property("ServiceName") + .HasColumnType("text"); + + b.Property("SettingsSchema") + .HasColumnType("text"); + + b.Property("TimeoutSeconds") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerInfoId") + .IsUnique(); + + b.ToTable("PlannerServiceCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("PlannerId") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("PlannerId") + .HasColumnType("text"); + + b.Property("PlannerName") + .HasColumnType("text"); + + b.Property("PlannerType") + .HasColumnType("text"); + + b.Property("PlannerVersion") + .HasColumnType("text"); + + b.Property("PlanningRequest") + .HasColumnType("jsonb"); + + b.Property("PlanningResponse") + .HasColumnType("jsonb"); + + b.Property("TimeRequestSent") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeResponseReceived") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.Project", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("StepId") + .HasColumnType("text"); + + b.Property("StepName") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("StepExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentExecutionSummaryUniqueId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("StepId") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentExecutionSummaryUniqueId"); + + b.ToTable("StepExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("CampaignTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommandTemplateId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeviceType") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateId") + .IsUnique(); + + b.ToTable("CommandMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("OutputVarName") + .HasColumnType("text"); + + b.Property("StepTemplateUniqueId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepTemplateUniqueId"); + + b.ToTable("CommandTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalyzerId") + .HasColumnType("text"); + + b.Property("CampaignCloseoutId") + .HasColumnType("uuid"); + + b.Property("CampaignExperimentId") + .HasColumnType("uuid"); + + b.Property("CampaignStartupId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignCloseoutId") + .IsUnique(); + + b.HasIndex("CampaignExperimentId") + .IsUnique(); + + b.HasIndex("CampaignStartupId") + .IsUnique(); + + b.ToTable("ExperimentTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DataSchema") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.HasKey("UniqueId"); + + b.ToTable("OutputMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommandTemplateUniqueId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentOverviewUniqueId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("SourceJson") + .HasColumnType("jsonb") + .HasColumnName("Source"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateUniqueId"); + + b.HasIndex("ExperimentOverviewUniqueId"); + + b.ToTable("Parameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("uuid"); + + b.Property("CommandMetadataUniqueId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("InitialValue") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NotPlannable") + .HasColumnType("boolean"); + + b.Property("OutputName") + .HasColumnType("text"); + + b.Property("ParameterId") + .HasColumnType("uuid"); + + b.Property("PlannerDescription") + .HasColumnType("text"); + + b.Property("PlannerName") + .HasColumnType("text"); + + b.Property("Schema") + .HasColumnType("text"); + + b.Property("Unit") + .HasColumnType("text"); + + b.Property("UseDefault") + .HasColumnType("boolean"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("CommandMetadataUniqueId"); + + b.HasIndex("ParameterId") + .IsUnique(); + + b.ToTable("ParameterMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentTemplateUniqueId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("IsParallel") + .HasColumnType("boolean"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentTemplateUniqueId"); + + b.ToTable("StepTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.DeviceVisualizationConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChartTitle") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("GridH") + .HasColumnType("integer"); + + b.Property("GridW") + .HasColumnType("integer"); + + b.Property("GridX") + .HasColumnType("integer"); + + b.Property("GridY") + .HasColumnType("integer"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("NumberDisplayPoints") + .HasColumnType("integer"); + + b.Property("Paths") + .HasColumnType("jsonb"); + + b.Property("PollingRate") + .HasColumnType("integer"); + + b.Property("ShowDataLabels") + .HasColumnType("boolean"); + + b.Property("ShowMarkers") + .HasColumnType("boolean"); + + b.Property("Style") + .HasColumnType("integer"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceVisualizationConfigs"); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.VisualizationPath", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssociatedDeviceId") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DataType") + .HasColumnType("integer"); + + b.Property("IsPlottable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Path") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("VisualizationPath"); + }); + + modelBuilder.Entity("Ares.Services.DriverInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DriverId") + .HasColumnType("text"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceDrivers"); + }); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithOne("AnalysisOverview") + .HasForeignKey("Ares.Datamodel.AnalysisOverview", "ExperimentOverviewId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.HasOne("Ares.Datamodel.Analyzing.AnalyzerInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Analyzing.AnalyzerCapabilities", "AnalyzerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + .WithMany("InputParameters") + .HasForeignKey("CustomCommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "CloseoutExecutionSummary") + .WithMany() + .HasForeignKey("CloseoutExecutionSummaryUniqueId"); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "StartupExecutionSummary") + .WithMany() + .HasForeignKey("StartupExecutionSummaryUniqueId"); + + b.Navigation("CloseoutExecutionSummary"); + + b.Navigation("StartupExecutionSummary"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.HasOne("Ares.Datamodel.StepExecutionStatus", null) + .WithMany("CommandExecutionStatuses") + .HasForeignKey("StepExecutionStatusUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithMany("CommandSummaries") + .HasForeignKey("StepExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("Result") + .HasForeignKey("Ares.Datamodel.CommandResult", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.HasOne("Ares.Datamodel.Device.DeviceInfo", null) + .WithMany("Commands") + .HasForeignKey("DeviceInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.HasOne("Ares.Datamodel.Device.SerialConnection", "SerialInfo") + .WithMany() + .HasForeignKey("SerialInfoUniqueId"); + + b.Navigation("SerialInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CampaignExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "ExperimentResultId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "StepExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithMany("ExperimentSummaries") + .HasForeignKey("CampaignExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExperimentOverview") + .HasForeignKey("Ares.Datamodel.ExperimentOverview", "ExperimentResultId"); + + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", "Template") + .WithMany() + .HasForeignKey("TemplateUniqueId"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", null) + .WithMany("Constraints") + .HasForeignKey("ParameterMetadataUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceCapabilities", null) + .WithMany("AvailablePlanners") + .HasForeignKey("PlannerServiceCapabilitiesUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannerAllocations") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", "Parameter") + .WithMany() + .HasForeignKey("ParameterUniqueId"); + + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", "Planner") + .WithMany() + .HasForeignKey("PlannerId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.Navigation("Parameter"); + + b.Navigation("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Planning.PlannerServiceCapabilities", "PlannerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithMany("StepSummaries") + .HasForeignKey("ExperimentExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "CommandTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.StepTemplate", null) + .WithMany("CommandTemplates") + .HasForeignKey("StepTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("CloseoutTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignCloseoutId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("ExperimentTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignExperimentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("StartupTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignStartupId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithOne("OutputMetadata") + .HasForeignKey("Ares.Datamodel.Templates.OutputMetadata", "UniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithMany("Parameters") + .HasForeignKey("CommandTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithMany("Parameters") + .HasForeignKey("ExperimentOverviewUniqueId"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannableParameters") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithMany("ParameterMetadatas") + .HasForeignKey("CommandMetadataUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.Parameter", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.ParameterMetadata", "ParameterId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", null) + .WithMany("StepTemplates") + .HasForeignKey("ExperimentTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Navigation("InputParameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("Result"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Navigation("Commands"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentOverview"); + + b.Navigation("StepSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Navigation("AnalysisOverview"); + + b.Navigation("Parameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Navigation("AvailablePlanners"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Navigation("CommandExecutionStatuses"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Navigation("CommandSummaries"); + + b.Navigation("ExecutionInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Navigation("CloseoutTemplate"); + + b.Navigation("ExperimentTemplate"); + + b.Navigation("PlannableParameters"); + + b.Navigation("PlannerAllocations"); + + b.Navigation("StartupTemplate"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Navigation("OutputMetadata"); + + b.Navigation("ParameterMetadatas"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Navigation("Metadata"); + + b.Navigation("Parameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Navigation("StepTemplates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Navigation("Constraints"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Navigation("CommandTemplates"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.Postgres/Migrations/20260626195922_CustomCommands_AresDbContext.cs b/AresService.Migrations.Postgres/Migrations/20260626195922_CustomCommands_AresDbContext.cs new file mode 100644 index 00000000..301f4043 --- /dev/null +++ b/AresService.Migrations.Postgres/Migrations/20260626195922_CustomCommands_AresDbContext.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.Postgres.Migrations.AresDb +{ + /// + public partial class CustomCommands_AresDbContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CustomCommands", + columns: table => new + { + UniqueId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: true), + Description = table.Column(type: "text", nullable: true), + OutputSchema = table.Column(type: "text", nullable: true), + ScriptBody = table.Column(type: "text", nullable: true), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"), + LastModified = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()") + }, + constraints: table => + { + table.PrimaryKey("PK_CustomCommands", x => x.UniqueId); + }); + + migrationBuilder.CreateTable( + name: "CustomCommandParameters", + columns: table => new + { + UniqueId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: true), + Schema = table.Column(type: "text", nullable: true), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"), + CustomCommandId = table.Column(type: "uuid", nullable: false), + LastModified = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()") + }, + constraints: table => + { + table.PrimaryKey("PK_CustomCommandParameters", x => x.UniqueId); + table.ForeignKey( + name: "FK_CustomCommandParameters_CustomCommands_CustomCommandId", + column: x => x.CustomCommandId, + principalTable: "CustomCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CustomCommandParameters_CustomCommandId", + table: "CustomCommandParameters", + column: "CustomCommandId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CustomCommandParameters"); + + migrationBuilder.DropTable( + name: "CustomCommands"); + } + } +} diff --git a/AresService.Migrations.Postgres/Migrations/20260626195925_CustomCommands_AresIdentityContext.Designer.cs b/AresService.Migrations.Postgres/Migrations/20260626195925_CustomCommands_AresIdentityContext.Designer.cs new file mode 100644 index 00000000..150edff3 --- /dev/null +++ b/AresService.Migrations.Postgres/Migrations/20260626195925_CustomCommands_AresIdentityContext.Designer.cs @@ -0,0 +1,277 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AresService.Migrations.Postgres.Migrations.AresIdentity +{ + [DbContext(typeof(AresIdentityContext))] + [Migration("20260626195925_CustomCommands_AresIdentityContext")] + partial class CustomCommands_AresIdentityContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AresService.Data.AresUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.Postgres/Migrations/20260626195925_CustomCommands_AresIdentityContext.cs b/AresService.Migrations.Postgres/Migrations/20260626195925_CustomCommands_AresIdentityContext.cs new file mode 100644 index 00000000..6608e3cd --- /dev/null +++ b/AresService.Migrations.Postgres/Migrations/20260626195925_CustomCommands_AresIdentityContext.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.Postgres.Migrations.AresIdentity +{ + /// + public partial class CustomCommands_AresIdentityContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/AresService.Migrations.Postgres/Migrations/20260710160725_AddCustomCommands_AresDbContext.Designer.cs b/AresService.Migrations.Postgres/Migrations/20260710160725_AddCustomCommands_AresDbContext.Designer.cs new file mode 100644 index 00000000..89fdca45 --- /dev/null +++ b/AresService.Migrations.Postgres/Migrations/20260710160725_AddCustomCommands_AresDbContext.Designer.cs @@ -0,0 +1,2269 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AresService.Migrations.Postgres.Migrations.AresDb +{ + [DbContext(typeof(AresDbContext))] + [Migration("20260710160725_AddCustomCommands_AresDbContext")] + partial class AddCustomCommands_AresDbContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalyzerInfo") + .HasColumnType("jsonb"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentOverviewId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Result") + .HasColumnType("double precision"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentOverviewId") + .IsUnique(); + + b.ToTable("AnalysisOverview"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.Analysis", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalysisOutcome") + .HasColumnType("integer"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ErrorString") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Result") + .HasColumnType("real"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalyzerInfoId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("SettingsSchema") + .HasColumnType("text"); + + b.Property("TimeoutSeconds") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("AnalyzerInfoId") + .IsUnique(); + + b.ToTable("AnalyzerCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyzers", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalyzerId") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalysisRequest") + .HasColumnType("jsonb"); + + b.Property("AnalysisResponse") + .HasColumnType("jsonb"); + + b.Property("AnalyzerId") + .HasColumnType("text"); + + b.Property("AnalyzerName") + .HasColumnType("text"); + + b.Property("AnalyzerType") + .HasColumnType("text"); + + b.Property("AnalyzerVersion") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("TimeRequestSent") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeResponseReceived") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresCampaignTag", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("TagName") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("CampaignTags"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresGeneralSettingsConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommandLatency") + .HasColumnType("jsonb"); + + b.Property("CommandRetryLimit") + .HasColumnType("integer"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentRetryLimit") + .HasColumnType("integer"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("RetryCooldown") + .HasColumnType("jsonb"); + + b.HasKey("UniqueId"); + + b.ToTable("AresGeneralSettingsConfig", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("CurrentVersionId") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.HasKey("UniqueId"); + + b.ToTable("CustomCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("CustomCommandVersionId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Schema") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandVersionId"); + + b.ToTable("CustomCommandParameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("CustomCommandId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OutputSchema") + .HasColumnType("text"); + + b.Property("ScriptBody") + .HasColumnType("text"); + + b.Property("VersionNumber") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandId", "VersionNumber") + .IsUnique(); + + b.ToTable("CustomCommandVersions", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("text"); + + b.Property("CampaignName") + .HasColumnType("text"); + + b.Property("CampaignNotes") + .HasColumnType("text"); + + b.Property("CampaignTags") + .HasColumnType("text"); + + b.Property("CloseoutExecutionSummaryUniqueId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("StartupExecutionSummaryUniqueId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("CloseoutExecutionSummaryUniqueId"); + + b.HasIndex("StartupExecutionSummaryUniqueId"); + + b.ToTable("CampaignExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommandId") + .HasColumnType("text"); + + b.Property("CommandName") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DeviceName") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Result") + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatusMessage") + .HasColumnType("text"); + + b.Property("StepExecutionStatusUniqueId") + .HasColumnType("uuid"); + + b.Property("VariableName") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionStatusUniqueId"); + + b.ToTable("CommandExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommandDescription") + .HasColumnType("text"); + + b.Property("CommandId") + .HasColumnType("text"); + + b.Property("CommandName") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("StepExecutionSummaryUniqueId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("text"); + + b.Property("VarName") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionSummaryUniqueId"); + + b.ToTable("CommandExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AwaitUserInput") + .HasColumnType("boolean"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Result") + .HasColumnType("text"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique(); + + b.ToTable("DeviceCommandResults", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceInfoId") + .HasColumnType("uuid"); + + b.Property("InputSchema") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OutputSchema") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("DeviceInfoId"); + + b.ToTable("DeviceCommandDescriptor"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeviceName") + .HasColumnType("text"); + + b.Property("DeviceSettings") + .HasColumnType("text"); + + b.Property("DriverId") + .HasColumnType("text"); + + b.Property("IsSimulated") + .HasColumnType("boolean"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("SerialInfoUniqueId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("SerialInfoUniqueId"); + + b.ToTable("DeviceConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("SettingsSchema") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceLoggingSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Deltas") + .HasColumnType("text"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("IntervalMs") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LoggingEnabled") + .HasColumnType("boolean"); + + b.Property("LoggingType") + .HasColumnType("integer"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceLoggingSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceState", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceStates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.RemoteDeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("RemoteDevices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.SerialConnection", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BaudRate") + .HasColumnType("integer"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("PortName") + .HasColumnType("text"); + + b.Property("SerialId") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("SerialConnection"); + }); + + modelBuilder.Entity("Ares.Datamodel.DeviceErrorHandlingConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .HasColumnType("integer"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Handling") + .HasColumnType("integer"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.HasKey("UniqueId"); + + b.ToTable("ErrorHandlingConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignExecutionSummaryId") + .HasColumnType("uuid"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentResultId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LocaltimeOffset") + .HasColumnType("text"); + + b.Property("StepExecutionSummaryId") + .HasColumnType("uuid"); + + b.Property("TimeFinished") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeStarted") + .HasColumnType("timestamp with time zone"); + + b.Property("Timezone") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryId") + .IsUnique(); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique(); + + b.HasIndex("ExperimentResultId") + .IsUnique(); + + b.HasIndex("StepExecutionSummaryId") + .IsUnique(); + + b.ToTable("ExecutionInfos", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignExecutionSummaryUniqueId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentId") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ResultOutputPath") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryUniqueId"); + + b.ToTable("ExperimentExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentResultId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Result") + .HasColumnType("text"); + + b.Property("TemplateUniqueId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentResultId") + .IsUnique(); + + b.HasIndex("TemplateUniqueId"); + + b.ToTable("ExperimentOverviews", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Maximum") + .HasColumnType("double precision"); + + b.Property("Minimum") + .HasColumnType("double precision"); + + b.Property("ParameterMetadataUniqueId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("ParameterMetadataUniqueId"); + + b.ToTable("Limits"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("PlannerName") + .HasColumnType("text"); + + b.Property("PlannerServiceCapabilitiesUniqueId") + .HasColumnType("uuid"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerServiceCapabilitiesUniqueId"); + + b.ToTable("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ParameterUniqueId") + .HasColumnType("uuid"); + + b.Property("PlannerId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("ParameterUniqueId"); + + b.HasIndex("PlannerId"); + + b.ToTable("PlannerAllocations", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerServices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("PlannerInfoId") + .HasColumnType("uuid"); + + b.Property("ServiceName") + .HasColumnType("text"); + + b.Property("SettingsSchema") + .HasColumnType("text"); + + b.Property("TimeoutSeconds") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerInfoId") + .IsUnique(); + + b.ToTable("PlannerServiceCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("PlannerId") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("PlannerId") + .HasColumnType("text"); + + b.Property("PlannerName") + .HasColumnType("text"); + + b.Property("PlannerType") + .HasColumnType("text"); + + b.Property("PlannerVersion") + .HasColumnType("text"); + + b.Property("PlanningRequest") + .HasColumnType("jsonb"); + + b.Property("PlanningResponse") + .HasColumnType("jsonb"); + + b.Property("TimeRequestSent") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeResponseReceived") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.Project", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("StepId") + .HasColumnType("text"); + + b.Property("StepName") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("StepExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentExecutionSummaryUniqueId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("StepId") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentExecutionSummaryUniqueId"); + + b.ToTable("StepExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("CampaignTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DeviceCommandId") + .HasColumnType("uuid"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("DeviceType") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("DeviceCommandId") + .IsUnique(); + + b.ToTable("CommandMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("OutputVarName") + .HasColumnType("text"); + + b.Property("StepTemplateUniqueId") + .HasColumnType("uuid"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepTemplateUniqueId"); + + b.ToTable("CommandTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommandTemplateId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateId") + .IsUnique(); + + b.ToTable("DeviceCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnalyzerId") + .HasColumnType("text"); + + b.Property("CampaignCloseoutId") + .HasColumnType("uuid"); + + b.Property("CampaignExperimentId") + .HasColumnType("uuid"); + + b.Property("CampaignStartupId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Resolved") + .HasColumnType("boolean"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignCloseoutId") + .IsUnique(); + + b.HasIndex("CampaignExperimentId") + .IsUnique(); + + b.HasIndex("CampaignStartupId") + .IsUnique(); + + b.ToTable("ExperimentTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DataSchema") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.HasKey("UniqueId"); + + b.ToTable("OutputMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommandTemplateUniqueId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentOverviewUniqueId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("SourceJson") + .HasColumnType("jsonb") + .HasColumnName("Source"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateUniqueId"); + + b.HasIndex("ExperimentOverviewUniqueId"); + + b.ToTable("Parameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("uuid"); + + b.Property("CommandMetadataUniqueId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("InitialValue") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NotPlannable") + .HasColumnType("boolean"); + + b.Property("OutputName") + .HasColumnType("text"); + + b.Property("ParameterId") + .HasColumnType("uuid"); + + b.Property("PlannerDescription") + .HasColumnType("text"); + + b.Property("PlannerName") + .HasColumnType("text"); + + b.Property("Schema") + .HasColumnType("text"); + + b.Property("Unit") + .HasColumnType("text"); + + b.Property("UseDefault") + .HasColumnType("boolean"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("CommandMetadataUniqueId"); + + b.HasIndex("ParameterId") + .IsUnique(); + + b.ToTable("ParameterMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("ExperimentTemplateUniqueId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("IsParallel") + .HasColumnType("boolean"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentTemplateUniqueId"); + + b.ToTable("StepTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.DeviceVisualizationConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChartTitle") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("GridH") + .HasColumnType("integer"); + + b.Property("GridW") + .HasColumnType("integer"); + + b.Property("GridX") + .HasColumnType("integer"); + + b.Property("GridY") + .HasColumnType("integer"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("NumberDisplayPoints") + .HasColumnType("integer"); + + b.Property("Paths") + .HasColumnType("jsonb"); + + b.Property("PollingRate") + .HasColumnType("integer"); + + b.Property("ShowDataLabels") + .HasColumnType("boolean"); + + b.Property("ShowMarkers") + .HasColumnType("boolean"); + + b.Property("Style") + .HasColumnType("integer"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceVisualizationConfigs"); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.VisualizationPath", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssociatedDeviceId") + .HasColumnType("text"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DataType") + .HasColumnType("integer"); + + b.Property("IsPlottable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Path") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("VisualizationPath"); + }); + + modelBuilder.Entity("Ares.Services.DriverInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DriverId") + .HasColumnType("text"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceDrivers"); + }); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithOne("AnalysisOverview") + .HasForeignKey("Ares.Datamodel.AnalysisOverview", "ExperimentOverviewId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.HasOne("Ares.Datamodel.Analyzing.AnalyzerInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Analyzing.AnalyzerCapabilities", "AnalyzerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommandVersion", null) + .WithMany("InputParameters") + .HasForeignKey("CustomCommandVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + .WithMany() + .HasForeignKey("CustomCommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "CloseoutExecutionSummary") + .WithMany() + .HasForeignKey("CloseoutExecutionSummaryUniqueId"); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "StartupExecutionSummary") + .WithMany() + .HasForeignKey("StartupExecutionSummaryUniqueId"); + + b.Navigation("CloseoutExecutionSummary"); + + b.Navigation("StartupExecutionSummary"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.HasOne("Ares.Datamodel.StepExecutionStatus", null) + .WithMany("CommandExecutionStatuses") + .HasForeignKey("StepExecutionStatusUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithMany("CommandSummaries") + .HasForeignKey("StepExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("Result") + .HasForeignKey("Ares.Datamodel.CommandResult", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.HasOne("Ares.Datamodel.Device.DeviceInfo", null) + .WithMany("Commands") + .HasForeignKey("DeviceInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.HasOne("Ares.Datamodel.Device.SerialConnection", "SerialInfo") + .WithMany() + .HasForeignKey("SerialInfoUniqueId"); + + b.Navigation("SerialInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CampaignExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "ExperimentResultId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "StepExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithMany("ExperimentSummaries") + .HasForeignKey("CampaignExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExperimentOverview") + .HasForeignKey("Ares.Datamodel.ExperimentOverview", "ExperimentResultId"); + + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", "Template") + .WithMany() + .HasForeignKey("TemplateUniqueId"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", null) + .WithMany("Constraints") + .HasForeignKey("ParameterMetadataUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceCapabilities", null) + .WithMany("AvailablePlanners") + .HasForeignKey("PlannerServiceCapabilitiesUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannerAllocations") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", "Parameter") + .WithMany() + .HasForeignKey("ParameterUniqueId"); + + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", "Planner") + .WithMany() + .HasForeignKey("PlannerId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.Navigation("Parameter"); + + b.Navigation("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Planning.PlannerServiceCapabilities", "PlannerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithMany("StepSummaries") + .HasForeignKey("ExperimentExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.DeviceCommand", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "DeviceCommandId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.StepTemplate", null) + .WithMany("CommandTemplates") + .HasForeignKey("StepTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Ares.Datamodel.Templates.CustomCommandInvocation", "CustomCommandInvocation", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("uuid"); + + b1.Property("CustomCommandId") + .HasColumnType("text"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.OwnsOne("Ares.Datamodel.Templates.SystemCommand", "SystemCommand", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("uuid"); + + b1.Property("Operation") + .HasColumnType("integer"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.Navigation("CustomCommandInvocation"); + + b.Navigation("SystemCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithOne("DeviceCommand") + .HasForeignKey("Ares.Datamodel.Templates.DeviceCommand", "CommandTemplateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("CloseoutTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignCloseoutId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("ExperimentTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignExperimentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("StartupTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignStartupId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithOne("OutputMetadata") + .HasForeignKey("Ares.Datamodel.Templates.OutputMetadata", "UniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithMany("ArgumentBindings") + .HasForeignKey("CommandTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithMany("Parameters") + .HasForeignKey("ExperimentOverviewUniqueId"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannableParameters") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithMany("ParameterMetadatas") + .HasForeignKey("CommandMetadataUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.Parameter", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.ParameterMetadata", "ParameterId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", null) + .WithMany("StepTemplates") + .HasForeignKey("ExperimentTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Navigation("InputParameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("Result"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Navigation("Commands"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentOverview"); + + b.Navigation("StepSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Navigation("AnalysisOverview"); + + b.Navigation("Parameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Navigation("AvailablePlanners"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Navigation("CommandExecutionStatuses"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Navigation("CommandSummaries"); + + b.Navigation("ExecutionInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Navigation("CloseoutTemplate"); + + b.Navigation("ExperimentTemplate"); + + b.Navigation("PlannableParameters"); + + b.Navigation("PlannerAllocations"); + + b.Navigation("StartupTemplate"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Navigation("OutputMetadata"); + + b.Navigation("ParameterMetadatas"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Navigation("ArgumentBindings"); + + b.Navigation("DeviceCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Navigation("StepTemplates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Navigation("Constraints"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Navigation("CommandTemplates"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.Postgres/Migrations/20260710160725_AddCustomCommands_AresDbContext.cs b/AresService.Migrations.Postgres/Migrations/20260710160725_AddCustomCommands_AresDbContext.cs new file mode 100644 index 00000000..d28df837 --- /dev/null +++ b/AresService.Migrations.Postgres/Migrations/20260710160725_AddCustomCommands_AresDbContext.cs @@ -0,0 +1,277 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.Postgres.Migrations.AresDb +{ + /// + public partial class AddCustomCommands_AresDbContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_CommandMetadata_CommandTemplates_CommandTemplateId", + table: "CommandMetadata"); + + migrationBuilder.DropForeignKey( + name: "FK_CustomCommandParameters_CustomCommands_CustomCommandId", + table: "CustomCommandParameters"); + + migrationBuilder.DropIndex( + name: "IX_CommandMetadata_CommandTemplateId", + table: "CommandMetadata"); + + migrationBuilder.DropColumn( + name: "Description", + table: "CustomCommands"); + + migrationBuilder.DropColumn( + name: "Name", + table: "CustomCommands"); + + migrationBuilder.DropColumn( + name: "OutputSchema", + table: "CustomCommands"); + + migrationBuilder.RenameColumn( + name: "ScriptBody", + table: "CustomCommands", + newName: "CurrentVersionId"); + + migrationBuilder.RenameColumn( + name: "CustomCommandId", + table: "CustomCommandParameters", + newName: "CustomCommandVersionId"); + + migrationBuilder.RenameIndex( + name: "IX_CustomCommandParameters_CustomCommandId", + table: "CustomCommandParameters", + newName: "IX_CustomCommandParameters_CustomCommandVersionId"); + + migrationBuilder.AddColumn( + name: "CustomCommandInvocation_CustomCommandId", + table: "CommandTemplates", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SystemCommand_Operation", + table: "CommandTemplates", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "DeviceCommandId", + table: "CommandMetadata", + type: "uuid", + nullable: true); + + migrationBuilder.CreateTable( + name: "CustomCommandVersions", + columns: table => new + { + UniqueId = table.Column(type: "uuid", nullable: false), + CustomCommandId = table.Column(type: "uuid", nullable: false), + VersionNumber = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "text", nullable: true), + Description = table.Column(type: "text", nullable: true), + OutputSchema = table.Column(type: "text", nullable: true), + ScriptBody = table.Column(type: "text", nullable: true), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"), + LastModified = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()") + }, + constraints: table => + { + table.PrimaryKey("PK_CustomCommandVersions", x => x.UniqueId); + table.ForeignKey( + name: "FK_CustomCommandVersions_CustomCommands_CustomCommandId", + column: x => x.CustomCommandId, + principalTable: "CustomCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "DeviceCommands", + columns: table => new + { + UniqueId = table.Column(type: "uuid", nullable: false), + CommandTemplateId = table.Column(type: "uuid", nullable: true), + CreationTime = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()"), + LastModified = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "NOW()") + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceCommands", x => x.UniqueId); + table.ForeignKey( + name: "FK_DeviceCommands_CommandTemplates_CommandTemplateId", + column: x => x.CommandTemplateId, + principalTable: "CommandTemplates", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql( + """ + INSERT INTO "DeviceCommands" ("UniqueId", "CommandTemplateId") + SELECT "UniqueId", "UniqueId" + FROM "CommandTemplates"; + + UPDATE "CommandMetadata" + SET "DeviceCommandId" = "CommandTemplateId"; + """); + + migrationBuilder.DropColumn( + name: "CommandTemplateId", + table: "CommandMetadata"); + + migrationBuilder.CreateIndex( + name: "IX_CommandMetadata_DeviceCommandId", + table: "CommandMetadata", + column: "DeviceCommandId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CustomCommandVersions_CustomCommandId_VersionNumber", + table: "CustomCommandVersions", + columns: new[] { "CustomCommandId", "VersionNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DeviceCommands_CommandTemplateId", + table: "DeviceCommands", + column: "CommandTemplateId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_CommandMetadata_DeviceCommands_DeviceCommandId", + table: "CommandMetadata", + column: "DeviceCommandId", + principalTable: "DeviceCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_CustomCommandParameters_CustomCommandVersions_CustomCommand~", + table: "CustomCommandParameters", + column: "CustomCommandVersionId", + principalTable: "CustomCommandVersions", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_CommandMetadata_DeviceCommands_DeviceCommandId", + table: "CommandMetadata"); + + migrationBuilder.DropForeignKey( + name: "FK_CustomCommandParameters_CustomCommandVersions_CustomCommand~", + table: "CustomCommandParameters"); + + migrationBuilder.DropTable( + name: "CustomCommandVersions"); + + migrationBuilder.DropIndex( + name: "IX_CommandMetadata_DeviceCommandId", + table: "CommandMetadata"); + + migrationBuilder.DropColumn( + name: "CustomCommandInvocation_CustomCommandId", + table: "CommandTemplates"); + + migrationBuilder.DropColumn( + name: "SystemCommand_Operation", + table: "CommandTemplates"); + + migrationBuilder.RenameColumn( + name: "CurrentVersionId", + table: "CustomCommands", + newName: "ScriptBody"); + + migrationBuilder.RenameColumn( + name: "CustomCommandVersionId", + table: "CustomCommandParameters", + newName: "CustomCommandId"); + + migrationBuilder.RenameIndex( + name: "IX_CustomCommandParameters_CustomCommandVersionId", + table: "CustomCommandParameters", + newName: "IX_CustomCommandParameters_CustomCommandId"); + + migrationBuilder.AddColumn( + name: "Description", + table: "CustomCommands", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "Name", + table: "CustomCommands", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "OutputSchema", + table: "CustomCommands", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "CommandTemplateId", + table: "CommandMetadata", + type: "uuid", + nullable: true); + + migrationBuilder.Sql( + """ + UPDATE "CommandMetadata" AS metadata + SET "CommandTemplateId" = commands."CommandTemplateId" + FROM "DeviceCommands" AS commands + WHERE commands."UniqueId" = metadata."DeviceCommandId"; + """); + + migrationBuilder.DropColumn( + name: "DeviceCommandId", + table: "CommandMetadata"); + + migrationBuilder.DropTable( + name: "DeviceCommands"); + + migrationBuilder.AlterColumn( + name: "CommandTemplateId", + table: "CommandMetadata", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_CommandMetadata_CommandTemplateId", + table: "CommandMetadata", + column: "CommandTemplateId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_CommandMetadata_CommandTemplates_CommandTemplateId", + table: "CommandMetadata", + column: "CommandTemplateId", + principalTable: "CommandTemplates", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_CustomCommandParameters_CustomCommands_CustomCommandId", + table: "CustomCommandParameters", + column: "CustomCommandId", + principalTable: "CustomCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/AresService.Migrations.Postgres/Migrations/20260710160726_AddCustomCommands_AresIdentityContext.Designer.cs b/AresService.Migrations.Postgres/Migrations/20260710160726_AddCustomCommands_AresIdentityContext.Designer.cs new file mode 100644 index 00000000..6ef24fe2 --- /dev/null +++ b/AresService.Migrations.Postgres/Migrations/20260710160726_AddCustomCommands_AresIdentityContext.Designer.cs @@ -0,0 +1,277 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AresService.Migrations.Postgres.Migrations.AresIdentity +{ + [DbContext(typeof(AresIdentityContext))] + [Migration("20260710160726_AddCustomCommands_AresIdentityContext")] + partial class AddCustomCommands_AresIdentityContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AresService.Data.AresUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.Postgres/Migrations/20260710160726_AddCustomCommands_AresIdentityContext.cs b/AresService.Migrations.Postgres/Migrations/20260710160726_AddCustomCommands_AresIdentityContext.cs new file mode 100644 index 00000000..d67d1b19 --- /dev/null +++ b/AresService.Migrations.Postgres/Migrations/20260710160726_AddCustomCommands_AresIdentityContext.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.Postgres.Migrations.AresIdentity +{ + /// + public partial class AddCustomCommands_AresIdentityContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/AresService.Migrations.Postgres/Migrations/AresDbContextModelSnapshot.cs b/AresService.Migrations.Postgres/Migrations/AresDbContextModelSnapshot.cs index 0d19c09f..fc9d1512 100644 --- a/AresService.Migrations.Postgres/Migrations/AresDbContextModelSnapshot.cs +++ b/AresService.Migrations.Postgres/Migrations/AresDbContextModelSnapshot.cs @@ -310,6 +310,104 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AresGeneralSettingsConfig", (string)null); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("CurrentVersionId") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.HasKey("UniqueId"); + + b.ToTable("CustomCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("CustomCommandVersionId") + .HasColumnType("uuid"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Schema") + .HasColumnType("text"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandVersionId"); + + b.ToTable("CustomCommandParameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("CustomCommandId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OutputSchema") + .HasColumnType("text"); + + b.Property("ScriptBody") + .HasColumnType("text"); + + b.Property("VersionNumber") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandId", "VersionNumber") + .IsUnique(); + + b.ToTable("CustomCommandVersions", (string)null); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.Property("UniqueId") @@ -1296,9 +1394,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("CommandTemplateId") - .HasColumnType("uuid"); - b.Property("CreationTime") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -1307,6 +1402,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description") .HasColumnType("text"); + b.Property("DeviceCommandId") + .HasColumnType("uuid"); + b.Property("DeviceId") .HasColumnType("text"); @@ -1323,7 +1421,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("UniqueId"); - b.HasIndex("CommandTemplateId") + b.HasIndex("DeviceCommandId") .IsUnique(); b.ToTable("CommandMetadata"); @@ -1361,6 +1459,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("CommandTemplates", (string)null); }); + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CommandTemplateId") + .HasColumnType("uuid"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("NOW()"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateId") + .IsUnique(); + + b.ToTable("DeviceCommands", (string)null); + }); + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => { b.Property("UniqueId") @@ -1716,6 +1841,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommandVersion", null) + .WithMany("InputParameters") + .HasForeignKey("CustomCommandVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + .WithMany() + .HasForeignKey("CustomCommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "CloseoutExecutionSummary") @@ -1875,11 +2018,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => { - b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + b.HasOne("Ares.Datamodel.Templates.DeviceCommand", null) .WithOne("Metadata") - .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "CommandTemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "DeviceCommandId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => @@ -1889,6 +2031,50 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("StepTemplateUniqueId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.OwnsOne("Ares.Datamodel.Templates.CustomCommandInvocation", "CustomCommandInvocation", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("uuid"); + + b1.Property("CustomCommandId") + .HasColumnType("text"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.OwnsOne("Ares.Datamodel.Templates.SystemCommand", "SystemCommand", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("uuid"); + + b1.Property("Operation") + .HasColumnType("integer"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.Navigation("CustomCommandInvocation"); + + b.Navigation("SystemCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithOne("DeviceCommand") + .HasForeignKey("Ares.Datamodel.Templates.DeviceCommand", "CommandTemplateId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => @@ -1921,7 +2107,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => { b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) - .WithMany("Parameters") + .WithMany("ArgumentBindings") .HasForeignKey("CommandTemplateUniqueId") .OnDelete(DeleteBehavior.Cascade); @@ -1961,6 +2147,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Capabilities"); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Navigation("InputParameters"); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.Navigation("ExecutionInfo"); @@ -2040,9 +2231,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => { - b.Navigation("Metadata"); + b.Navigation("ArgumentBindings"); - b.Navigation("Parameters"); + b.Navigation("DeviceCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Navigation("Metadata"); }); modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => diff --git a/AresService.Migrations.SqlServer/Migrations/20260626195929_CustomCommands_AresDbContext.Designer.cs b/AresService.Migrations.SqlServer/Migrations/20260626195929_CustomCommands_AresDbContext.Designer.cs new file mode 100644 index 00000000..09b6b7a6 --- /dev/null +++ b/AresService.Migrations.SqlServer/Migrations/20260626195929_CustomCommands_AresDbContext.Designer.cs @@ -0,0 +1,2164 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AresService.Migrations.SqlServer.Migrations.AresDb +{ + [DbContext(typeof(AresDbContext))] + [Migration("20260626195929_CustomCommands_AresDbContext")] + partial class CustomCommands_AresDbContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalyzerInfo") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentOverviewId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Result") + .HasColumnType("float"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentOverviewId") + .IsUnique() + .HasFilter("[ExperimentOverviewId] IS NOT NULL"); + + b.ToTable("AnalysisOverview"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.Analysis", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalysisOutcome") + .HasColumnType("int"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ErrorString") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Result") + .HasColumnType("real"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalyzerInfoId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("SettingsSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeoutSeconds") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("AnalyzerInfoId") + .IsUnique(); + + b.ToTable("AnalyzerCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyzers", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalyzerId") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalysisRequest") + .HasColumnType("nvarchar(max)"); + + b.Property("AnalysisResponse") + .HasColumnType("nvarchar(max)"); + + b.Property("AnalyzerId") + .HasColumnType("nvarchar(max)"); + + b.Property("AnalyzerName") + .HasColumnType("nvarchar(max)"); + + b.Property("AnalyzerType") + .HasColumnType("nvarchar(max)"); + + b.Property("AnalyzerVersion") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("TimeRequestSent") + .HasColumnType("datetime2"); + + b.Property("TimeResponseReceived") + .HasColumnType("datetime2"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresCampaignTag", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("TagName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("CampaignTags"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresGeneralSettingsConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandLatency") + .HasColumnType("nvarchar(max)"); + + b.Property("CommandRetryLimit") + .HasColumnType("int"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentRetryLimit") + .HasColumnType("int"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("RetryCooldown") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("AresGeneralSettingsConfig", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("OutputSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("ScriptBody") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("CustomCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("CustomCommandId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Schema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandId"); + + b.ToTable("CustomCommandParameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignId") + .HasColumnType("nvarchar(max)"); + + b.Property("CampaignName") + .HasColumnType("nvarchar(max)"); + + b.Property("CampaignNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("CampaignTags") + .HasColumnType("nvarchar(max)"); + + b.Property("CloseoutExecutionSummaryUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("StartupExecutionSummaryUniqueId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("CloseoutExecutionSummaryUniqueId"); + + b.HasIndex("StartupExecutionSummaryUniqueId"); + + b.ToTable("CampaignExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandId") + .HasColumnType("nvarchar(max)"); + + b.Property("CommandName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DeviceName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Result") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StatusMessage") + .HasColumnType("nvarchar(max)"); + + b.Property("StepExecutionStatusUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("VariableName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionStatusUniqueId"); + + b.ToTable("CommandExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandDescription") + .HasColumnType("nvarchar(max)"); + + b.Property("CommandId") + .HasColumnType("nvarchar(max)"); + + b.Property("CommandName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("StatusCode") + .HasColumnType("int"); + + b.Property("StepExecutionSummaryUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemplateId") + .HasColumnType("nvarchar(max)"); + + b.Property("VarName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionSummaryUniqueId"); + + b.ToTable("CommandExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AwaitUserInput") + .HasColumnType("bit"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Error") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Result") + .HasColumnType("nvarchar(max)"); + + b.Property("StatusCode") + .HasColumnType("int"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique() + .HasFilter("[CommandExecutionSummaryId] IS NOT NULL"); + + b.ToTable("DeviceCommandResults", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceInfoId") + .HasColumnType("uniqueidentifier"); + + b.Property("InputSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("OutputSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("DeviceInfoId"); + + b.ToTable("DeviceCommandDescriptor"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceName") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceSettings") + .HasColumnType("nvarchar(max)"); + + b.Property("DriverId") + .HasColumnType("nvarchar(max)"); + + b.Property("IsSimulated") + .HasColumnType("bit"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("SerialInfoUniqueId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("SerialInfoUniqueId"); + + b.ToTable("DeviceConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SettingsSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceLoggingSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Deltas") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("IntervalMs") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LoggingEnabled") + .HasColumnType("bit"); + + b.Property("LoggingType") + .HasColumnType("int"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceLoggingSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceState", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Timestamp") + .HasColumnType("datetime2"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceStates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.RemoteDeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("RemoteDevices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.SerialConnection", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BaudRate") + .HasColumnType("int"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("PortName") + .HasColumnType("nvarchar(max)"); + + b.Property("SerialId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("SerialConnection"); + }); + + modelBuilder.Entity("Ares.Datamodel.DeviceErrorHandlingConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Handling") + .HasColumnType("int"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.HasKey("UniqueId"); + + b.ToTable("ErrorHandlingConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignExecutionSummaryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentResultId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LocaltimeOffset") + .HasColumnType("nvarchar(max)"); + + b.Property("StepExecutionSummaryId") + .HasColumnType("uniqueidentifier"); + + b.Property("TimeFinished") + .HasColumnType("datetime2"); + + b.Property("TimeStarted") + .HasColumnType("datetime2"); + + b.Property("Timezone") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryId") + .IsUnique() + .HasFilter("[CampaignExecutionSummaryId] IS NOT NULL"); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique() + .HasFilter("[CommandExecutionSummaryId] IS NOT NULL"); + + b.HasIndex("ExperimentResultId") + .IsUnique() + .HasFilter("[ExperimentResultId] IS NOT NULL"); + + b.HasIndex("StepExecutionSummaryId") + .IsUnique() + .HasFilter("[StepExecutionSummaryId] IS NOT NULL"); + + b.ToTable("ExecutionInfos", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignExecutionSummaryUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentId") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ResultOutputPath") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryUniqueId"); + + b.ToTable("ExperimentExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentResultId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Result") + .HasColumnType("nvarchar(max)"); + + b.Property("TemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentResultId") + .IsUnique() + .HasFilter("[ExperimentResultId] IS NOT NULL"); + + b.HasIndex("TemplateUniqueId"); + + b.ToTable("ExperimentOverviews", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Maximum") + .HasColumnType("float"); + + b.Property("Minimum") + .HasColumnType("float"); + + b.Property("ParameterMetadataUniqueId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("ParameterMetadataUniqueId"); + + b.ToTable("Limits"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("PlannerName") + .HasColumnType("nvarchar(max)"); + + b.Property("PlannerServiceCapabilitiesUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerServiceCapabilitiesUniqueId"); + + b.ToTable("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ParameterUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("PlannerId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("ParameterUniqueId"); + + b.HasIndex("PlannerId"); + + b.ToTable("PlannerAllocations", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerServices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("PlannerInfoId") + .HasColumnType("uniqueidentifier"); + + b.Property("ServiceName") + .HasColumnType("nvarchar(max)"); + + b.Property("SettingsSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeoutSeconds") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerInfoId") + .IsUnique(); + + b.ToTable("PlannerServiceCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("PlannerId") + .HasColumnType("nvarchar(max)"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("PlannerId") + .HasColumnType("nvarchar(max)"); + + b.Property("PlannerName") + .HasColumnType("nvarchar(max)"); + + b.Property("PlannerType") + .HasColumnType("nvarchar(max)"); + + b.Property("PlannerVersion") + .HasColumnType("nvarchar(max)"); + + b.Property("PlanningRequest") + .HasColumnType("nvarchar(max)"); + + b.Property("PlanningResponse") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeRequestSent") + .HasColumnType("datetime2"); + + b.Property("TimeResponseReceived") + .HasColumnType("datetime2"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.Project", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("StepId") + .HasColumnType("nvarchar(max)"); + + b.Property("StepName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("StepExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentExecutionSummaryUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("StepId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentExecutionSummaryUniqueId"); + + b.ToTable("StepExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UniqueId"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[Name] IS NOT NULL"); + + b.ToTable("CampaignTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandTemplateId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceType") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateId") + .IsUnique(); + + b.ToTable("CommandMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("OutputVarName") + .HasColumnType("nvarchar(max)"); + + b.Property("StepTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepTemplateUniqueId"); + + b.ToTable("CommandTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalyzerId") + .HasColumnType("nvarchar(max)"); + + b.Property("CampaignCloseoutId") + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignExperimentId") + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignStartupId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Resolved") + .HasColumnType("bit"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignCloseoutId") + .IsUnique() + .HasFilter("[CampaignCloseoutId] IS NOT NULL"); + + b.HasIndex("CampaignExperimentId") + .IsUnique() + .HasFilter("[CampaignExperimentId] IS NOT NULL"); + + b.HasIndex("CampaignStartupId") + .IsUnique() + .HasFilter("[CampaignStartupId] IS NOT NULL"); + + b.ToTable("ExperimentTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DataSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.HasKey("UniqueId"); + + b.ToTable("OutputMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentOverviewUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("SourceJson") + .HasColumnType("nvarchar(max)") + .HasColumnName("Source"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateUniqueId"); + + b.HasIndex("ExperimentOverviewUniqueId"); + + b.ToTable("Parameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CommandMetadataUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("InitialValue") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NotPlannable") + .HasColumnType("bit"); + + b.Property("OutputName") + .HasColumnType("nvarchar(max)"); + + b.Property("ParameterId") + .HasColumnType("uniqueidentifier"); + + b.Property("PlannerDescription") + .HasColumnType("nvarchar(max)"); + + b.Property("PlannerName") + .HasColumnType("nvarchar(max)"); + + b.Property("Schema") + .HasColumnType("nvarchar(max)"); + + b.Property("Unit") + .HasColumnType("nvarchar(max)"); + + b.Property("UseDefault") + .HasColumnType("bit"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("CommandMetadataUniqueId"); + + b.HasIndex("ParameterId") + .IsUnique() + .HasFilter("[ParameterId] IS NOT NULL"); + + b.ToTable("ParameterMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("IsParallel") + .HasColumnType("bit"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentTemplateUniqueId"); + + b.ToTable("StepTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.DeviceVisualizationConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ChartTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("GridH") + .HasColumnType("int"); + + b.Property("GridW") + .HasColumnType("int"); + + b.Property("GridX") + .HasColumnType("int"); + + b.Property("GridY") + .HasColumnType("int"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("NumberDisplayPoints") + .HasColumnType("int"); + + b.Property("Paths") + .HasColumnType("nvarchar(max)"); + + b.Property("PollingRate") + .HasColumnType("int"); + + b.Property("ShowDataLabels") + .HasColumnType("bit"); + + b.Property("ShowMarkers") + .HasColumnType("bit"); + + b.Property("Style") + .HasColumnType("int"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceVisualizationConfigs"); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.VisualizationPath", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssociatedDeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DataType") + .HasColumnType("int"); + + b.Property("IsPlottable") + .HasColumnType("bit"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Path") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("VisualizationPath"); + }); + + modelBuilder.Entity("Ares.Services.DriverInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DriverId") + .HasColumnType("nvarchar(max)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceDrivers"); + }); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithOne("AnalysisOverview") + .HasForeignKey("Ares.Datamodel.AnalysisOverview", "ExperimentOverviewId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.HasOne("Ares.Datamodel.Analyzing.AnalyzerInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Analyzing.AnalyzerCapabilities", "AnalyzerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + .WithMany("InputParameters") + .HasForeignKey("CustomCommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "CloseoutExecutionSummary") + .WithMany() + .HasForeignKey("CloseoutExecutionSummaryUniqueId"); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "StartupExecutionSummary") + .WithMany() + .HasForeignKey("StartupExecutionSummaryUniqueId"); + + b.Navigation("CloseoutExecutionSummary"); + + b.Navigation("StartupExecutionSummary"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.HasOne("Ares.Datamodel.StepExecutionStatus", null) + .WithMany("CommandExecutionStatuses") + .HasForeignKey("StepExecutionStatusUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithMany("CommandSummaries") + .HasForeignKey("StepExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("Result") + .HasForeignKey("Ares.Datamodel.CommandResult", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.HasOne("Ares.Datamodel.Device.DeviceInfo", null) + .WithMany("Commands") + .HasForeignKey("DeviceInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.HasOne("Ares.Datamodel.Device.SerialConnection", "SerialInfo") + .WithMany() + .HasForeignKey("SerialInfoUniqueId"); + + b.Navigation("SerialInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CampaignExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "ExperimentResultId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "StepExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithMany("ExperimentSummaries") + .HasForeignKey("CampaignExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExperimentOverview") + .HasForeignKey("Ares.Datamodel.ExperimentOverview", "ExperimentResultId"); + + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", "Template") + .WithMany() + .HasForeignKey("TemplateUniqueId"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", null) + .WithMany("Constraints") + .HasForeignKey("ParameterMetadataUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceCapabilities", null) + .WithMany("AvailablePlanners") + .HasForeignKey("PlannerServiceCapabilitiesUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannerAllocations") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", "Parameter") + .WithMany() + .HasForeignKey("ParameterUniqueId"); + + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", "Planner") + .WithMany() + .HasForeignKey("PlannerId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.Navigation("Parameter"); + + b.Navigation("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Planning.PlannerServiceCapabilities", "PlannerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithMany("StepSummaries") + .HasForeignKey("ExperimentExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "CommandTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.StepTemplate", null) + .WithMany("CommandTemplates") + .HasForeignKey("StepTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("CloseoutTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignCloseoutId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("ExperimentTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignExperimentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("StartupTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignStartupId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithOne("OutputMetadata") + .HasForeignKey("Ares.Datamodel.Templates.OutputMetadata", "UniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithMany("Parameters") + .HasForeignKey("CommandTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithMany("Parameters") + .HasForeignKey("ExperimentOverviewUniqueId"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannableParameters") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithMany("ParameterMetadatas") + .HasForeignKey("CommandMetadataUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.Parameter", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.ParameterMetadata", "ParameterId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", null) + .WithMany("StepTemplates") + .HasForeignKey("ExperimentTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Navigation("InputParameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("Result"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Navigation("Commands"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentOverview"); + + b.Navigation("StepSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Navigation("AnalysisOverview"); + + b.Navigation("Parameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Navigation("AvailablePlanners"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Navigation("CommandExecutionStatuses"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Navigation("CommandSummaries"); + + b.Navigation("ExecutionInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Navigation("CloseoutTemplate"); + + b.Navigation("ExperimentTemplate"); + + b.Navigation("PlannableParameters"); + + b.Navigation("PlannerAllocations"); + + b.Navigation("StartupTemplate"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Navigation("OutputMetadata"); + + b.Navigation("ParameterMetadatas"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Navigation("Metadata"); + + b.Navigation("Parameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Navigation("StepTemplates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Navigation("Constraints"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Navigation("CommandTemplates"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.SqlServer/Migrations/20260626195929_CustomCommands_AresDbContext.cs b/AresService.Migrations.SqlServer/Migrations/20260626195929_CustomCommands_AresDbContext.cs new file mode 100644 index 00000000..0a84f8ab --- /dev/null +++ b/AresService.Migrations.SqlServer/Migrations/20260626195929_CustomCommands_AresDbContext.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.SqlServer.Migrations.AresDb +{ + /// + public partial class CustomCommands_AresDbContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CustomCommands", + columns: table => new + { + UniqueId = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: true), + Description = table.Column(type: "nvarchar(max)", nullable: true), + OutputSchema = table.Column(type: "nvarchar(max)", nullable: true), + ScriptBody = table.Column(type: "nvarchar(max)", nullable: true), + CreationTime = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()"), + LastModified = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()") + }, + constraints: table => + { + table.PrimaryKey("PK_CustomCommands", x => x.UniqueId); + }); + + migrationBuilder.CreateTable( + name: "CustomCommandParameters", + columns: table => new + { + UniqueId = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: true), + Schema = table.Column(type: "nvarchar(max)", nullable: true), + CreationTime = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()"), + CustomCommandId = table.Column(type: "uniqueidentifier", nullable: false), + LastModified = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()") + }, + constraints: table => + { + table.PrimaryKey("PK_CustomCommandParameters", x => x.UniqueId); + table.ForeignKey( + name: "FK_CustomCommandParameters_CustomCommands_CustomCommandId", + column: x => x.CustomCommandId, + principalTable: "CustomCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CustomCommandParameters_CustomCommandId", + table: "CustomCommandParameters", + column: "CustomCommandId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CustomCommandParameters"); + + migrationBuilder.DropTable( + name: "CustomCommands"); + } + } +} diff --git a/AresService.Migrations.SqlServer/Migrations/20260626195932_CustomCommands_AresIdentityContext.Designer.cs b/AresService.Migrations.SqlServer/Migrations/20260626195932_CustomCommands_AresIdentityContext.Designer.cs new file mode 100644 index 00000000..f6adbc9f --- /dev/null +++ b/AresService.Migrations.SqlServer/Migrations/20260626195932_CustomCommands_AresIdentityContext.Designer.cs @@ -0,0 +1,279 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AresService.Migrations.SqlServer.Migrations.AresIdentity +{ + [DbContext(typeof(AresIdentityContext))] + [Migration("20260626195932_CustomCommands_AresIdentityContext")] + partial class CustomCommands_AresIdentityContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AresService.Data.AresUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.SqlServer/Migrations/20260626195932_CustomCommands_AresIdentityContext.cs b/AresService.Migrations.SqlServer/Migrations/20260626195932_CustomCommands_AresIdentityContext.cs new file mode 100644 index 00000000..cfe805d3 --- /dev/null +++ b/AresService.Migrations.SqlServer/Migrations/20260626195932_CustomCommands_AresIdentityContext.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.SqlServer.Migrations.AresIdentity +{ + /// + public partial class CustomCommands_AresIdentityContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/AresService.Migrations.SqlServer/Migrations/20260710160728_AddCustomCommands_AresDbContext.Designer.cs b/AresService.Migrations.SqlServer/Migrations/20260710160728_AddCustomCommands_AresDbContext.Designer.cs new file mode 100644 index 00000000..d868ac8a --- /dev/null +++ b/AresService.Migrations.SqlServer/Migrations/20260710160728_AddCustomCommands_AresDbContext.Designer.cs @@ -0,0 +1,2283 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AresService.Migrations.SqlServer.Migrations.AresDb +{ + [DbContext(typeof(AresDbContext))] + [Migration("20260710160728_AddCustomCommands_AresDbContext")] + partial class AddCustomCommands_AresDbContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalyzerInfo") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentOverviewId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Result") + .HasColumnType("float"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentOverviewId") + .IsUnique() + .HasFilter("[ExperimentOverviewId] IS NOT NULL"); + + b.ToTable("AnalysisOverview"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.Analysis", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalysisOutcome") + .HasColumnType("int"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ErrorString") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Result") + .HasColumnType("real"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalyzerInfoId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("SettingsSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeoutSeconds") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("AnalyzerInfoId") + .IsUnique(); + + b.ToTable("AnalyzerCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyzers", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalyzerId") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalysisRequest") + .HasColumnType("nvarchar(max)"); + + b.Property("AnalysisResponse") + .HasColumnType("nvarchar(max)"); + + b.Property("AnalyzerId") + .HasColumnType("nvarchar(max)"); + + b.Property("AnalyzerName") + .HasColumnType("nvarchar(max)"); + + b.Property("AnalyzerType") + .HasColumnType("nvarchar(max)"); + + b.Property("AnalyzerVersion") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("TimeRequestSent") + .HasColumnType("datetime2"); + + b.Property("TimeResponseReceived") + .HasColumnType("datetime2"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresCampaignTag", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("TagName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("CampaignTags"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresGeneralSettingsConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandLatency") + .HasColumnType("nvarchar(max)"); + + b.Property("CommandRetryLimit") + .HasColumnType("int"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentRetryLimit") + .HasColumnType("int"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("RetryCooldown") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("AresGeneralSettingsConfig", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("CurrentVersionId") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.HasKey("UniqueId"); + + b.ToTable("CustomCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("CustomCommandVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Schema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandVersionId"); + + b.ToTable("CustomCommandParameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("CustomCommandId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("OutputSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("ScriptBody") + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandId", "VersionNumber") + .IsUnique(); + + b.ToTable("CustomCommandVersions", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignId") + .HasColumnType("nvarchar(max)"); + + b.Property("CampaignName") + .HasColumnType("nvarchar(max)"); + + b.Property("CampaignNotes") + .HasColumnType("nvarchar(max)"); + + b.Property("CampaignTags") + .HasColumnType("nvarchar(max)"); + + b.Property("CloseoutExecutionSummaryUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("StartupExecutionSummaryUniqueId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("CloseoutExecutionSummaryUniqueId"); + + b.HasIndex("StartupExecutionSummaryUniqueId"); + + b.ToTable("CampaignExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandId") + .HasColumnType("nvarchar(max)"); + + b.Property("CommandName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DeviceName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Result") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StatusMessage") + .HasColumnType("nvarchar(max)"); + + b.Property("StepExecutionStatusUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("VariableName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionStatusUniqueId"); + + b.ToTable("CommandExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandDescription") + .HasColumnType("nvarchar(max)"); + + b.Property("CommandId") + .HasColumnType("nvarchar(max)"); + + b.Property("CommandName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("StatusCode") + .HasColumnType("int"); + + b.Property("StepExecutionSummaryUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("TemplateId") + .HasColumnType("nvarchar(max)"); + + b.Property("VarName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionSummaryUniqueId"); + + b.ToTable("CommandExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AwaitUserInput") + .HasColumnType("bit"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Error") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Result") + .HasColumnType("nvarchar(max)"); + + b.Property("StatusCode") + .HasColumnType("int"); + + b.Property("Success") + .HasColumnType("bit"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique() + .HasFilter("[CommandExecutionSummaryId] IS NOT NULL"); + + b.ToTable("DeviceCommandResults", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceInfoId") + .HasColumnType("uniqueidentifier"); + + b.Property("InputSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("OutputSchema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("DeviceInfoId"); + + b.ToTable("DeviceCommandDescriptor"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceName") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceSettings") + .HasColumnType("nvarchar(max)"); + + b.Property("DriverId") + .HasColumnType("nvarchar(max)"); + + b.Property("IsSimulated") + .HasColumnType("bit"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("SerialInfoUniqueId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("SerialInfoUniqueId"); + + b.ToTable("DeviceConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("SettingsSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceLoggingSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Deltas") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("IntervalMs") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LoggingEnabled") + .HasColumnType("bit"); + + b.Property("LoggingType") + .HasColumnType("int"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceLoggingSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceState", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Data") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Timestamp") + .HasColumnType("datetime2"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceStates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.RemoteDeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("RemoteDevices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.SerialConnection", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BaudRate") + .HasColumnType("int"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("PortName") + .HasColumnType("nvarchar(max)"); + + b.Property("SerialId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("SerialConnection"); + }); + + modelBuilder.Entity("Ares.Datamodel.DeviceErrorHandlingConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .HasColumnType("int"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Handling") + .HasColumnType("int"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.HasKey("UniqueId"); + + b.ToTable("ErrorHandlingConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignExecutionSummaryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentResultId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LocaltimeOffset") + .HasColumnType("nvarchar(max)"); + + b.Property("StepExecutionSummaryId") + .HasColumnType("uniqueidentifier"); + + b.Property("TimeFinished") + .HasColumnType("datetime2"); + + b.Property("TimeStarted") + .HasColumnType("datetime2"); + + b.Property("Timezone") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryId") + .IsUnique() + .HasFilter("[CampaignExecutionSummaryId] IS NOT NULL"); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique() + .HasFilter("[CommandExecutionSummaryId] IS NOT NULL"); + + b.HasIndex("ExperimentResultId") + .IsUnique() + .HasFilter("[ExperimentResultId] IS NOT NULL"); + + b.HasIndex("StepExecutionSummaryId") + .IsUnique() + .HasFilter("[StepExecutionSummaryId] IS NOT NULL"); + + b.ToTable("ExecutionInfos", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignExecutionSummaryUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentId") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ResultOutputPath") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryUniqueId"); + + b.ToTable("ExperimentExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentResultId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Result") + .HasColumnType("nvarchar(max)"); + + b.Property("TemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentResultId") + .IsUnique() + .HasFilter("[ExperimentResultId] IS NOT NULL"); + + b.HasIndex("TemplateUniqueId"); + + b.ToTable("ExperimentOverviews", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Maximum") + .HasColumnType("float"); + + b.Property("Minimum") + .HasColumnType("float"); + + b.Property("ParameterMetadataUniqueId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("ParameterMetadataUniqueId"); + + b.ToTable("Limits"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("PlannerName") + .HasColumnType("nvarchar(max)"); + + b.Property("PlannerServiceCapabilitiesUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerServiceCapabilitiesUniqueId"); + + b.ToTable("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ParameterUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("PlannerId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("ParameterUniqueId"); + + b.HasIndex("PlannerId"); + + b.ToTable("PlannerAllocations", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Url") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerServices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("PlannerInfoId") + .HasColumnType("uniqueidentifier"); + + b.Property("ServiceName") + .HasColumnType("nvarchar(max)"); + + b.Property("SettingsSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeoutSeconds") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerInfoId") + .IsUnique(); + + b.ToTable("PlannerServiceCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("PlannerId") + .HasColumnType("nvarchar(max)"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("PlannerId") + .HasColumnType("nvarchar(max)"); + + b.Property("PlannerName") + .HasColumnType("nvarchar(max)"); + + b.Property("PlannerType") + .HasColumnType("nvarchar(max)"); + + b.Property("PlannerVersion") + .HasColumnType("nvarchar(max)"); + + b.Property("PlanningRequest") + .HasColumnType("nvarchar(max)"); + + b.Property("PlanningResponse") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeRequestSent") + .HasColumnType("datetime2"); + + b.Property("TimeResponseReceived") + .HasColumnType("datetime2"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.Project", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("StepId") + .HasColumnType("nvarchar(max)"); + + b.Property("StepName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("StepExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentExecutionSummaryUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("StepId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentExecutionSummaryUniqueId"); + + b.ToTable("StepExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UniqueId"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[Name] IS NOT NULL"); + + b.ToTable("CampaignTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceCommandId") + .HasColumnType("uniqueidentifier"); + + b.Property("DeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceType") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("DeviceCommandId") + .IsUnique() + .HasFilter("[DeviceCommandId] IS NOT NULL"); + + b.ToTable("CommandMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("OutputVarName") + .HasColumnType("nvarchar(max)"); + + b.Property("StepTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepTemplateUniqueId"); + + b.ToTable("CommandTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandTemplateId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateId") + .IsUnique() + .HasFilter("[CommandTemplateId] IS NOT NULL"); + + b.ToTable("DeviceCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnalyzerId") + .HasColumnType("nvarchar(max)"); + + b.Property("CampaignCloseoutId") + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignExperimentId") + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignStartupId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Resolved") + .HasColumnType("bit"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignCloseoutId") + .IsUnique() + .HasFilter("[CampaignCloseoutId] IS NOT NULL"); + + b.HasIndex("CampaignExperimentId") + .IsUnique() + .HasFilter("[CampaignExperimentId] IS NOT NULL"); + + b.HasIndex("CampaignStartupId") + .IsUnique() + .HasFilter("[CampaignStartupId] IS NOT NULL"); + + b.ToTable("ExperimentTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DataSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.HasKey("UniqueId"); + + b.ToTable("OutputMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentOverviewUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("SourceJson") + .HasColumnType("nvarchar(max)") + .HasColumnName("Source"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateUniqueId"); + + b.HasIndex("ExperimentOverviewUniqueId"); + + b.ToTable("Parameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CommandMetadataUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("InitialValue") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NotPlannable") + .HasColumnType("bit"); + + b.Property("OutputName") + .HasColumnType("nvarchar(max)"); + + b.Property("ParameterId") + .HasColumnType("uniqueidentifier"); + + b.Property("PlannerDescription") + .HasColumnType("nvarchar(max)"); + + b.Property("PlannerName") + .HasColumnType("nvarchar(max)"); + + b.Property("Schema") + .HasColumnType("nvarchar(max)"); + + b.Property("Unit") + .HasColumnType("nvarchar(max)"); + + b.Property("UseDefault") + .HasColumnType("bit"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("CommandMetadataUniqueId"); + + b.HasIndex("ParameterId") + .IsUnique() + .HasFilter("[ParameterId] IS NOT NULL"); + + b.ToTable("ParameterMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("ExperimentTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b.Property("Index") + .HasColumnType("bigint"); + + b.Property("IsParallel") + .HasColumnType("bit"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentTemplateUniqueId"); + + b.ToTable("StepTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.DeviceVisualizationConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ChartTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("GridH") + .HasColumnType("int"); + + b.Property("GridW") + .HasColumnType("int"); + + b.Property("GridX") + .HasColumnType("int"); + + b.Property("GridY") + .HasColumnType("int"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("NumberDisplayPoints") + .HasColumnType("int"); + + b.Property("Paths") + .HasColumnType("nvarchar(max)"); + + b.Property("PollingRate") + .HasColumnType("int"); + + b.Property("ShowDataLabels") + .HasColumnType("bit"); + + b.Property("ShowMarkers") + .HasColumnType("bit"); + + b.Property("Style") + .HasColumnType("int"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceVisualizationConfigs"); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.VisualizationPath", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssociatedDeviceId") + .HasColumnType("nvarchar(max)"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DataType") + .HasColumnType("int"); + + b.Property("IsPlottable") + .HasColumnType("bit"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Path") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("VisualizationPath"); + }); + + modelBuilder.Entity("Ares.Services.DriverInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DriverId") + .HasColumnType("nvarchar(max)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Version") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceDrivers"); + }); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithOne("AnalysisOverview") + .HasForeignKey("Ares.Datamodel.AnalysisOverview", "ExperimentOverviewId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.HasOne("Ares.Datamodel.Analyzing.AnalyzerInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Analyzing.AnalyzerCapabilities", "AnalyzerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommandVersion", null) + .WithMany("InputParameters") + .HasForeignKey("CustomCommandVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + .WithMany() + .HasForeignKey("CustomCommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "CloseoutExecutionSummary") + .WithMany() + .HasForeignKey("CloseoutExecutionSummaryUniqueId"); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "StartupExecutionSummary") + .WithMany() + .HasForeignKey("StartupExecutionSummaryUniqueId"); + + b.Navigation("CloseoutExecutionSummary"); + + b.Navigation("StartupExecutionSummary"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.HasOne("Ares.Datamodel.StepExecutionStatus", null) + .WithMany("CommandExecutionStatuses") + .HasForeignKey("StepExecutionStatusUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithMany("CommandSummaries") + .HasForeignKey("StepExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("Result") + .HasForeignKey("Ares.Datamodel.CommandResult", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.HasOne("Ares.Datamodel.Device.DeviceInfo", null) + .WithMany("Commands") + .HasForeignKey("DeviceInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.HasOne("Ares.Datamodel.Device.SerialConnection", "SerialInfo") + .WithMany() + .HasForeignKey("SerialInfoUniqueId"); + + b.Navigation("SerialInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CampaignExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "ExperimentResultId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "StepExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithMany("ExperimentSummaries") + .HasForeignKey("CampaignExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExperimentOverview") + .HasForeignKey("Ares.Datamodel.ExperimentOverview", "ExperimentResultId"); + + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", "Template") + .WithMany() + .HasForeignKey("TemplateUniqueId"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", null) + .WithMany("Constraints") + .HasForeignKey("ParameterMetadataUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceCapabilities", null) + .WithMany("AvailablePlanners") + .HasForeignKey("PlannerServiceCapabilitiesUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannerAllocations") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", "Parameter") + .WithMany() + .HasForeignKey("ParameterUniqueId"); + + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", "Planner") + .WithMany() + .HasForeignKey("PlannerId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.Navigation("Parameter"); + + b.Navigation("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Planning.PlannerServiceCapabilities", "PlannerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithMany("StepSummaries") + .HasForeignKey("ExperimentExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.DeviceCommand", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "DeviceCommandId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.StepTemplate", null) + .WithMany("CommandTemplates") + .HasForeignKey("StepTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Ares.Datamodel.Templates.CustomCommandInvocation", "CustomCommandInvocation", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b1.Property("CustomCommandId") + .HasColumnType("nvarchar(max)"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.OwnsOne("Ares.Datamodel.Templates.SystemCommand", "SystemCommand", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Operation") + .HasColumnType("int"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.Navigation("CustomCommandInvocation"); + + b.Navigation("SystemCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithOne("DeviceCommand") + .HasForeignKey("Ares.Datamodel.Templates.DeviceCommand", "CommandTemplateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("CloseoutTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignCloseoutId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("ExperimentTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignExperimentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("StartupTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignStartupId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithOne("OutputMetadata") + .HasForeignKey("Ares.Datamodel.Templates.OutputMetadata", "UniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithMany("ArgumentBindings") + .HasForeignKey("CommandTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithMany("Parameters") + .HasForeignKey("ExperimentOverviewUniqueId"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannableParameters") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithMany("ParameterMetadatas") + .HasForeignKey("CommandMetadataUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.Parameter", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.ParameterMetadata", "ParameterId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", null) + .WithMany("StepTemplates") + .HasForeignKey("ExperimentTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Navigation("InputParameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("Result"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Navigation("Commands"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentOverview"); + + b.Navigation("StepSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Navigation("AnalysisOverview"); + + b.Navigation("Parameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Navigation("AvailablePlanners"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Navigation("CommandExecutionStatuses"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Navigation("CommandSummaries"); + + b.Navigation("ExecutionInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Navigation("CloseoutTemplate"); + + b.Navigation("ExperimentTemplate"); + + b.Navigation("PlannableParameters"); + + b.Navigation("PlannerAllocations"); + + b.Navigation("StartupTemplate"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Navigation("OutputMetadata"); + + b.Navigation("ParameterMetadatas"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Navigation("ArgumentBindings"); + + b.Navigation("DeviceCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Navigation("StepTemplates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Navigation("Constraints"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Navigation("CommandTemplates"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.SqlServer/Migrations/20260710160728_AddCustomCommands_AresDbContext.cs b/AresService.Migrations.SqlServer/Migrations/20260710160728_AddCustomCommands_AresDbContext.cs new file mode 100644 index 00000000..eed26b98 --- /dev/null +++ b/AresService.Migrations.SqlServer/Migrations/20260710160728_AddCustomCommands_AresDbContext.cs @@ -0,0 +1,280 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.SqlServer.Migrations.AresDb +{ + /// + public partial class AddCustomCommands_AresDbContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_CommandMetadata_CommandTemplates_CommandTemplateId", + table: "CommandMetadata"); + + migrationBuilder.DropForeignKey( + name: "FK_CustomCommandParameters_CustomCommands_CustomCommandId", + table: "CustomCommandParameters"); + + migrationBuilder.DropIndex( + name: "IX_CommandMetadata_CommandTemplateId", + table: "CommandMetadata"); + + migrationBuilder.DropColumn( + name: "Description", + table: "CustomCommands"); + + migrationBuilder.DropColumn( + name: "Name", + table: "CustomCommands"); + + migrationBuilder.DropColumn( + name: "OutputSchema", + table: "CustomCommands"); + + migrationBuilder.RenameColumn( + name: "ScriptBody", + table: "CustomCommands", + newName: "CurrentVersionId"); + + migrationBuilder.RenameColumn( + name: "CustomCommandId", + table: "CustomCommandParameters", + newName: "CustomCommandVersionId"); + + migrationBuilder.RenameIndex( + name: "IX_CustomCommandParameters_CustomCommandId", + table: "CustomCommandParameters", + newName: "IX_CustomCommandParameters_CustomCommandVersionId"); + + migrationBuilder.AddColumn( + name: "CustomCommandInvocation_CustomCommandId", + table: "CommandTemplates", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "SystemCommand_Operation", + table: "CommandTemplates", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "DeviceCommandId", + table: "CommandMetadata", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.CreateTable( + name: "CustomCommandVersions", + columns: table => new + { + UniqueId = table.Column(type: "uniqueidentifier", nullable: false), + CustomCommandId = table.Column(type: "uniqueidentifier", nullable: false), + VersionNumber = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: true), + Description = table.Column(type: "nvarchar(max)", nullable: true), + OutputSchema = table.Column(type: "nvarchar(max)", nullable: true), + ScriptBody = table.Column(type: "nvarchar(max)", nullable: true), + CreationTime = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()"), + LastModified = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()") + }, + constraints: table => + { + table.PrimaryKey("PK_CustomCommandVersions", x => x.UniqueId); + table.ForeignKey( + name: "FK_CustomCommandVersions_CustomCommands_CustomCommandId", + column: x => x.CustomCommandId, + principalTable: "CustomCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "DeviceCommands", + columns: table => new + { + UniqueId = table.Column(type: "uniqueidentifier", nullable: false), + CommandTemplateId = table.Column(type: "uniqueidentifier", nullable: true), + CreationTime = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()"), + LastModified = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()") + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceCommands", x => x.UniqueId); + table.ForeignKey( + name: "FK_DeviceCommands_CommandTemplates_CommandTemplateId", + column: x => x.CommandTemplateId, + principalTable: "CommandTemplates", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql( + """ + INSERT INTO [DeviceCommands] ([UniqueId], [CommandTemplateId]) + SELECT [UniqueId], [UniqueId] + FROM [CommandTemplates]; + + UPDATE [CommandMetadata] + SET [DeviceCommandId] = [CommandTemplateId]; + """); + + migrationBuilder.DropColumn( + name: "CommandTemplateId", + table: "CommandMetadata"); + + migrationBuilder.CreateIndex( + name: "IX_CommandMetadata_DeviceCommandId", + table: "CommandMetadata", + column: "DeviceCommandId", + unique: true, + filter: "[DeviceCommandId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_CustomCommandVersions_CustomCommandId_VersionNumber", + table: "CustomCommandVersions", + columns: new[] { "CustomCommandId", "VersionNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DeviceCommands_CommandTemplateId", + table: "DeviceCommands", + column: "CommandTemplateId", + unique: true, + filter: "[CommandTemplateId] IS NOT NULL"); + + migrationBuilder.AddForeignKey( + name: "FK_CommandMetadata_DeviceCommands_DeviceCommandId", + table: "CommandMetadata", + column: "DeviceCommandId", + principalTable: "DeviceCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_CustomCommandParameters_CustomCommandVersions_CustomCommandVersionId", + table: "CustomCommandParameters", + column: "CustomCommandVersionId", + principalTable: "CustomCommandVersions", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_CommandMetadata_DeviceCommands_DeviceCommandId", + table: "CommandMetadata"); + + migrationBuilder.DropForeignKey( + name: "FK_CustomCommandParameters_CustomCommandVersions_CustomCommandVersionId", + table: "CustomCommandParameters"); + + migrationBuilder.DropTable( + name: "CustomCommandVersions"); + + migrationBuilder.DropIndex( + name: "IX_CommandMetadata_DeviceCommandId", + table: "CommandMetadata"); + + migrationBuilder.DropColumn( + name: "CustomCommandInvocation_CustomCommandId", + table: "CommandTemplates"); + + migrationBuilder.DropColumn( + name: "SystemCommand_Operation", + table: "CommandTemplates"); + + migrationBuilder.RenameColumn( + name: "CurrentVersionId", + table: "CustomCommands", + newName: "ScriptBody"); + + migrationBuilder.RenameColumn( + name: "CustomCommandVersionId", + table: "CustomCommandParameters", + newName: "CustomCommandId"); + + migrationBuilder.RenameIndex( + name: "IX_CustomCommandParameters_CustomCommandVersionId", + table: "CustomCommandParameters", + newName: "IX_CustomCommandParameters_CustomCommandId"); + + migrationBuilder.AddColumn( + name: "Description", + table: "CustomCommands", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "Name", + table: "CustomCommands", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "OutputSchema", + table: "CustomCommands", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "CommandTemplateId", + table: "CommandMetadata", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.Sql( + """ + UPDATE metadata + SET metadata.[CommandTemplateId] = commands.[CommandTemplateId] + FROM [CommandMetadata] AS metadata + INNER JOIN [DeviceCommands] AS commands + ON commands.[UniqueId] = metadata.[DeviceCommandId]; + """); + + migrationBuilder.DropColumn( + name: "DeviceCommandId", + table: "CommandMetadata"); + + migrationBuilder.DropTable( + name: "DeviceCommands"); + + migrationBuilder.AlterColumn( + name: "CommandTemplateId", + table: "CommandMetadata", + type: "uniqueidentifier", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_CommandMetadata_CommandTemplateId", + table: "CommandMetadata", + column: "CommandTemplateId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_CommandMetadata_CommandTemplates_CommandTemplateId", + table: "CommandMetadata", + column: "CommandTemplateId", + principalTable: "CommandTemplates", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_CustomCommandParameters_CustomCommands_CustomCommandId", + table: "CustomCommandParameters", + column: "CustomCommandId", + principalTable: "CustomCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/AresService.Migrations.SqlServer/Migrations/20260710160729_AddCustomCommands_AresIdentityContext.Designer.cs b/AresService.Migrations.SqlServer/Migrations/20260710160729_AddCustomCommands_AresIdentityContext.Designer.cs new file mode 100644 index 00000000..cfa925a4 --- /dev/null +++ b/AresService.Migrations.SqlServer/Migrations/20260710160729_AddCustomCommands_AresIdentityContext.Designer.cs @@ -0,0 +1,279 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AresService.Migrations.SqlServer.Migrations.AresIdentity +{ + [DbContext(typeof(AresIdentityContext))] + [Migration("20260710160729_AddCustomCommands_AresIdentityContext")] + partial class AddCustomCommands_AresIdentityContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AresService.Data.AresUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.SqlServer/Migrations/20260710160729_AddCustomCommands_AresIdentityContext.cs b/AresService.Migrations.SqlServer/Migrations/20260710160729_AddCustomCommands_AresIdentityContext.cs new file mode 100644 index 00000000..1d8cd1f5 --- /dev/null +++ b/AresService.Migrations.SqlServer/Migrations/20260710160729_AddCustomCommands_AresIdentityContext.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.SqlServer.Migrations.AresIdentity +{ + /// + public partial class AddCustomCommands_AresIdentityContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/AresService.Migrations.SqlServer/Migrations/AresDbContextModelSnapshot.cs b/AresService.Migrations.SqlServer/Migrations/AresDbContextModelSnapshot.cs index 10fbf9df..2df66b6b 100644 --- a/AresService.Migrations.SqlServer/Migrations/AresDbContextModelSnapshot.cs +++ b/AresService.Migrations.SqlServer/Migrations/AresDbContextModelSnapshot.cs @@ -311,6 +311,104 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AresGeneralSettingsConfig", (string)null); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("CurrentVersionId") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.HasKey("UniqueId"); + + b.ToTable("CustomCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("CustomCommandVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Schema") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandVersionId"); + + b.ToTable("CustomCommandParameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("CustomCommandId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("OutputSchema") + .HasColumnType("nvarchar(max)"); + + b.Property("ScriptBody") + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("bigint"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandId", "VersionNumber") + .IsUnique(); + + b.ToTable("CustomCommandVersions", (string)null); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.Property("UniqueId") @@ -1304,9 +1402,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("CommandTemplateId") - .HasColumnType("uniqueidentifier"); - b.Property("CreationTime") .ValueGeneratedOnAdd() .HasColumnType("datetime2") @@ -1315,6 +1410,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description") .HasColumnType("nvarchar(max)"); + b.Property("DeviceCommandId") + .HasColumnType("uniqueidentifier"); + b.Property("DeviceId") .HasColumnType("nvarchar(max)"); @@ -1331,8 +1429,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("UniqueId"); - b.HasIndex("CommandTemplateId") - .IsUnique(); + b.HasIndex("DeviceCommandId") + .IsUnique() + .HasFilter("[DeviceCommandId] IS NOT NULL"); b.ToTable("CommandMetadata"); }); @@ -1369,6 +1468,34 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("CommandTemplates", (string)null); }); + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CommandTemplateId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime2") + .HasDefaultValueSql("getdate()"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateId") + .IsUnique() + .HasFilter("[CommandTemplateId] IS NOT NULL"); + + b.ToTable("DeviceCommands", (string)null); + }); + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => { b.Property("UniqueId") @@ -1728,6 +1855,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommandVersion", null) + .WithMany("InputParameters") + .HasForeignKey("CustomCommandVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + .WithMany() + .HasForeignKey("CustomCommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "CloseoutExecutionSummary") @@ -1887,11 +2032,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => { - b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + b.HasOne("Ares.Datamodel.Templates.DeviceCommand", null) .WithOne("Metadata") - .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "CommandTemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "DeviceCommandId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => @@ -1901,6 +2045,50 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("StepTemplateUniqueId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.OwnsOne("Ares.Datamodel.Templates.CustomCommandInvocation", "CustomCommandInvocation", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b1.Property("CustomCommandId") + .HasColumnType("nvarchar(max)"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.OwnsOne("Ares.Datamodel.Templates.SystemCommand", "SystemCommand", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Operation") + .HasColumnType("int"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.Navigation("CustomCommandInvocation"); + + b.Navigation("SystemCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithOne("DeviceCommand") + .HasForeignKey("Ares.Datamodel.Templates.DeviceCommand", "CommandTemplateId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => @@ -1933,7 +2121,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => { b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) - .WithMany("Parameters") + .WithMany("ArgumentBindings") .HasForeignKey("CommandTemplateUniqueId") .OnDelete(DeleteBehavior.Cascade); @@ -1973,6 +2161,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Capabilities"); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Navigation("InputParameters"); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.Navigation("ExecutionInfo"); @@ -2052,9 +2245,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => { - b.Navigation("Metadata"); + b.Navigation("ArgumentBindings"); - b.Navigation("Parameters"); + b.Navigation("DeviceCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Navigation("Metadata"); }); modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => diff --git a/AresService.Migrations.Sqlite/Migrations/20260617194346_FixCorruptedParameterSources_AresDbContext.cs b/AresService.Migrations.Sqlite/Migrations/20260617194346_FixCorruptedParameterSources_AresDbContext.cs index 13b21838..5d0075a0 100644 --- a/AresService.Migrations.Sqlite/Migrations/20260617194346_FixCorruptedParameterSources_AresDbContext.cs +++ b/AresService.Migrations.Sqlite/Migrations/20260617194346_FixCorruptedParameterSources_AresDbContext.cs @@ -10,7 +10,7 @@ public partial class FixCorruptedParameterSources_AresDbContext : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.Sql("UPDATE Parameters SET Source = '{}' WHERE Source = '0'"); + migrationBuilder.Sql("UPDATE Parameters SET Source = '{}' WHERE Source = '0';"); } /// diff --git a/AresService.Migrations.Sqlite/Migrations/20260626195916_CustomCommands_AresDbContext.Designer.cs b/AresService.Migrations.Sqlite/Migrations/20260626195916_CustomCommands_AresDbContext.Designer.cs new file mode 100644 index 00000000..2d3567bc --- /dev/null +++ b/AresService.Migrations.Sqlite/Migrations/20260626195916_CustomCommands_AresDbContext.Designer.cs @@ -0,0 +1,2147 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AresService.Migrations.Sqlite.Migrations.AresDb +{ + [DbContext(typeof(AresDbContext))] + [Migration("20260626195916_CustomCommands_AresDbContext")] + partial class CustomCommands_AresDbContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.8"); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalyzerInfo") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentOverviewId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Result") + .HasColumnType("REAL"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentOverviewId") + .IsUnique(); + + b.ToTable("AnalysisOverview"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.Analysis", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalysisOutcome") + .HasColumnType("INTEGER"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ErrorString") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Result") + .HasColumnType("REAL"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalyzerInfoId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("SettingsSchema") + .HasColumnType("TEXT"); + + b.Property("TimeoutSeconds") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("AnalyzerInfoId") + .IsUnique(); + + b.ToTable("AnalyzerCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyzers", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalyzerId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalysisRequest") + .HasColumnType("TEXT"); + + b.Property("AnalysisResponse") + .HasColumnType("TEXT"); + + b.Property("AnalyzerId") + .HasColumnType("TEXT"); + + b.Property("AnalyzerName") + .HasColumnType("TEXT"); + + b.Property("AnalyzerType") + .HasColumnType("TEXT"); + + b.Property("AnalyzerVersion") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("TimeRequestSent") + .HasColumnType("TEXT"); + + b.Property("TimeResponseReceived") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresCampaignTag", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("TagName") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("CampaignTags"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresGeneralSettingsConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommandLatency") + .HasColumnType("TEXT"); + + b.Property("CommandRetryLimit") + .HasColumnType("INTEGER"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentRetryLimit") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("RetryCooldown") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("AresGeneralSettingsConfig", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OutputSchema") + .HasColumnType("TEXT"); + + b.Property("ScriptBody") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("CustomCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("CustomCommandId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Schema") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandId"); + + b.ToTable("CustomCommandParameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CampaignId") + .HasColumnType("TEXT"); + + b.Property("CampaignName") + .HasColumnType("TEXT"); + + b.Property("CampaignNotes") + .HasColumnType("TEXT"); + + b.Property("CampaignTags") + .HasColumnType("TEXT"); + + b.Property("CloseoutExecutionSummaryUniqueId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("StartupExecutionSummaryUniqueId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CloseoutExecutionSummaryUniqueId"); + + b.HasIndex("StartupExecutionSummaryUniqueId"); + + b.ToTable("CampaignExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommandId") + .HasColumnType("TEXT"); + + b.Property("CommandName") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DeviceName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Result") + .HasColumnType("TEXT"); + + b.Property("State") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StatusMessage") + .HasColumnType("TEXT"); + + b.Property("StepExecutionStatusUniqueId") + .HasColumnType("TEXT"); + + b.Property("VariableName") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionStatusUniqueId"); + + b.ToTable("CommandExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommandDescription") + .HasColumnType("TEXT"); + + b.Property("CommandId") + .HasColumnType("TEXT"); + + b.Property("CommandName") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("StatusCode") + .HasColumnType("INTEGER"); + + b.Property("StepExecutionSummaryUniqueId") + .HasColumnType("TEXT"); + + b.Property("TemplateId") + .HasColumnType("TEXT"); + + b.Property("VarName") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionSummaryUniqueId"); + + b.ToTable("CommandExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AwaitUserInput") + .HasColumnType("INTEGER"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Result") + .HasColumnType("TEXT"); + + b.Property("StatusCode") + .HasColumnType("INTEGER"); + + b.Property("Success") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique(); + + b.ToTable("DeviceCommandResults", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("DeviceInfoId") + .HasColumnType("TEXT"); + + b.Property("InputSchema") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OutputSchema") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("DeviceInfoId"); + + b.ToTable("DeviceCommandDescriptor"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .HasColumnType("TEXT"); + + b.Property("DeviceSettings") + .HasColumnType("TEXT"); + + b.Property("DriverId") + .HasColumnType("TEXT"); + + b.Property("IsSimulated") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("SerialInfoUniqueId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("SerialInfoUniqueId"); + + b.ToTable("DeviceConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SettingsSchema") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceLoggingSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Deltas") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("IntervalMs") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LoggingEnabled") + .HasColumnType("INTEGER"); + + b.Property("LoggingType") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceLoggingSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceState", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceStates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.RemoteDeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("RemoteDevices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.SerialConnection", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BaudRate") + .HasColumnType("INTEGER"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("PortName") + .HasColumnType("TEXT"); + + b.Property("SerialId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("SerialConnection"); + }); + + modelBuilder.Entity("Ares.Datamodel.DeviceErrorHandlingConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Code") + .HasColumnType("INTEGER"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Handling") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.HasKey("UniqueId"); + + b.ToTable("ErrorHandlingConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CampaignExecutionSummaryId") + .HasColumnType("TEXT"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentResultId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LocaltimeOffset") + .HasColumnType("TEXT"); + + b.Property("StepExecutionSummaryId") + .HasColumnType("TEXT"); + + b.Property("TimeFinished") + .HasColumnType("TEXT"); + + b.Property("TimeStarted") + .HasColumnType("TEXT"); + + b.Property("Timezone") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryId") + .IsUnique(); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique(); + + b.HasIndex("ExperimentResultId") + .IsUnique(); + + b.HasIndex("StepExecutionSummaryId") + .IsUnique(); + + b.ToTable("ExecutionInfos", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CampaignExecutionSummaryUniqueId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ResultOutputPath") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryUniqueId"); + + b.ToTable("ExperimentExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentResultId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Result") + .HasColumnType("TEXT"); + + b.Property("TemplateUniqueId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentResultId") + .IsUnique(); + + b.HasIndex("TemplateUniqueId"); + + b.ToTable("ExperimentOverviews", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Maximum") + .HasColumnType("REAL"); + + b.Property("Minimum") + .HasColumnType("REAL"); + + b.Property("ParameterMetadataUniqueId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("ParameterMetadataUniqueId"); + + b.ToTable("Limits"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("PlannerName") + .HasColumnType("TEXT"); + + b.Property("PlannerServiceCapabilitiesUniqueId") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerServiceCapabilitiesUniqueId"); + + b.ToTable("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ParameterUniqueId") + .HasColumnType("TEXT"); + + b.Property("PlannerId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("ParameterUniqueId"); + + b.HasIndex("PlannerId"); + + b.ToTable("PlannerAllocations", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerServices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("PlannerInfoId") + .HasColumnType("TEXT"); + + b.Property("ServiceName") + .HasColumnType("TEXT"); + + b.Property("SettingsSchema") + .HasColumnType("TEXT"); + + b.Property("TimeoutSeconds") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerInfoId") + .IsUnique(); + + b.ToTable("PlannerServiceCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("PlannerId") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("PlannerId") + .HasColumnType("TEXT"); + + b.Property("PlannerName") + .HasColumnType("TEXT"); + + b.Property("PlannerType") + .HasColumnType("TEXT"); + + b.Property("PlannerVersion") + .HasColumnType("TEXT"); + + b.Property("PlanningRequest") + .HasColumnType("TEXT"); + + b.Property("PlanningResponse") + .HasColumnType("TEXT"); + + b.Property("TimeRequestSent") + .HasColumnType("TEXT"); + + b.Property("TimeResponseReceived") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.Project", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("StepId") + .HasColumnType("TEXT"); + + b.Property("StepName") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("StepExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentExecutionSummaryUniqueId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("StepId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentExecutionSummaryUniqueId"); + + b.ToTable("StepExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("CampaignTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommandTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateId") + .IsUnique(); + + b.ToTable("CommandMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("OutputVarName") + .HasColumnType("TEXT"); + + b.Property("StepTemplateUniqueId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepTemplateUniqueId"); + + b.ToTable("CommandTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalyzerId") + .HasColumnType("TEXT"); + + b.Property("CampaignCloseoutId") + .HasColumnType("TEXT"); + + b.Property("CampaignExperimentId") + .HasColumnType("TEXT"); + + b.Property("CampaignStartupId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Resolved") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignCloseoutId") + .IsUnique(); + + b.HasIndex("CampaignExperimentId") + .IsUnique(); + + b.HasIndex("CampaignStartupId") + .IsUnique(); + + b.ToTable("ExperimentTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DataSchema") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.HasKey("UniqueId"); + + b.ToTable("OutputMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommandTemplateUniqueId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentOverviewUniqueId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("SourceJson") + .HasColumnType("TEXT") + .HasColumnName("Source"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateUniqueId"); + + b.HasIndex("ExperimentOverviewUniqueId"); + + b.ToTable("Parameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("TEXT"); + + b.Property("CommandMetadataUniqueId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("InitialValue") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NotPlannable") + .HasColumnType("INTEGER"); + + b.Property("OutputName") + .HasColumnType("TEXT"); + + b.Property("ParameterId") + .HasColumnType("TEXT"); + + b.Property("PlannerDescription") + .HasColumnType("TEXT"); + + b.Property("PlannerName") + .HasColumnType("TEXT"); + + b.Property("Schema") + .HasColumnType("TEXT"); + + b.Property("Unit") + .HasColumnType("TEXT"); + + b.Property("UseDefault") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("CommandMetadataUniqueId"); + + b.HasIndex("ParameterId") + .IsUnique(); + + b.ToTable("ParameterMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentTemplateUniqueId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("IsParallel") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentTemplateUniqueId"); + + b.ToTable("StepTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.DeviceVisualizationConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ChartTitle") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("GridH") + .HasColumnType("INTEGER"); + + b.Property("GridW") + .HasColumnType("INTEGER"); + + b.Property("GridX") + .HasColumnType("INTEGER"); + + b.Property("GridY") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("NumberDisplayPoints") + .HasColumnType("INTEGER"); + + b.Property("Paths") + .HasColumnType("TEXT"); + + b.Property("PollingRate") + .HasColumnType("INTEGER"); + + b.Property("ShowDataLabels") + .HasColumnType("INTEGER"); + + b.Property("ShowMarkers") + .HasColumnType("INTEGER"); + + b.Property("Style") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceVisualizationConfigs"); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.VisualizationPath", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AssociatedDeviceId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DataType") + .HasColumnType("INTEGER"); + + b.Property("IsPlottable") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("VisualizationPath"); + }); + + modelBuilder.Entity("Ares.Services.DriverInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("DriverId") + .HasColumnType("TEXT"); + + b.Property("FileSizeBytes") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceDrivers"); + }); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithOne("AnalysisOverview") + .HasForeignKey("Ares.Datamodel.AnalysisOverview", "ExperimentOverviewId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.HasOne("Ares.Datamodel.Analyzing.AnalyzerInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Analyzing.AnalyzerCapabilities", "AnalyzerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + .WithMany("InputParameters") + .HasForeignKey("CustomCommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "CloseoutExecutionSummary") + .WithMany() + .HasForeignKey("CloseoutExecutionSummaryUniqueId"); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "StartupExecutionSummary") + .WithMany() + .HasForeignKey("StartupExecutionSummaryUniqueId"); + + b.Navigation("CloseoutExecutionSummary"); + + b.Navigation("StartupExecutionSummary"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.HasOne("Ares.Datamodel.StepExecutionStatus", null) + .WithMany("CommandExecutionStatuses") + .HasForeignKey("StepExecutionStatusUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithMany("CommandSummaries") + .HasForeignKey("StepExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("Result") + .HasForeignKey("Ares.Datamodel.CommandResult", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.HasOne("Ares.Datamodel.Device.DeviceInfo", null) + .WithMany("Commands") + .HasForeignKey("DeviceInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.HasOne("Ares.Datamodel.Device.SerialConnection", "SerialInfo") + .WithMany() + .HasForeignKey("SerialInfoUniqueId"); + + b.Navigation("SerialInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CampaignExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "ExperimentResultId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "StepExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithMany("ExperimentSummaries") + .HasForeignKey("CampaignExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExperimentOverview") + .HasForeignKey("Ares.Datamodel.ExperimentOverview", "ExperimentResultId"); + + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", "Template") + .WithMany() + .HasForeignKey("TemplateUniqueId"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", null) + .WithMany("Constraints") + .HasForeignKey("ParameterMetadataUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceCapabilities", null) + .WithMany("AvailablePlanners") + .HasForeignKey("PlannerServiceCapabilitiesUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannerAllocations") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", "Parameter") + .WithMany() + .HasForeignKey("ParameterUniqueId"); + + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", "Planner") + .WithMany() + .HasForeignKey("PlannerId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.Navigation("Parameter"); + + b.Navigation("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Planning.PlannerServiceCapabilities", "PlannerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithMany("StepSummaries") + .HasForeignKey("ExperimentExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "CommandTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.StepTemplate", null) + .WithMany("CommandTemplates") + .HasForeignKey("StepTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("CloseoutTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignCloseoutId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("ExperimentTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignExperimentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("StartupTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignStartupId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithOne("OutputMetadata") + .HasForeignKey("Ares.Datamodel.Templates.OutputMetadata", "UniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithMany("Parameters") + .HasForeignKey("CommandTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithMany("Parameters") + .HasForeignKey("ExperimentOverviewUniqueId"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannableParameters") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithMany("ParameterMetadatas") + .HasForeignKey("CommandMetadataUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.Parameter", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.ParameterMetadata", "ParameterId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", null) + .WithMany("StepTemplates") + .HasForeignKey("ExperimentTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Navigation("InputParameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("Result"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Navigation("Commands"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentOverview"); + + b.Navigation("StepSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Navigation("AnalysisOverview"); + + b.Navigation("Parameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Navigation("AvailablePlanners"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Navigation("CommandExecutionStatuses"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Navigation("CommandSummaries"); + + b.Navigation("ExecutionInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Navigation("CloseoutTemplate"); + + b.Navigation("ExperimentTemplate"); + + b.Navigation("PlannableParameters"); + + b.Navigation("PlannerAllocations"); + + b.Navigation("StartupTemplate"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Navigation("OutputMetadata"); + + b.Navigation("ParameterMetadatas"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Navigation("Metadata"); + + b.Navigation("Parameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Navigation("StepTemplates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Navigation("Constraints"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Navigation("CommandTemplates"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.Sqlite/Migrations/20260626195916_CustomCommands_AresDbContext.cs b/AresService.Migrations.Sqlite/Migrations/20260626195916_CustomCommands_AresDbContext.cs new file mode 100644 index 00000000..bec3d4d7 --- /dev/null +++ b/AresService.Migrations.Sqlite/Migrations/20260626195916_CustomCommands_AresDbContext.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.Sqlite.Migrations.AresDb +{ + /// + public partial class CustomCommands_AresDbContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CustomCommands", + columns: table => new + { + UniqueId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + Description = table.Column(type: "TEXT", nullable: true), + OutputSchema = table.Column(type: "TEXT", nullable: true), + ScriptBody = table.Column(type: "TEXT", nullable: true), + CreationTime = table.Column(type: "TEXT", nullable: false, defaultValueSql: "DATETIME('now')"), + LastModified = table.Column(type: "TEXT", nullable: false, defaultValueSql: "DATETIME('now')") + }, + constraints: table => + { + table.PrimaryKey("PK_CustomCommands", x => x.UniqueId); + }); + + migrationBuilder.CreateTable( + name: "CustomCommandParameters", + columns: table => new + { + UniqueId = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + Schema = table.Column(type: "TEXT", nullable: true), + CreationTime = table.Column(type: "TEXT", nullable: false, defaultValueSql: "DATETIME('now')"), + CustomCommandId = table.Column(type: "TEXT", nullable: false), + LastModified = table.Column(type: "TEXT", nullable: false, defaultValueSql: "DATETIME('now')") + }, + constraints: table => + { + table.PrimaryKey("PK_CustomCommandParameters", x => x.UniqueId); + table.ForeignKey( + name: "FK_CustomCommandParameters_CustomCommands_CustomCommandId", + column: x => x.CustomCommandId, + principalTable: "CustomCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CustomCommandParameters_CustomCommandId", + table: "CustomCommandParameters", + column: "CustomCommandId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CustomCommandParameters"); + + migrationBuilder.DropTable( + name: "CustomCommands"); + } + } +} diff --git a/AresService.Migrations.Sqlite/Migrations/20260626195919_CustomCommands_AresIdentityContext.Designer.cs b/AresService.Migrations.Sqlite/Migrations/20260626195919_CustomCommands_AresIdentityContext.Designer.cs new file mode 100644 index 00000000..45298806 --- /dev/null +++ b/AresService.Migrations.Sqlite/Migrations/20260626195919_CustomCommands_AresIdentityContext.Designer.cs @@ -0,0 +1,268 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AresService.Migrations.Sqlite.Migrations.AresIdentity +{ + [DbContext(typeof(AresIdentityContext))] + [Migration("20260626195919_CustomCommands_AresIdentityContext")] + partial class CustomCommands_AresIdentityContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.8"); + + modelBuilder.Entity("AresService.Data.AresUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.Sqlite/Migrations/20260626195919_CustomCommands_AresIdentityContext.cs b/AresService.Migrations.Sqlite/Migrations/20260626195919_CustomCommands_AresIdentityContext.cs new file mode 100644 index 00000000..e7e66dfe --- /dev/null +++ b/AresService.Migrations.Sqlite/Migrations/20260626195919_CustomCommands_AresIdentityContext.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.Sqlite.Migrations.AresIdentity +{ + /// + public partial class CustomCommands_AresIdentityContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/AresService.Migrations.Sqlite/Migrations/20260710160722_AddCustomCommands_AresDbContext.Designer.cs b/AresService.Migrations.Sqlite/Migrations/20260710160722_AddCustomCommands_AresDbContext.Designer.cs new file mode 100644 index 00000000..1c3e3587 --- /dev/null +++ b/AresService.Migrations.Sqlite/Migrations/20260710160722_AddCustomCommands_AresDbContext.Designer.cs @@ -0,0 +1,2264 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AresService.Migrations.Sqlite.Migrations.AresDb +{ + [DbContext(typeof(AresDbContext))] + [Migration("20260710160722_AddCustomCommands_AresDbContext")] + partial class AddCustomCommands_AresDbContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.8"); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalyzerInfo") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentOverviewId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Result") + .HasColumnType("REAL"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentOverviewId") + .IsUnique(); + + b.ToTable("AnalysisOverview"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.Analysis", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalysisOutcome") + .HasColumnType("INTEGER"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ErrorString") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Result") + .HasColumnType("REAL"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalyzerInfoId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("SettingsSchema") + .HasColumnType("TEXT"); + + b.Property("TimeoutSeconds") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("AnalyzerInfoId") + .IsUnique(); + + b.ToTable("AnalyzerCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("Analyzers", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalyzerId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalysisRequest") + .HasColumnType("TEXT"); + + b.Property("AnalysisResponse") + .HasColumnType("TEXT"); + + b.Property("AnalyzerId") + .HasColumnType("TEXT"); + + b.Property("AnalyzerName") + .HasColumnType("TEXT"); + + b.Property("AnalyzerType") + .HasColumnType("TEXT"); + + b.Property("AnalyzerVersion") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("TimeRequestSent") + .HasColumnType("TEXT"); + + b.Property("TimeResponseReceived") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("AnalyzerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresCampaignTag", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("TagName") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("CampaignTags"); + }); + + modelBuilder.Entity("Ares.Datamodel.AresGeneralSettingsConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommandLatency") + .HasColumnType("TEXT"); + + b.Property("CommandRetryLimit") + .HasColumnType("INTEGER"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentRetryLimit") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("RetryCooldown") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("AresGeneralSettingsConfig", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("CurrentVersionId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.HasKey("UniqueId"); + + b.ToTable("CustomCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("CustomCommandVersionId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Schema") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandVersionId"); + + b.ToTable("CustomCommandParameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("CustomCommandId") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OutputSchema") + .HasColumnType("TEXT"); + + b.Property("ScriptBody") + .HasColumnType("TEXT"); + + b.Property("VersionNumber") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandId", "VersionNumber") + .IsUnique(); + + b.ToTable("CustomCommandVersions", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CampaignId") + .HasColumnType("TEXT"); + + b.Property("CampaignName") + .HasColumnType("TEXT"); + + b.Property("CampaignNotes") + .HasColumnType("TEXT"); + + b.Property("CampaignTags") + .HasColumnType("TEXT"); + + b.Property("CloseoutExecutionSummaryUniqueId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("StartupExecutionSummaryUniqueId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CloseoutExecutionSummaryUniqueId"); + + b.HasIndex("StartupExecutionSummaryUniqueId"); + + b.ToTable("CampaignExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommandId") + .HasColumnType("TEXT"); + + b.Property("CommandName") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DeviceName") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Result") + .HasColumnType("TEXT"); + + b.Property("State") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("StatusMessage") + .HasColumnType("TEXT"); + + b.Property("StepExecutionStatusUniqueId") + .HasColumnType("TEXT"); + + b.Property("VariableName") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionStatusUniqueId"); + + b.ToTable("CommandExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommandDescription") + .HasColumnType("TEXT"); + + b.Property("CommandId") + .HasColumnType("TEXT"); + + b.Property("CommandName") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("StatusCode") + .HasColumnType("INTEGER"); + + b.Property("StepExecutionSummaryUniqueId") + .HasColumnType("TEXT"); + + b.Property("TemplateId") + .HasColumnType("TEXT"); + + b.Property("VarName") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepExecutionSummaryUniqueId"); + + b.ToTable("CommandExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AwaitUserInput") + .HasColumnType("INTEGER"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Error") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Result") + .HasColumnType("TEXT"); + + b.Property("StatusCode") + .HasColumnType("INTEGER"); + + b.Property("Success") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique(); + + b.ToTable("DeviceCommandResults", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("DeviceInfoId") + .HasColumnType("TEXT"); + + b.Property("InputSchema") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OutputSchema") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("DeviceInfoId"); + + b.ToTable("DeviceCommandDescriptor"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .HasColumnType("TEXT"); + + b.Property("DeviceSettings") + .HasColumnType("TEXT"); + + b.Property("DriverId") + .HasColumnType("TEXT"); + + b.Property("IsSimulated") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("SerialInfoUniqueId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("SerialInfoUniqueId"); + + b.ToTable("DeviceConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SettingsSchema") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceLoggingSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Deltas") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("IntervalMs") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LoggingEnabled") + .HasColumnType("INTEGER"); + + b.Property("LoggingType") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceLoggingSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceState", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceStates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.RemoteDeviceConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("RemoteDevices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.SerialConnection", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BaudRate") + .HasColumnType("INTEGER"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("PortName") + .HasColumnType("TEXT"); + + b.Property("SerialId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("SerialConnection"); + }); + + modelBuilder.Entity("Ares.Datamodel.DeviceErrorHandlingConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Code") + .HasColumnType("INTEGER"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Handling") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.HasKey("UniqueId"); + + b.ToTable("ErrorHandlingConfigs", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CampaignExecutionSummaryId") + .HasColumnType("TEXT"); + + b.Property("CommandExecutionSummaryId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentResultId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LocaltimeOffset") + .HasColumnType("TEXT"); + + b.Property("StepExecutionSummaryId") + .HasColumnType("TEXT"); + + b.Property("TimeFinished") + .HasColumnType("TEXT"); + + b.Property("TimeStarted") + .HasColumnType("TEXT"); + + b.Property("Timezone") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryId") + .IsUnique(); + + b.HasIndex("CommandExecutionSummaryId") + .IsUnique(); + + b.HasIndex("ExperimentResultId") + .IsUnique(); + + b.HasIndex("StepExecutionSummaryId") + .IsUnique(); + + b.ToTable("ExecutionInfos", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CampaignExecutionSummaryUniqueId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ResultOutputPath") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignExecutionSummaryUniqueId"); + + b.ToTable("ExperimentExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentResultId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Result") + .HasColumnType("TEXT"); + + b.Property("TemplateUniqueId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentResultId") + .IsUnique(); + + b.HasIndex("TemplateUniqueId"); + + b.ToTable("ExperimentOverviews", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Maximum") + .HasColumnType("REAL"); + + b.Property("Minimum") + .HasColumnType("REAL"); + + b.Property("ParameterMetadataUniqueId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("ParameterMetadataUniqueId"); + + b.ToTable("Limits"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("PlannerName") + .HasColumnType("TEXT"); + + b.Property("PlannerServiceCapabilitiesUniqueId") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerServiceCapabilitiesUniqueId"); + + b.ToTable("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ParameterUniqueId") + .HasColumnType("TEXT"); + + b.Property("PlannerId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("ParameterUniqueId"); + + b.HasIndex("PlannerId"); + + b.ToTable("PlannerAllocations", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerServices", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("PlannerInfoId") + .HasColumnType("TEXT"); + + b.Property("ServiceName") + .HasColumnType("TEXT"); + + b.Property("SettingsSchema") + .HasColumnType("TEXT"); + + b.Property("TimeoutSeconds") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("PlannerInfoId") + .IsUnique(); + + b.ToTable("PlannerServiceCapabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerInfos"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerSettings", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("PlannerId") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerSettings"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerTransaction", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("PlannerId") + .HasColumnType("TEXT"); + + b.Property("PlannerName") + .HasColumnType("TEXT"); + + b.Property("PlannerType") + .HasColumnType("TEXT"); + + b.Property("PlannerVersion") + .HasColumnType("TEXT"); + + b.Property("PlanningRequest") + .HasColumnType("TEXT"); + + b.Property("PlanningResponse") + .HasColumnType("TEXT"); + + b.Property("TimeRequestSent") + .HasColumnType("TEXT"); + + b.Property("TimeResponseReceived") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("PlannerTransactions"); + }); + + modelBuilder.Entity("Ares.Datamodel.Project", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("Projects", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("StepId") + .HasColumnType("TEXT"); + + b.Property("StepName") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("StepExecutionStatuses", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentExecutionSummaryUniqueId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("StepId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentExecutionSummaryUniqueId"); + + b.ToTable("StepExecutionSummaries", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("CampaignTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("DeviceCommandId") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("DeviceCommandId") + .IsUnique(); + + b.ToTable("CommandMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("OutputVarName") + .HasColumnType("TEXT"); + + b.Property("StepTemplateUniqueId") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("StepTemplateUniqueId"); + + b.ToTable("CommandTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommandTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateId") + .IsUnique(); + + b.ToTable("DeviceCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AnalyzerId") + .HasColumnType("TEXT"); + + b.Property("CampaignCloseoutId") + .HasColumnType("TEXT"); + + b.Property("CampaignExperimentId") + .HasColumnType("TEXT"); + + b.Property("CampaignStartupId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Resolved") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignCloseoutId") + .IsUnique(); + + b.HasIndex("CampaignExperimentId") + .IsUnique(); + + b.HasIndex("CampaignStartupId") + .IsUnique(); + + b.ToTable("ExperimentTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DataSchema") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.HasKey("UniqueId"); + + b.ToTable("OutputMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommandTemplateUniqueId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentOverviewUniqueId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("SourceJson") + .HasColumnType("TEXT") + .HasColumnName("Source"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateUniqueId"); + + b.HasIndex("ExperimentOverviewUniqueId"); + + b.ToTable("Parameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CampaignTemplateUniqueId") + .HasColumnType("TEXT"); + + b.Property("CommandMetadataUniqueId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("InitialValue") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NotPlannable") + .HasColumnType("INTEGER"); + + b.Property("OutputName") + .HasColumnType("TEXT"); + + b.Property("ParameterId") + .HasColumnType("TEXT"); + + b.Property("PlannerDescription") + .HasColumnType("TEXT"); + + b.Property("PlannerName") + .HasColumnType("TEXT"); + + b.Property("Schema") + .HasColumnType("TEXT"); + + b.Property("Unit") + .HasColumnType("TEXT"); + + b.Property("UseDefault") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("CampaignTemplateUniqueId"); + + b.HasIndex("CommandMetadataUniqueId"); + + b.HasIndex("ParameterId") + .IsUnique(); + + b.ToTable("ParameterMetadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("ExperimentTemplateUniqueId") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("IsParallel") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("ExperimentTemplateUniqueId"); + + b.ToTable("StepTemplates", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.DeviceVisualizationConfig", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ChartTitle") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("GridH") + .HasColumnType("INTEGER"); + + b.Property("GridW") + .HasColumnType("INTEGER"); + + b.Property("GridX") + .HasColumnType("INTEGER"); + + b.Property("GridY") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("NumberDisplayPoints") + .HasColumnType("INTEGER"); + + b.Property("Paths") + .HasColumnType("TEXT"); + + b.Property("PollingRate") + .HasColumnType("INTEGER"); + + b.Property("ShowDataLabels") + .HasColumnType("INTEGER"); + + b.Property("ShowMarkers") + .HasColumnType("INTEGER"); + + b.Property("Style") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceVisualizationConfigs"); + }); + + modelBuilder.Entity("Ares.Datamodel.Visualizing.Local.VisualizationPath", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AssociatedDeviceId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DataType") + .HasColumnType("INTEGER"); + + b.Property("IsPlottable") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("VisualizationPath"); + }); + + modelBuilder.Entity("Ares.Services.DriverInfo", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("DriverId") + .HasColumnType("TEXT"); + + b.Property("FileSizeBytes") + .HasColumnType("INTEGER"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.ToTable("DeviceDrivers"); + }); + + modelBuilder.Entity("Ares.Datamodel.AnalysisOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithOne("AnalysisOverview") + .HasForeignKey("Ares.Datamodel.AnalysisOverview", "ExperimentOverviewId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerCapabilities", b => + { + b.HasOne("Ares.Datamodel.Analyzing.AnalyzerInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Analyzing.AnalyzerCapabilities", "AnalyzerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommandVersion", null) + .WithMany("InputParameters") + .HasForeignKey("CustomCommandVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + .WithMany() + .HasForeignKey("CustomCommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "CloseoutExecutionSummary") + .WithMany() + .HasForeignKey("CloseoutExecutionSummaryUniqueId"); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "StartupExecutionSummary") + .WithMany() + .HasForeignKey("StartupExecutionSummaryUniqueId"); + + b.Navigation("CloseoutExecutionSummary"); + + b.Navigation("StartupExecutionSummary"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionStatus", b => + { + b.HasOne("Ares.Datamodel.StepExecutionStatus", null) + .WithMany("CommandExecutionStatuses") + .HasForeignKey("StepExecutionStatusUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithMany("CommandSummaries") + .HasForeignKey("StepExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandResult", b => + { + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("Result") + .HasForeignKey("Ares.Datamodel.CommandResult", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceCommandDescriptor", b => + { + b.HasOne("Ares.Datamodel.Device.DeviceInfo", null) + .WithMany("Commands") + .HasForeignKey("DeviceInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceConfig", b => + { + b.HasOne("Ares.Datamodel.Device.SerialConnection", "SerialInfo") + .WithMany() + .HasForeignKey("SerialInfoUniqueId"); + + b.Navigation("SerialInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExecutionInfo", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CampaignExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.CommandExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "CommandExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "ExperimentResultId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.StepExecutionSummary", null) + .WithOne("ExecutionInfo") + .HasForeignKey("Ares.Datamodel.ExecutionInfo", "StepExecutionSummaryId") + .OnDelete(DeleteBehavior.ClientCascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.CampaignExecutionSummary", null) + .WithMany("ExperimentSummaries") + .HasForeignKey("CampaignExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithOne("ExperimentOverview") + .HasForeignKey("Ares.Datamodel.ExperimentOverview", "ExperimentResultId"); + + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", "Template") + .WithMany() + .HasForeignKey("TemplateUniqueId"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("Ares.Datamodel.Limits", b => + { + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", null) + .WithMany("Constraints") + .HasForeignKey("ParameterMetadataUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.Planner", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceCapabilities", null) + .WithMany("AvailablePlanners") + .HasForeignKey("PlannerServiceCapabilitiesUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerAllocation", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannerAllocations") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Ares.Datamodel.Templates.ParameterMetadata", "Parameter") + .WithMany() + .HasForeignKey("ParameterUniqueId"); + + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", "Planner") + .WithMany() + .HasForeignKey("PlannerId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.Navigation("Parameter"); + + b.Navigation("Planner"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.HasOne("Ares.Datamodel.Planning.PlannerServiceInfo", null) + .WithOne("Capabilities") + .HasForeignKey("Ares.Datamodel.Planning.PlannerServiceCapabilities", "PlannerInfoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", null) + .WithMany("StepSummaries") + .HasForeignKey("ExperimentExecutionSummaryUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.DeviceCommand", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "DeviceCommandId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.StepTemplate", null) + .WithMany("CommandTemplates") + .HasForeignKey("StepTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Ares.Datamodel.Templates.CustomCommandInvocation", "CustomCommandInvocation", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("TEXT"); + + b1.Property("CustomCommandId") + .HasColumnType("TEXT"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.OwnsOne("Ares.Datamodel.Templates.SystemCommand", "SystemCommand", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("TEXT"); + + b1.Property("Operation") + .HasColumnType("INTEGER"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.Navigation("CustomCommandInvocation"); + + b.Navigation("SystemCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithOne("DeviceCommand") + .HasForeignKey("Ares.Datamodel.Templates.DeviceCommand", "CommandTemplateId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("CloseoutTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignCloseoutId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("ExperimentTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignExperimentId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithOne("StartupTemplate") + .HasForeignKey("Ares.Datamodel.Templates.ExperimentTemplate", "CampaignStartupId") + .OnDelete(DeleteBehavior.NoAction); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.OutputMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithOne("OutputMetadata") + .HasForeignKey("Ares.Datamodel.Templates.OutputMetadata", "UniqueId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithMany("ArgumentBindings") + .HasForeignKey("CommandTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Ares.Datamodel.ExperimentOverview", null) + .WithMany("Parameters") + .HasForeignKey("ExperimentOverviewUniqueId"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.HasOne("Ares.Datamodel.Templates.CampaignTemplate", null) + .WithMany("PlannableParameters") + .HasForeignKey("CampaignTemplateUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.CommandMetadata", null) + .WithMany("ParameterMetadatas") + .HasForeignKey("CommandMetadataUniqueId") + .OnDelete(DeleteBehavior.ClientCascade); + + b.HasOne("Ares.Datamodel.Templates.Parameter", null) + .WithOne("Metadata") + .HasForeignKey("Ares.Datamodel.Templates.ParameterMetadata", "ParameterId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.HasOne("Ares.Datamodel.Templates.ExperimentTemplate", null) + .WithMany("StepTemplates") + .HasForeignKey("ExperimentTemplateUniqueId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Ares.Datamodel.Analyzing.AnalyzerInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Navigation("InputParameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.CommandExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("Result"); + }); + + modelBuilder.Entity("Ares.Datamodel.Device.DeviceInfo", b => + { + b.Navigation("Commands"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentExecutionSummary", b => + { + b.Navigation("ExecutionInfo"); + + b.Navigation("ExperimentOverview"); + + b.Navigation("StepSummaries"); + }); + + modelBuilder.Entity("Ares.Datamodel.ExperimentOverview", b => + { + b.Navigation("AnalysisOverview"); + + b.Navigation("Parameters"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceCapabilities", b => + { + b.Navigation("AvailablePlanners"); + }); + + modelBuilder.Entity("Ares.Datamodel.Planning.PlannerServiceInfo", b => + { + b.Navigation("Capabilities"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionStatus", b => + { + b.Navigation("CommandExecutionStatuses"); + }); + + modelBuilder.Entity("Ares.Datamodel.StepExecutionSummary", b => + { + b.Navigation("CommandSummaries"); + + b.Navigation("ExecutionInfo"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CampaignTemplate", b => + { + b.Navigation("CloseoutTemplate"); + + b.Navigation("ExperimentTemplate"); + + b.Navigation("PlannableParameters"); + + b.Navigation("PlannerAllocations"); + + b.Navigation("StartupTemplate"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => + { + b.Navigation("OutputMetadata"); + + b.Navigation("ParameterMetadatas"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => + { + b.Navigation("ArgumentBindings"); + + b.Navigation("DeviceCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => + { + b.Navigation("StepTemplates"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => + { + b.Navigation("Metadata"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.ParameterMetadata", b => + { + b.Navigation("Constraints"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.StepTemplate", b => + { + b.Navigation("CommandTemplates"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.Sqlite/Migrations/20260710160722_AddCustomCommands_AresDbContext.cs b/AresService.Migrations.Sqlite/Migrations/20260710160722_AddCustomCommands_AresDbContext.cs new file mode 100644 index 00000000..ac4b8ecf --- /dev/null +++ b/AresService.Migrations.Sqlite/Migrations/20260710160722_AddCustomCommands_AresDbContext.cs @@ -0,0 +1,283 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.Sqlite.Migrations.AresDb +{ + /// + public partial class AddCustomCommands_AresDbContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_CommandMetadata_CommandTemplates_CommandTemplateId", + table: "CommandMetadata"); + + migrationBuilder.DropForeignKey( + name: "FK_CustomCommandParameters_CustomCommands_CustomCommandId", + table: "CustomCommandParameters"); + + migrationBuilder.DropIndex( + name: "IX_CommandMetadata_CommandTemplateId", + table: "CommandMetadata"); + + migrationBuilder.DropColumn( + name: "Description", + table: "CustomCommands"); + + migrationBuilder.DropColumn( + name: "Name", + table: "CustomCommands"); + + migrationBuilder.DropColumn( + name: "OutputSchema", + table: "CustomCommands"); + + migrationBuilder.RenameColumn( + name: "ScriptBody", + table: "CustomCommands", + newName: "CurrentVersionId"); + + migrationBuilder.RenameColumn( + name: "CustomCommandId", + table: "CustomCommandParameters", + newName: "CustomCommandVersionId"); + + migrationBuilder.RenameIndex( + name: "IX_CustomCommandParameters_CustomCommandId", + table: "CustomCommandParameters", + newName: "IX_CustomCommandParameters_CustomCommandVersionId"); + + migrationBuilder.AddColumn( + name: "CustomCommandInvocation_CustomCommandId", + table: "CommandTemplates", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "SystemCommand_Operation", + table: "CommandTemplates", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "DeviceCommandId", + table: "CommandMetadata", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateTable( + name: "CustomCommandVersions", + columns: table => new + { + UniqueId = table.Column(type: "TEXT", nullable: false), + CustomCommandId = table.Column(type: "TEXT", nullable: false), + VersionNumber = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + Description = table.Column(type: "TEXT", nullable: true), + OutputSchema = table.Column(type: "TEXT", nullable: true), + ScriptBody = table.Column(type: "TEXT", nullable: true), + CreationTime = table.Column(type: "TEXT", nullable: false, defaultValueSql: "DATETIME('now')"), + LastModified = table.Column(type: "TEXT", nullable: false, defaultValueSql: "DATETIME('now')") + }, + constraints: table => + { + table.PrimaryKey("PK_CustomCommandVersions", x => x.UniqueId); + table.ForeignKey( + name: "FK_CustomCommandVersions_CustomCommands_CustomCommandId", + column: x => x.CustomCommandId, + principalTable: "CustomCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "DeviceCommands", + columns: table => new + { + UniqueId = table.Column(type: "TEXT", nullable: false), + CommandTemplateId = table.Column(type: "TEXT", nullable: true), + CreationTime = table.Column(type: "TEXT", nullable: false, defaultValueSql: "DATETIME('now')"), + LastModified = table.Column(type: "TEXT", nullable: false, defaultValueSql: "DATETIME('now')") + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceCommands", x => x.UniqueId); + table.ForeignKey( + name: "FK_DeviceCommands_CommandTemplates_CommandTemplateId", + column: x => x.CommandTemplateId, + principalTable: "CommandTemplates", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql( + """ + INSERT INTO "DeviceCommands" ("UniqueId", "CommandTemplateId") + SELECT "UniqueId", "UniqueId" + FROM "CommandTemplates"; + + UPDATE "CommandMetadata" + SET "DeviceCommandId" = "CommandTemplateId"; + """); + + migrationBuilder.DropColumn( + name: "CommandTemplateId", + table: "CommandMetadata"); + + migrationBuilder.CreateIndex( + name: "IX_CommandMetadata_DeviceCommandId", + table: "CommandMetadata", + column: "DeviceCommandId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CustomCommandVersions_CustomCommandId_VersionNumber", + table: "CustomCommandVersions", + columns: new[] { "CustomCommandId", "VersionNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DeviceCommands_CommandTemplateId", + table: "DeviceCommands", + column: "CommandTemplateId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_CommandMetadata_DeviceCommands_DeviceCommandId", + table: "CommandMetadata", + column: "DeviceCommandId", + principalTable: "DeviceCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_CustomCommandParameters_CustomCommandVersions_CustomCommandVersionId", + table: "CustomCommandParameters", + column: "CustomCommandVersionId", + principalTable: "CustomCommandVersions", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_CommandMetadata_DeviceCommands_DeviceCommandId", + table: "CommandMetadata"); + + migrationBuilder.DropForeignKey( + name: "FK_CustomCommandParameters_CustomCommandVersions_CustomCommandVersionId", + table: "CustomCommandParameters"); + + migrationBuilder.DropTable( + name: "CustomCommandVersions"); + + migrationBuilder.DropIndex( + name: "IX_CommandMetadata_DeviceCommandId", + table: "CommandMetadata"); + + migrationBuilder.DropColumn( + name: "CustomCommandInvocation_CustomCommandId", + table: "CommandTemplates"); + + migrationBuilder.DropColumn( + name: "SystemCommand_Operation", + table: "CommandTemplates"); + + migrationBuilder.RenameColumn( + name: "CurrentVersionId", + table: "CustomCommands", + newName: "ScriptBody"); + + migrationBuilder.RenameColumn( + name: "CustomCommandVersionId", + table: "CustomCommandParameters", + newName: "CustomCommandId"); + + migrationBuilder.RenameIndex( + name: "IX_CustomCommandParameters_CustomCommandVersionId", + table: "CustomCommandParameters", + newName: "IX_CustomCommandParameters_CustomCommandId"); + + migrationBuilder.AddColumn( + name: "Description", + table: "CustomCommands", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "Name", + table: "CustomCommands", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "OutputSchema", + table: "CustomCommands", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "CommandTemplateId", + table: "CommandMetadata", + type: "TEXT", + nullable: true); + + migrationBuilder.Sql( + """ + UPDATE "CommandMetadata" + SET "CommandTemplateId" = ( + SELECT "CommandTemplateId" + FROM "DeviceCommands" + WHERE "DeviceCommands"."UniqueId" = "CommandMetadata"."DeviceCommandId" + ); + """); + + migrationBuilder.DropColumn( + name: "DeviceCommandId", + table: "CommandMetadata"); + + migrationBuilder.Sql("PRAGMA foreign_keys = 0;", suppressTransaction: true); + + migrationBuilder.DropTable( + name: "DeviceCommands"); + + migrationBuilder.AlterColumn( + name: "CommandTemplateId", + table: "CommandMetadata", + type: "TEXT", + nullable: false, + oldClrType: typeof(Guid), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_CommandMetadata_CommandTemplateId", + table: "CommandMetadata", + column: "CommandTemplateId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_CommandMetadata_CommandTemplates_CommandTemplateId", + table: "CommandMetadata", + column: "CommandTemplateId", + principalTable: "CommandTemplates", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_CustomCommandParameters_CustomCommands_CustomCommandId", + table: "CustomCommandParameters", + column: "CustomCommandId", + principalTable: "CustomCommands", + principalColumn: "UniqueId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.Sql("PRAGMA foreign_keys = 1;", suppressTransaction: true); + } + } +} diff --git a/AresService.Migrations.Sqlite/Migrations/20260710160723_AddCustomCommands_AresIdentityContext.Designer.cs b/AresService.Migrations.Sqlite/Migrations/20260710160723_AddCustomCommands_AresIdentityContext.Designer.cs new file mode 100644 index 00000000..fbf9bd1f --- /dev/null +++ b/AresService.Migrations.Sqlite/Migrations/20260710160723_AddCustomCommands_AresIdentityContext.Designer.cs @@ -0,0 +1,268 @@ +// +using System; +using AresService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AresService.Migrations.Sqlite.Migrations.AresIdentity +{ + [DbContext(typeof(AresIdentityContext))] + [Migration("20260710160723_AddCustomCommands_AresIdentityContext")] + partial class AddCustomCommands_AresIdentityContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.8"); + + modelBuilder.Entity("AresService.Data.AresUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AresService.Data.AresUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AresService.Migrations.Sqlite/Migrations/20260710160723_AddCustomCommands_AresIdentityContext.cs b/AresService.Migrations.Sqlite/Migrations/20260710160723_AddCustomCommands_AresIdentityContext.cs new file mode 100644 index 00000000..cb1751d2 --- /dev/null +++ b/AresService.Migrations.Sqlite/Migrations/20260710160723_AddCustomCommands_AresIdentityContext.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AresService.Migrations.Sqlite.Migrations.AresIdentity +{ + /// + public partial class AddCustomCommands_AresIdentityContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/AresService.Migrations.Sqlite/Migrations/AresDbContextModelSnapshot.cs b/AresService.Migrations.Sqlite/Migrations/AresDbContextModelSnapshot.cs index 9c9b63cc..683b7476 100644 --- a/AresService.Migrations.Sqlite/Migrations/AresDbContextModelSnapshot.cs +++ b/AresService.Migrations.Sqlite/Migrations/AresDbContextModelSnapshot.cs @@ -305,6 +305,104 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AresGeneralSettingsConfig", (string)null); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("CurrentVersionId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.HasKey("UniqueId"); + + b.ToTable("CustomCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("CustomCommandVersionId") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Schema") + .HasColumnType("TEXT"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandVersionId"); + + b.ToTable("CustomCommandParameters", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("CustomCommandId") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OutputSchema") + .HasColumnType("TEXT"); + + b.Property("ScriptBody") + .HasColumnType("TEXT"); + + b.Property("VersionNumber") + .HasColumnType("INTEGER"); + + b.HasKey("UniqueId"); + + b.HasIndex("CustomCommandId", "VersionNumber") + .IsUnique(); + + b.ToTable("CustomCommandVersions", (string)null); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.Property("UniqueId") @@ -1291,9 +1389,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.Property("CommandTemplateId") - .HasColumnType("TEXT"); - b.Property("CreationTime") .ValueGeneratedOnAdd() .HasColumnType("TEXT") @@ -1302,6 +1397,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description") .HasColumnType("TEXT"); + b.Property("DeviceCommandId") + .HasColumnType("TEXT"); + b.Property("DeviceId") .HasColumnType("TEXT"); @@ -1318,7 +1416,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("UniqueId"); - b.HasIndex("CommandTemplateId") + b.HasIndex("DeviceCommandId") .IsUnique(); b.ToTable("CommandMetadata"); @@ -1356,6 +1454,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("CommandTemplates", (string)null); }); + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CommandTemplateId") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.HasKey("UniqueId"); + + b.HasIndex("CommandTemplateId") + .IsUnique(); + + b.ToTable("DeviceCommands", (string)null); + }); + modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => { b.Property("UniqueId") @@ -1711,6 +1836,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommandVersion", null) + .WithMany("InputParameters") + .HasForeignKey("CustomCommandVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + .WithMany() + .HasForeignKey("CustomCommandId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.HasOne("Ares.Datamodel.ExperimentExecutionSummary", "CloseoutExecutionSummary") @@ -1870,11 +2013,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Templates.CommandMetadata", b => { - b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + b.HasOne("Ares.Datamodel.Templates.DeviceCommand", null) .WithOne("Metadata") - .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "CommandTemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("Ares.Datamodel.Templates.CommandMetadata", "DeviceCommandId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => @@ -1884,6 +2026,50 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("StepTemplateUniqueId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.OwnsOne("Ares.Datamodel.Templates.CustomCommandInvocation", "CustomCommandInvocation", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("TEXT"); + + b1.Property("CustomCommandId") + .HasColumnType("TEXT"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.OwnsOne("Ares.Datamodel.Templates.SystemCommand", "SystemCommand", b1 => + { + b1.Property("CommandTemplateUniqueId") + .HasColumnType("TEXT"); + + b1.Property("Operation") + .HasColumnType("INTEGER"); + + b1.HasKey("CommandTemplateUniqueId"); + + b1.ToTable("CommandTemplates"); + + b1.WithOwner() + .HasForeignKey("CommandTemplateUniqueId"); + }); + + b.Navigation("CustomCommandInvocation"); + + b.Navigation("SystemCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) + .WithOne("DeviceCommand") + .HasForeignKey("Ares.Datamodel.Templates.DeviceCommand", "CommandTemplateId") + .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => @@ -1916,7 +2102,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Templates.Parameter", b => { b.HasOne("Ares.Datamodel.Templates.CommandTemplate", null) - .WithMany("Parameters") + .WithMany("ArgumentBindings") .HasForeignKey("CommandTemplateUniqueId") .OnDelete(DeleteBehavior.Cascade); @@ -1956,6 +2142,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Capabilities"); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => + { + b.Navigation("InputParameters"); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.Navigation("ExecutionInfo"); @@ -2035,9 +2226,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Templates.CommandTemplate", b => { - b.Navigation("Metadata"); + b.Navigation("ArgumentBindings"); - b.Navigation("Parameters"); + b.Navigation("DeviceCommand"); + }); + + modelBuilder.Entity("Ares.Datamodel.Templates.DeviceCommand", b => + { + b.Navigation("Metadata"); }); modelBuilder.Entity("Ares.Datamodel.Templates.ExperimentTemplate", b => diff --git a/DemoRemoteAnalyzer/DemoRemoteAnalyzer.csproj b/DemoRemoteAnalyzer/DemoRemoteAnalyzer.csproj index 4e2d795c..1e6c285c 100644 --- a/DemoRemoteAnalyzer/DemoRemoteAnalyzer.csproj +++ b/DemoRemoteAnalyzer/DemoRemoteAnalyzer.csproj @@ -7,7 +7,7 @@ - + diff --git a/DemoRemoteDevice/DemoRemoteDevice.csproj b/DemoRemoteDevice/DemoRemoteDevice.csproj index cb54850d..257a8834 100644 --- a/DemoRemoteDevice/DemoRemoteDevice.csproj +++ b/DemoRemoteDevice/DemoRemoteDevice.csproj @@ -7,7 +7,7 @@ - + diff --git a/DemoRemotePlanner/DemoRemotePlanner.csproj b/DemoRemotePlanner/DemoRemotePlanner.csproj index 4e2d795c..1e6c285c 100644 --- a/DemoRemotePlanner/DemoRemotePlanner.csproj +++ b/DemoRemotePlanner/DemoRemotePlanner.csproj @@ -7,7 +7,7 @@ - + diff --git a/Directory.Build.targets b/Directory.Build.targets index 97f323f2..77454bae 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -3,4 +3,10 @@ $(NoWarn);IDE0305;IDE0290;3270;CA1860 true - \ No newline at end of file + + + + + + diff --git a/UI.Tests/Features/CampaignEdit/ViewModels/CampaignListViewModelTests.cs b/UI.Tests/Features/CampaignEdit/ViewModels/CampaignListViewModelTests.cs new file mode 100644 index 00000000..06f5ec2f --- /dev/null +++ b/UI.Tests/Features/CampaignEdit/ViewModels/CampaignListViewModelTests.cs @@ -0,0 +1,40 @@ +using Ares.Core.Campaigns; +using Ares.Datamodel.Templates; +using Microsoft.AspNetCore.Components.Forms; +using Moq; +using System.Text; +using UI.Features.CampaignEdit.ViewModels; + +namespace UI.Tests.Features.CampaignEdit.ViewModels; + +internal class CampaignListViewModelTests +{ + [Test] + public void ImportCampaignTemplate_RejectsFilesOverFiveMiB() + { + var file = new Mock(); + file.SetupGet(value => value.Size).Returns(CampaignListViewModel.MaximumImportFileSize + 1); + var viewModel = new CampaignListViewModel(null!, Mock.Of()); + + Assert.ThrowsAsync(() => viewModel.ImportCampaignTemplate(file.Object)); + } + + [Test] + public async Task ImportCampaignTemplate_PassesFileContentsToTransferService() + { + const string json = "{\"name\":\"Campaign\"}"; + var file = new Mock(); + file.SetupGet(value => value.Size).Returns(Encoding.UTF8.GetByteCount(json)); + file.Setup(value => value.OpenReadStream(CampaignListViewModel.MaximumImportFileSize, It.IsAny())) + .Returns(new MemoryStream(Encoding.UTF8.GetBytes(json))); + var transfer = new Mock(); + transfer.Setup(service => service.ImportAsync(json, It.IsAny())) + .ReturnsAsync(new CampaignTemplateImportResult(new CampaignTemplate { Name = "Campaign" }, [])); + var viewModel = new CampaignListViewModel(null!, transfer.Object); + + var result = await viewModel.ImportCampaignTemplate(file.Object); + + Assert.That(result.Template.Name, Is.EqualTo("Campaign")); + transfer.Verify(service => service.ImportAsync(json, It.IsAny()), Times.Once); + } +} diff --git a/UI.Tests/Features/CampaignEdit/ViewModels/CommandOutputVariableReferenceBuilderTests.cs b/UI.Tests/Features/CampaignEdit/ViewModels/CommandOutputVariableReferenceBuilderTests.cs new file mode 100644 index 00000000..6d25816b --- /dev/null +++ b/UI.Tests/Features/CampaignEdit/ViewModels/CommandOutputVariableReferenceBuilderTests.cs @@ -0,0 +1,145 @@ +using Ares.Datamodel; +using Ares.Datamodel.Templates; +using UI.Features.CampaignEdit.ViewModels; + +namespace UI.Tests.Features.CampaignEdit.ViewModels; + +public class CommandOutputVariableReferenceBuilderTests +{ + private static readonly IReadOnlyDictionary NoCustomCommandSchemas + = new Dictionary(StringComparer.OrdinalIgnoreCase); + + [Test] + public void Build_CustomCommandWithScalarOutput_ReturnsAssignedVariable() + { + var template = CreateCustomCommandTemplate("command-id", "measurement"); + var schemas = CreateSchemaLookup(("COMMAND-ID", new AresValueSchema { Type = AresDataType.Number })); + + var references = CommandOutputVariableReferenceBuilder.Build(template, schemas); + + Assert.That(references, Is.EqualTo(new[] + { + new CommandOutputVariableReference("measurement", AresDataType.Number) + })); + } + + [Test] + public void Build_CustomCommandWithNestedStructOutput_ReturnsEveryPath() + { + var template = CreateCustomCommandTemplate("command-id", "result"); + var innerStruct = new AresStructSchema(); + innerStruct.Fields["value"] = new AresValueSchema { Type = AresDataType.Number }; + var outerStruct = new AresStructSchema(); + outerStruct.Fields["sample"] = new AresValueSchema + { + Type = AresDataType.Struct, + StructSchema = innerStruct + }; + var schemas = CreateSchemaLookup(("command-id", new AresValueSchema + { + Type = AresDataType.Struct, + StructSchema = outerStruct + })); + + var references = CommandOutputVariableReferenceBuilder.Build(template, schemas); + + Assert.That(references, Is.EqualTo(new[] + { + new CommandOutputVariableReference("result", AresDataType.Struct), + new CommandOutputVariableReference("result.sample", AresDataType.Struct), + new CommandOutputVariableReference("result.sample.value", AresDataType.Number) + })); + } + + [Test] + public void Build_CustomCommandNotInLookup_ReturnsNoReferences() + { + var template = CreateCustomCommandTemplate("missing-command", "result"); + + var references = CommandOutputVariableReferenceBuilder.Build(template, NoCustomCommandSchemas); + + Assert.That(references, Is.Empty); + } + + [TestCase(AresDataType.Unit)] + [TestCase(AresDataType.UnspecifiedType)] + public void Build_CustomCommandWithoutUsableOutput_ReturnsNoReferences(AresDataType outputType) + { + var template = CreateCustomCommandTemplate("command-id", "result"); + var schemas = CreateSchemaLookup(("command-id", new AresValueSchema { Type = outputType })); + + var references = CommandOutputVariableReferenceBuilder.Build(template, schemas); + + Assert.That(references, Is.Empty); + } + + [Test] + public void Build_TemplateWithoutOutputVariable_ReturnsNoReferences() + { + var template = CreateCustomCommandTemplate("command-id", null); + var schemas = CreateSchemaLookup(("command-id", new AresValueSchema { Type = AresDataType.Number })); + + var references = CommandOutputVariableReferenceBuilder.Build(template, schemas); + + Assert.That(references, Is.Empty); + } + + [Test] + public void Build_DeviceCommand_PreservesExistingBehavior() + { + var template = new CommandTemplate + { + OutputVarName = "device_result", + DeviceCommand = new DeviceCommand + { + Metadata = new CommandMetadata + { + OutputMetadata = new OutputMetadata + { + DataSchema = new AresValueSchema { Type = AresDataType.String } + } + } + } + }; + + var references = CommandOutputVariableReferenceBuilder.Build(template, NoCustomCommandSchemas); + + Assert.That(references, Is.EqualTo(new[] + { + new CommandOutputVariableReference("device_result", AresDataType.String) + })); + } + + [Test] + public void Build_SystemCommand_PreservesExistingBehavior() + { + var template = new CommandTemplate + { + OutputVarName = "timestamp", + SystemCommand = new SystemCommand { Operation = SystemOperation.GetTimestamp } + }; + + var references = CommandOutputVariableReferenceBuilder.Build(template, NoCustomCommandSchemas); + + Assert.That(references, Is.EqualTo(new[] + { + new CommandOutputVariableReference("timestamp", AresDataType.Timestamp) + })); + } + + private static CommandTemplate CreateCustomCommandTemplate(string customCommandId, string? outputVariableName) + { + var template = new CommandTemplate + { + CustomCommandInvocation = new CustomCommandInvocation { CustomCommandId = customCommandId } + }; + if(outputVariableName is not null) + template.OutputVarName = outputVariableName; + + return template; + } + + private static IReadOnlyDictionary CreateSchemaLookup( + params (string Id, AresValueSchema Schema)[] schemas) + => schemas.ToDictionary(schema => schema.Id, schema => schema.Schema, StringComparer.OrdinalIgnoreCase); +} diff --git a/UI.Tests/UI.Tests.csproj b/UI.Tests/UI.Tests.csproj new file mode 100644 index 00000000..36a92d12 --- /dev/null +++ b/UI.Tests/UI.Tests.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/UI.Tests/Usings.cs b/UI.Tests/Usings.cs new file mode 100644 index 00000000..32445676 --- /dev/null +++ b/UI.Tests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; diff --git a/UI/Application/Devices/Repos/DeviceControlViewModelRepo.cs b/UI/Application/Devices/Repos/DeviceControlViewModelRepo.cs index d32b0ffb..d60e49a0 100644 --- a/UI/Application/Devices/Repos/DeviceControlViewModelRepo.cs +++ b/UI/Application/Devices/Repos/DeviceControlViewModelRepo.cs @@ -1,4 +1,3 @@ -using Ares.Core.CoreDevice; using Ares.Core.Device.Providers; using Ares.Core.Device.Remote; using Ares.Toolkit.Device.UI; @@ -34,7 +33,7 @@ public DeviceControlViewModelRepo(IAresDeviceProvider deviceProvider, public void Initialize() { _deviceProvider.Connect() - .Filter(d => d is not RemoteDevice && d is not AresCoreDevice) + .Filter(d => d is not RemoteDevice) .Transform(_factory.CreateUnitControlViewModel) .DisposeMany() .PopulateInto(_viewModelCache) diff --git a/UI/Components/AresValueSchemaDesigner.razor b/UI/Components/AresValueSchemaDesigner.razor new file mode 100644 index 00000000..18162f07 --- /dev/null +++ b/UI/Components/AresValueSchemaDesigner.razor @@ -0,0 +1,649 @@ +@using Ares.Datamodel +@using Ares.Datamodel.Extensions +@using Google.Protobuf + +
+
+ +
+

@DisplayTitle

+ @SchemaSummary +
+
+ + @if(!IsCollapsed) + { +
+ @if(IsMaxDepthExceeded) + { +
+ Maximum schema nesting depth reached. +
+ } + else + { + + +
+ + + +
+ + @if(IsNumericType(_schema.Type)) + { +
+
Numeric Bounds
+
+ + + + + +
+
+ } + + @if(_schema.Type == AresDataType.Quantity) + { +
+
Quantity
+
+ + + + + + + + + +
+
+ } + + @if(SupportsStringChoices(_schema.Type)) + { +
+
+
String Choices
+ +
+ @for(var i = 0; i < StringChoices.Strings.Count; i++) + { + var index = i; +
+ + +
+ } +
+ } + + @if(SupportsNumberChoices(_schema.Type)) + { +
+
+
Number Choices
+ +
+ @for(var i = 0; i < NumberChoices.Numbers.Count; i++) + { + var index = i; +
+ + +
+ } +
+ } + + @if(_schema.Type == AresDataType.Struct) + { +
+
+
Struct Fields
+ +
+ @foreach(var field in StructFields) + { +
+
+ + +
+ +
+ } +
+ } + + @if(_schema.Type == AresDataType.List) + { +
+
List Element Schema
+ +
+ } + +
+ + + @if(HasDefaultValue) + { + @if(UseValueDesignerForDefault) + { + + } + else + { + + @if(!string.IsNullOrWhiteSpace(_defaultJsonError)) + { +
@_defaultJsonError
+ } + } + } +
+ } +
+ } +
+ +@code { + [Parameter] + public AresValueSchema? Schema { get; set; } + + [Parameter] + public EventCallback SchemaChanged { get; set; } + + [Parameter] + public string? Title { get; set; } + + [Parameter] + public bool Collapsible { get; set; } + + [Parameter] + public int MaxDepth { get; set; } = 6; + + [Parameter] + public int Depth { get; set; } + + private AresValueSchema _schema = new() { Type = AresDataType.String }; + private bool _collapsed; + private string _defaultJson = string.Empty; + private string? _defaultJsonError; + + private static readonly AresDataType[] DataTypeOptions = Enum.GetValues() + .Where(type => type != AresDataType.UnspecifiedType) + .ToArray(); + + private static readonly QuantityType[] QuantityTypeOptions = Enum.GetValues(); + + private string RootClass => $"ares-schema-designer{(Depth > 0 ? " ares-schema-designer--nested" : string.Empty)}"; + private string DisplayTitle => string.IsNullOrWhiteSpace(Title) ? "Schema" : Title; + private string CollapseIcon => !Collapsible ? "unfold_more" : _collapsed ? "chevron_right" : "expand_more"; + private string CollapseTitle => !Collapsible ? "Schema details" : _collapsed ? "Expand schema" : "Collapse schema"; + private bool IsCollapsed => Collapsible && _collapsed; + private bool IsMaxDepthExceeded => Depth > MaxDepth; + private bool HasDefaultValue => _schema.DefaultValue is not null; + private string SchemaSummary => $"{GetDataTypeLabel(_schema.Type)}{(_schema.Optional ? ", optional" : string.Empty)}"; + + private QuantitySchema QuantitySchema + { + get + { + _schema.QuantitySchema ??= new QuantitySchema(); + return _schema.QuantitySchema; + } + } + + private StringArray StringChoices + { + get + { + if(_schema.AvailableChoicesCase != AresValueSchema.AvailableChoicesOneofCase.StringChoices) + _schema.StringChoices = new StringArray(); + return _schema.StringChoices; + } + } + + private NumberArray NumberChoices + { + get + { + if(_schema.AvailableChoicesCase != AresValueSchema.AvailableChoicesOneofCase.NumberChoices) + _schema.NumberChoices = new NumberArray(); + return _schema.NumberChoices; + } + } + + private AresStructSchema StructSchema + { + get + { + _schema.StructSchema ??= new AresStructSchema(); + return _schema.StructSchema; + } + } + + private AresValueSchema ListElementSchema + { + get + { + _schema.ListElementSchema ??= new AresValueSchema { Type = AresDataType.String }; + return _schema.ListElementSchema; + } + } + + private IReadOnlyList StructFields => StructSchema.Fields + .Select(entry => new StructField(entry.Key, entry.Value)) + .ToArray(); + + private bool UseValueDesignerForDefault => _schema.Type is + AresDataType.Boolean or + AresDataType.String or + AresDataType.Number or + AresDataType.Quantity or + AresDataType.StringArray or + AresDataType.NumberArray; + + protected override void OnParametersSet() + { + _schema = Schema?.Clone() ?? new AresValueSchema { Type = AresDataType.String }; + EnsureTypeState(_schema.Type, clearExisting: false); + RefreshDefaultJson(); + } + + private void ToggleCollapsed() + { + if(Collapsible) + _collapsed = !_collapsed; + } + + private async Task EmitChanged() + { + RefreshDefaultJson(); + await SchemaChanged.InvokeAsync(_schema.Clone()); + } + + private async Task OnTypeChanged(ChangeEventArgs args) + { + if(Enum.TryParse(args.Value?.ToString(), out var type)) + { + _schema.Type = type; + EnsureTypeState(type, clearExisting: true); + await EmitChanged(); + } + } + + private async Task OnOptionalChanged(ChangeEventArgs args) + { + _schema.Optional = args.Value is bool value && value; + await EmitChanged(); + } + + private async Task OnDescriptionChanged(ChangeEventArgs args) + { + _schema.Description = args.Value?.ToString() ?? string.Empty; + await EmitChanged(); + } + + private async Task OnHasMinNumberChanged(ChangeEventArgs args) + { + if(args.Value is bool value && value) + _schema.MinNumberValue = _schema.HasMinNumberValue ? _schema.MinNumberValue : 0; + else + _schema.ClearMinNumberValue(); + await EmitChanged(); + } + + private async Task OnHasMaxNumberChanged(ChangeEventArgs args) + { + if(args.Value is bool value && value) + _schema.MaxNumberValue = _schema.HasMaxNumberValue ? _schema.MaxNumberValue : 0; + else + _schema.ClearMaxNumberValue(); + await EmitChanged(); + } + + private async Task OnMinNumberChanged(ChangeEventArgs args) + { + if(double.TryParse(args.Value?.ToString(), out var value)) + _schema.MinNumberValue = value; + await EmitChanged(); + } + + private async Task OnMaxNumberChanged(ChangeEventArgs args) + { + if(double.TryParse(args.Value?.ToString(), out var value)) + _schema.MaxNumberValue = value; + await EmitChanged(); + } + + private async Task OnQuantityTypeChanged(ChangeEventArgs args) + { + if(Enum.TryParse(args.Value?.ToString(), out var type)) + QuantitySchema.QuantityType = type; + await EmitChanged(); + } + + private async Task OnQuantityBoundsUnitChanged(ChangeEventArgs args) + { + QuantitySchema.BoundsUnit = args.Value?.ToString() ?? string.Empty; + await EmitChanged(); + } + + private async Task OnHasMinScalarChanged(ChangeEventArgs args) + { + if(args.Value is bool value && value) + QuantitySchema.MinScalarValue = QuantitySchema.HasMinScalarValue ? QuantitySchema.MinScalarValue : 0; + else + QuantitySchema.ClearMinScalarValue(); + await EmitChanged(); + } + + private async Task OnHasMaxScalarChanged(ChangeEventArgs args) + { + if(args.Value is bool value && value) + QuantitySchema.MaxScalarValue = QuantitySchema.HasMaxScalarValue ? QuantitySchema.MaxScalarValue : 0; + else + QuantitySchema.ClearMaxScalarValue(); + await EmitChanged(); + } + + private async Task OnMinScalarChanged(ChangeEventArgs args) + { + if(double.TryParse(args.Value?.ToString(), out var value)) + QuantitySchema.MinScalarValue = value; + await EmitChanged(); + } + + private async Task OnMaxScalarChanged(ChangeEventArgs args) + { + if(double.TryParse(args.Value?.ToString(), out var value)) + QuantitySchema.MaxScalarValue = value; + await EmitChanged(); + } + + private async Task AddStringChoice() + { + StringChoices.Strings.Add(string.Empty); + await EmitChanged(); + } + + private async Task OnStringChoiceChanged(int index, ChangeEventArgs args) + { + if(index >= 0 && index < StringChoices.Strings.Count) + StringChoices.Strings[index] = args.Value?.ToString() ?? string.Empty; + await EmitChanged(); + } + + private async Task RemoveStringChoice(int index) + { + if(index >= 0 && index < StringChoices.Strings.Count) + StringChoices.Strings.RemoveAt(index); + await EmitChanged(); + } + + private async Task AddNumberChoice() + { + NumberChoices.Numbers.Add(0); + await EmitChanged(); + } + + private async Task OnNumberChoiceChanged(int index, ChangeEventArgs args) + { + if(index >= 0 && index < NumberChoices.Numbers.Count && double.TryParse(args.Value?.ToString(), out var value)) + NumberChoices.Numbers[index] = value; + await EmitChanged(); + } + + private async Task RemoveNumberChoice(int index) + { + if(index >= 0 && index < NumberChoices.Numbers.Count) + NumberChoices.Numbers.RemoveAt(index); + await EmitChanged(); + } + + private async Task AddStructField() + { + var name = GetNextFieldName(); + StructSchema.Fields[name] = new AresValueSchema { Type = AresDataType.String }; + await EmitChanged(); + } + + private async Task RenameStructField(string oldName, ChangeEventArgs args) + { + var newName = args.Value?.ToString()?.Trim() ?? string.Empty; + if(string.IsNullOrWhiteSpace(newName) || newName == oldName || StructSchema.Fields.ContainsKey(newName)) + return; + + var reorderedFields = StructSchema.Fields + .Select(entry => entry.Key == oldName + ? new KeyValuePair(newName, entry.Value) + : entry) + .ToArray(); + + StructSchema.Fields.Clear(); + foreach(var field in reorderedFields) + StructSchema.Fields[field.Key] = field.Value; + + await EmitChanged(); + } + + private async Task RemoveStructField(string name) + { + StructSchema.Fields.Remove(name); + await EmitChanged(); + } + + private async Task OnStructFieldSchemaChanged(string name, AresValueSchema schema) + { + if(StructSchema.Fields.ContainsKey(name)) + StructSchema.Fields[name] = schema.Clone(); + await EmitChanged(); + } + + private async Task OnListElementSchemaChanged(AresValueSchema schema) + { + _schema.ListElementSchema = schema.Clone(); + await EmitChanged(); + } + + private async Task OnHasDefaultValueChanged(ChangeEventArgs args) + { + if(args.Value is bool value && value) + _schema.DefaultValue = AresValueHelper.CreateDefault(_schema.Type); + else + _schema.DefaultValue = null; + await EmitChanged(); + } + + private async Task OnDefaultValueChanged(AresValue value) + { + _schema.DefaultValue = value.Clone(); + await EmitChanged(); + } + + private async Task OnDefaultJsonChanged(ChangeEventArgs args) + { + _defaultJson = args.Value?.ToString() ?? string.Empty; + try + { + _schema.DefaultValue = JsonParser.Default.Parse(_defaultJson); + _defaultJsonError = null; + await EmitChanged(); + } + catch(Exception ex) + { + _defaultJsonError = ex.Message; + } + } + + private void EnsureTypeState(AresDataType type, bool clearExisting) + { + if(clearExisting) + { + _schema.QuantitySchema = null; + _schema.StructSchema = null; + _schema.ListElementSchema = null; + _schema.DefaultValue = null; + _schema.ClearAvailableChoices(); + _schema.ClearMinNumberValue(); + _schema.ClearMaxNumberValue(); + } + + if(type == AresDataType.Quantity) + _schema.QuantitySchema ??= new QuantitySchema(); + else if(type == AresDataType.Struct) + _schema.StructSchema ??= new AresStructSchema(); + else if(type == AresDataType.List) + _schema.ListElementSchema ??= new AresValueSchema { Type = AresDataType.String }; + + if(!SupportsStringChoices(type) && _schema.AvailableChoicesCase == AresValueSchema.AvailableChoicesOneofCase.StringChoices) + _schema.ClearAvailableChoices(); + if(!SupportsNumberChoices(type) && _schema.AvailableChoicesCase == AresValueSchema.AvailableChoicesOneofCase.NumberChoices) + _schema.ClearAvailableChoices(); + if(!IsNumericType(type)) + { + _schema.ClearMinNumberValue(); + _schema.ClearMaxNumberValue(); + } + } + + private void RefreshDefaultJson() + { + _defaultJson = _schema.DefaultValue is null ? string.Empty : JsonFormatter.Default.Format(_schema.DefaultValue); + _defaultJsonError = null; + } + + private string GetNextFieldName() + { + var index = StructSchema.Fields.Count + 1; + var name = $"field_{index}"; + while(StructSchema.Fields.ContainsKey(name)) + { + index++; + name = $"field_{index}"; + } + return name; + } + + private static bool SupportsStringChoices(AresDataType type) + => type is AresDataType.String or AresDataType.StringArray; + + private static bool SupportsNumberChoices(AresDataType type) + => type is AresDataType.Number or AresDataType.NumberArray or AresDataType.Float or AresDataType.FloatArray or AresDataType.Int or AresDataType.IntArray; + + private static bool IsNumericType(AresDataType type) + => type is AresDataType.Number or AresDataType.NumberArray or AresDataType.Float or AresDataType.FloatArray or AresDataType.Int or AresDataType.IntArray; + + private static string GetDataTypeLabel(AresDataType type) + => type.ToString(); + + private static string GetQuantityTypeLabel(QuantityType type) + => type.ToString(); + + private record StructField(string Name, AresValueSchema Schema); +} diff --git a/UI/Components/AresValueSchemaDesigner.razor.css b/UI/Components/AresValueSchemaDesigner.razor.css new file mode 100644 index 00000000..0214e5bb --- /dev/null +++ b/UI/Components/AresValueSchemaDesigner.razor.css @@ -0,0 +1,182 @@ +.ares-schema-designer { + display: flex; + flex-direction: column; + gap: 0.75rem; + min-width: 0; + overflow-x: hidden; + border: 1px solid var(--rz-base-400, #dee2e6); + border-radius: 6px; + padding: 0.75rem; +} + +.ares-schema-designer--nested { + margin-top: 0.5rem; + background: var(--rz-base-600, rgba(255, 255, 255, 0.03)); +} + +.ares-schema-designer__header { + display: flex; + align-items: flex-start; + gap: 0.5rem; + min-width: 0; +} + +.ares-schema-designer__heading { + min-width: 0; +} + +.ares-schema-designer__title { + margin: 0; + font-size: 1rem; + line-height: 1.25; +} + +.ares-schema-designer__summary { + display: block; + margin-top: 0.2rem; + color: var(--rz-text-secondary-color, #6c757d); + font-size: 0.875rem; + overflow-wrap: anywhere; +} + +.ares-schema-designer__body { + display: flex; + flex-direction: column; + gap: 0.85rem; + min-width: 0; +} + +.ares-schema-designer__grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; + min-width: 0; +} + +.ares-schema-designer__field, +.ares-schema-designer__toggle-field { + display: flex; + min-width: 0; +} + +.ares-schema-designer__field { + flex-direction: column; + gap: 0.35rem; +} + +.ares-schema-designer__toggle-field { + align-items: center; + gap: 0.5rem; +} + +.ares-schema-designer__field span, +.ares-schema-designer__toggle-field span { + font-weight: 600; +} + +.ares-schema-designer input, +.ares-schema-designer select, +.ares-schema-designer textarea { + min-width: 0; + width: 100%; + border: 1px solid var(--rz-base-400, #dee2e6); + border-radius: 6px; + background: var(--rz-input-background-color, transparent); + color: inherit; + padding: 0.45rem 0.55rem; +} + +.ares-schema-designer input[type="checkbox"] { + width: auto; +} + +.ares-schema-designer input:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.ares-schema-designer textarea { + resize: vertical; +} + +.ares-schema-designer__section { + display: flex; + flex-direction: column; + gap: 0.6rem; + min-width: 0; + border-top: 1px solid var(--rz-base-300, #ced4da); + padding-top: 0.75rem; +} + +.ares-schema-designer__section h5 { + margin: 0; + font-size: 0.95rem; +} + +.ares-schema-designer__section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + min-width: 0; +} + +.ares-schema-designer__list-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 0.5rem; + min-width: 0; +} + +.ares-schema-designer__struct-field { + min-width: 0; + border: 1px solid var(--rz-base-300, #ced4da); + border-radius: 6px; + padding: 0.6rem; +} + +.ares-schema-designer__collapse-button, +.ares-schema-designer__icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 2rem; + height: 2rem; + border: 1px solid var(--rz-base-400, #dee2e6); + border-radius: 6px; + background: transparent; + color: inherit; + cursor: pointer; +} + +.ares-schema-designer__collapse-button:disabled { + cursor: default; + opacity: 0.55; +} + +.ares-schema-designer__icon-button--danger { + color: var(--rz-danger, #dc3545); +} + +.ares-schema-designer__notice, +.ares-schema-designer__error { + border-radius: 6px; + padding: 0.6rem 0.75rem; +} + +.ares-schema-designer__notice { + background: var(--rz-warning-lighter, rgba(255, 193, 7, 0.12)); +} + +.ares-schema-designer__error { + background: var(--rz-danger-lighter, rgba(220, 53, 69, 0.12)); + color: var(--rz-danger, #dc3545); +} + +@media (max-width: 720px) { + .ares-schema-designer__grid { + grid-template-columns: 1fr; + } +} diff --git a/UI/Components/Experiments/CommandViewer.razor b/UI/Components/Experiments/CommandViewer.razor index 2cc66b76..c1123baa 100644 --- a/UI/Components/Experiments/CommandViewer.razor +++ b/UI/Components/Experiments/CommandViewer.razor @@ -9,20 +9,20 @@ @if (CommandTemplate is not null) { - if (CommandTemplate.Metadata?.Name is not null) + if (CommandTemplate.DeviceCommand?.Metadata?.Name is not null) {
@(DeviceName ?? "UNKNOWN DEVICE")
- @CommandTemplate.Metadata.Name + @CommandTemplate.DeviceCommand.Metadata.Name
- @if (CommandTemplate.Parameters.Any()) + @if (CommandTemplate.ArgumentBindings.Any()) { } - @foreach (var argument in CommandTemplate.Parameters) + @foreach (var argument in CommandTemplate.ArgumentBindings) { var assignmentError = GetParameterAssignmentError(argument); @if (assignmentError is not null) @@ -138,7 +138,7 @@ protected override Task OnInitializedAsync() { if(CommandTemplate is not null) - DeviceName = DeviceProvider.GetDevice(CommandTemplate.Metadata.DeviceId)?.Name ?? string.Empty; + DeviceName = DeviceProvider.GetDevice(CommandTemplate.DeviceCommand?.Metadata?.DeviceId ?? "")?.Name ?? string.Empty; return base.OnInitializedAsync(); } diff --git a/UI/Components/Experiments/ExperimentViewer.razor b/UI/Components/Experiments/ExperimentViewer.razor index 573b4834..0caa3d40 100644 --- a/UI/Components/Experiments/ExperimentViewer.razor +++ b/UI/Components/Experiments/ExperimentViewer.razor @@ -1,9 +1,16 @@ @inject DialogService DialogService +@inject Ares.Core.CustomCommands.ICustomCommandPersistenceService CustomCommandService +@using Ares.Datamodel @using Ares.Datamodel.Templates @using UI.Features.CampaignEdit.ViewModels @if (CampaignTemplate is not null) { + @if(!string.IsNullOrWhiteSpace(_customCommandLoadError)) + { + + } +
@@ -13,6 +20,7 @@
@@ -25,6 +33,7 @@
@@ -36,6 +45,7 @@
@@ -49,7 +59,29 @@ [Parameter] public CampaignTemplate? CampaignTemplate { get; set; } - private static CommandOutputVariableReference[] GetPriorVariableReferences( + private IReadOnlyDictionary _customCommandOutputSchemas + = new Dictionary(StringComparer.OrdinalIgnoreCase); + private string? _customCommandLoadError; + + protected override async Task OnInitializedAsync() + { + try + { + _customCommandOutputSchemas = (await CustomCommandService.GetCommandsAsync()) + .Where(command => !string.IsNullOrWhiteSpace(command.CustomCommandId) && command.OutputSchema is not null) + .ToDictionary( + command => command.CustomCommandId, + command => command.OutputSchema, + StringComparer.OrdinalIgnoreCase); + } + catch(Exception exception) + { + _customCommandOutputSchemas = new Dictionary(StringComparer.OrdinalIgnoreCase); + _customCommandLoadError = $"Custom command output schemas could not be loaded. {exception.Message}"; + } + } + + private CommandOutputVariableReference[] GetPriorVariableReferences( ExperimentTemplate experimentTemplate, StepTemplate stepTemplate) { @@ -60,7 +92,8 @@ if(currentStep == stepTemplate) return references.ToArray(); - references.AddRange(currentStep.CommandTemplates.SelectMany(CommandOutputVariableReferenceBuilder.Build)); + references.AddRange(currentStep.CommandTemplates + .SelectMany(command => CommandOutputVariableReferenceBuilder.Build(command, _customCommandOutputSchemas))); } return references.ToArray(); diff --git a/UI/Components/Experiments/StepViewer.razor b/UI/Components/Experiments/StepViewer.razor index a0614bbb..826de935 100644 --- a/UI/Components/Experiments/StepViewer.razor +++ b/UI/Components/Experiments/StepViewer.razor @@ -1,3 +1,4 @@ +@using Ares.Datamodel @using Ares.Datamodel.Templates @using UI.Features.CampaignEdit.ViewModels @@ -68,6 +69,10 @@ [Parameter] public IEnumerable PriorVariableReferences { get; set; } = []; + [Parameter] + public IReadOnlyDictionary CustomCommandOutputSchemas { get; set; } + = new Dictionary(StringComparer.OrdinalIgnoreCase); + private bool HasExperimentOutputs { get; set; } public IList CommandTemplates { get; private set; } = Array.Empty(); @@ -92,7 +97,7 @@ { availableReferences.AddRange(CommandTemplates .Where(command => command.Index < template.Index) - .SelectMany(CommandOutputVariableReferenceBuilder.Build)); + .SelectMany(command => CommandOutputVariableReferenceBuilder.Build(command, CustomCommandOutputSchemas))); } return availableReferences.ToArray(); diff --git a/UI/Components/Layouts/MainLayout.razor b/UI/Components/Layouts/MainLayout.razor index a948b8d0..b3825389 100644 --- a/UI/Components/Layouts/MainLayout.razor +++ b/UI/Components/Layouts/MainLayout.razor @@ -44,7 +44,7 @@ - + @@ -69,7 +69,7 @@ - + diff --git a/UI/Components/Nav/AresNavItem.razor b/UI/Components/Nav/AresNavItem.razor index fabdc0b8..1fe17bef 100644 --- a/UI/Components/Nav/AresNavItem.razor +++ b/UI/Components/Nav/AresNavItem.razor @@ -6,6 +6,10 @@ @Text + @if(!string.IsNullOrWhiteSpace(BadgeText)) + { + @BadgeText + } @if (ChildContent is not null) {
@@ -40,6 +44,9 @@ [Parameter] public string? Text { get; set; } + [Parameter] + public string? BadgeText { get; set; } + [Parameter] public NavLinkMatch Match { get; set; } = NavLinkMatch.All; @@ -55,4 +62,4 @@ { ExpandedSubNav = !ExpandedSubNav; } -} \ No newline at end of file +} diff --git a/UI/Components/ScriptEditor.razor b/UI/Components/ScriptEditor.razor index a2199406..aa31bc41 100644 --- a/UI/Components/ScriptEditor.razor +++ b/UI/Components/ScriptEditor.razor @@ -2,7 +2,9 @@ @inject IJSRuntime JSRuntime @implements IAsyncDisposable - +
+ +
@code { private const string DefaultScript = "# Welcome to ARES\ndef main():\n return True\n"; @@ -46,6 +48,19 @@ await _editor.SetValue(script ?? string.Empty); } + public async Task RefreshLanguageFeaturesAsync() + { + if(_diagnosticsModule is not null) + { + await _diagnosticsModule.InvokeVoidAsync("refreshDiagnostics"); + } + + if(_semanticModule is not null) + { + await _semanticModule.InvokeVoidAsync("refreshSemanticTokens"); + } + } + protected override async Task OnAfterRenderAsync(bool firstRender) { if(firstRender) @@ -143,7 +158,7 @@ private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor) { - var initialScript = string.IsNullOrWhiteSpace(InitialScript) ? DefaultScript : InitialScript; + var initialScript = InitialScript ?? DefaultScript; return new StandaloneEditorConstructionOptions { diff --git a/UI/Components/ScriptEditor.razor.css b/UI/Components/ScriptEditor.razor.css index fb69b93d..b9cea48d 100644 --- a/UI/Components/ScriptEditor.razor.css +++ b/UI/Components/ScriptEditor.razor.css @@ -1,3 +1,11 @@ -.monaco-editor-container { - height: 100px; +.script-editor { + width: 100%; + height: 100%; + min-height: 0; +} + +.script-editor ::deep .script-editor__monaco { + width: 100%; + height: 100%; + min-height: 0; } diff --git a/UI/Domain/Experiments/ExperimentTemplateExtensions.cs b/UI/Domain/Experiments/ExperimentTemplateExtensions.cs index c24d605e..10b90615 100644 --- a/UI/Domain/Experiments/ExperimentTemplateExtensions.cs +++ b/UI/Domain/Experiments/ExperimentTemplateExtensions.cs @@ -8,7 +8,7 @@ internal static class ExperimentTemplateExtensions public static IEnumerable GetAllParameters(this ExperimentTemplate template) => template.StepTemplates .SelectMany(stepTemplate => stepTemplate.CommandTemplates) - .SelectMany(commandTemplate => commandTemplate.Parameters); + .SelectMany(commandTemplate => commandTemplate.ArgumentBindings); public static IEnumerable GetAllPlannedParameters(this ExperimentTemplate template) => template.GetAllParameters().Where(parameter => parameter.IsPlanned()); diff --git a/UI/Features/CampaignEdit/Factories/CommandDesignerFactory.cs b/UI/Features/CampaignEdit/Factories/CommandDesignerFactory.cs index c95c2d07..311a12af 100644 --- a/UI/Features/CampaignEdit/Factories/CommandDesignerFactory.cs +++ b/UI/Features/CampaignEdit/Factories/CommandDesignerFactory.cs @@ -1,4 +1,5 @@ -using Ares.Datamodel.Templates; +using Ares.Core.CustomCommands; +using Ares.Datamodel.Templates; using Ares.Services.Device; using Ares.Core.Grpc.Services; using CommandDesignerViewModel=UI.Features.CampaignEdit.ViewModels.CommandDesignerViewModel; @@ -10,20 +11,32 @@ public class CommandDesignerFactory private readonly CommandParameterDesignerFactory _commandParameterDesignerFactory; private readonly DevicesService _devicesClient; private readonly MetadataPickerFactory _metadataPickerFactory; + private readonly ICustomCommandPersistenceService _customCommandPersistenceService; public CommandDesignerFactory( MetadataPickerFactory metadataPickerFactory, CommandParameterDesignerFactory commandParameterDesignerFactory, - DevicesService devicesClient) + DevicesService devicesClient, + ICustomCommandPersistenceService customCommandPersistenceService) { _metadataPickerFactory = metadataPickerFactory; _commandParameterDesignerFactory = commandParameterDesignerFactory; _devicesClient = devicesClient; + _customCommandPersistenceService = customCommandPersistenceService; } public CommandDesignerViewModel Create() - => new CommandDesignerViewModel(_commandParameterDesignerFactory, _metadataPickerFactory, _devicesClient); + => new CommandDesignerViewModel( + _commandParameterDesignerFactory, + _metadataPickerFactory, + _devicesClient, + _customCommandPersistenceService); public CommandDesignerViewModel Create(CommandTemplate existingTemplate) - => new CommandDesignerViewModel(existingTemplate, _commandParameterDesignerFactory, _metadataPickerFactory, _devicesClient); + => new CommandDesignerViewModel( + existingTemplate, + _commandParameterDesignerFactory, + _metadataPickerFactory, + _devicesClient, + _customCommandPersistenceService); } diff --git a/UI/Features/CampaignEdit/ViewModels/AnalyzerDesignerViewModel.cs b/UI/Features/CampaignEdit/ViewModels/AnalyzerDesignerViewModel.cs index 67313c17..602e1b1f 100644 --- a/UI/Features/CampaignEdit/ViewModels/AnalyzerDesignerViewModel.cs +++ b/UI/Features/CampaignEdit/ViewModels/AnalyzerDesignerViewModel.cs @@ -49,6 +49,10 @@ public async Task UpdateMappings() var parameters = await _analysisService.GetAnalyzerParameters( new AnalyzerParametersRequest { AnalyzerId = AnalyzerId }, null); + await Task.WhenAll(_commandDesignerViewModels + .Concat(_startupCommandDesignerViewModels) + .Select(command => command.EnsureInitializedAsync())); + var inputMappings = parameters.AnalysisSchema.Fields.Select(field => new ExperimentOutputAnalyzerInputMapping(field.Key, field.Value.Type, !field.Value.Optional)).ToArray(); CalculateAppropriateOutputs(inputMappings); @@ -110,7 +114,7 @@ private static IEnumerable GetOutputSchemaPaths(Comm if(!commandDesigner.OutputProvider || string.IsNullOrWhiteSpace(commandDesigner.OutputVariableName)) return []; - var outputSchema = commandDesigner.CommandMetadata?.OutputMetadata?.DataSchema; + var outputSchema = commandDesigner.OutputSchema; if(outputSchema is null) return []; diff --git a/UI/Features/CampaignEdit/ViewModels/CampaignListViewModel.cs b/UI/Features/CampaignEdit/ViewModels/CampaignListViewModel.cs index f278a945..b5f4efc9 100644 --- a/UI/Features/CampaignEdit/ViewModels/CampaignListViewModel.cs +++ b/UI/Features/CampaignEdit/ViewModels/CampaignListViewModel.cs @@ -1,20 +1,27 @@ -using Ares.Core.Grpc.Services; +using Ares.Core.Campaigns; +using Ares.Core.Grpc.Services; using Ares.Datamodel.Templates; using Ares.Services; using DynamicData; using ReactiveUI; using System.Collections.ObjectModel; +using Microsoft.AspNetCore.Components.Forms; namespace UI.Features.CampaignEdit.ViewModels; public class CampaignListViewModel : ReactiveObject { private readonly AutomationService _automationClient; + private readonly ICampaignTemplateTransferService _campaignTemplateTransferService; + public const long MaximumImportFileSize = 5 * 1024 * 1024; public readonly ObservableCollection Templates = []; - public CampaignListViewModel(AutomationService automationClient) + public CampaignListViewModel( + AutomationService automationClient, + ICampaignTemplateTransferService campaignTemplateTransferService) { _automationClient = automationClient; + _campaignTemplateTransferService = campaignTemplateTransferService; } public async Task GetFullCampaignTemplate(string campaignId) @@ -22,9 +29,17 @@ public CampaignListViewModel(AutomationService automationClient) return await _automationClient.GetSingleCampaign(new CampaignRequest { UniqueId = campaignId }, null); } - public async Task GetCopyOfCampaignTemplate(string campaignId) + public Task ExportCampaignTemplate(string campaignId) + => _campaignTemplateTransferService.ExportAsync(campaignId); + + public async Task ImportCampaignTemplate(IBrowserFile file) { - return await _automationClient.GetCopyOfCampaign(new CampaignRequest { UniqueId = campaignId }, null); + if(file.Size > MaximumImportFileSize) + throw new CampaignTemplateImportException("Campaign template files must be 5 MiB or smaller."); + + await using var stream = file.OpenReadStream(MaximumImportFileSize); + using var reader = new StreamReader(stream); + return await _campaignTemplateTransferService.ImportAsync(await reader.ReadToEndAsync()); } public async Task RefreshCampaigns() diff --git a/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs b/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs index 07bcfb1d..9e3da4f1 100644 --- a/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs +++ b/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs @@ -1,8 +1,11 @@ -using Ares.Datamodel; +using Ares.Core.CustomCommands; +using Ares.Core.Execution.Executors; +using Ares.Core.Grpc.Services; +using Ares.Datamodel; +using Ares.Datamodel.Automation; using Ares.Datamodel.Extensions; using Ares.Datamodel.Templates; using Ares.Services.Device; -using Ares.Core.Grpc.Services; using ReactiveUI; using ReactiveUI.SourceGenerators; using UI.Features.CampaignEdit.Factories; @@ -14,150 +17,201 @@ public partial class CommandDesignerViewModel : ReactiveObject private readonly CommandParameterDesignerFactory _commandParameterDesignerFactory; private readonly MetadataPickerFactory _metadataPickerFactory; private readonly DevicesService _devicesClient; + private readonly ICustomCommandPersistenceService _customCommandPersistenceService; + private readonly Task _initializationTask; private CommandMetadata? _commandMetadata; - private CommandTemplate _commandTemplate = null!; + private CustomCommandVersion? _selectedCustomCommand; private CommandOutputVariableReference[] _availableVariableReferences = []; public CommandDesignerViewModel( CommandTemplate existingTemplate, CommandParameterDesignerFactory commandParameterDesignerFactory, MetadataPickerFactory metadataPickerFactory, - DevicesService devicesClient) + DevicesService devicesClient, + ICustomCommandPersistenceService customCommandPersistenceService) { ArgumentDesigners = []; + AvailableCustomCommands = []; _commandParameterDesignerFactory = commandParameterDesignerFactory; _metadataPickerFactory = metadataPickerFactory; _devicesClient = devicesClient; - + _customCommandPersistenceService = customCommandPersistenceService; CommandTemplate = existingTemplate; + _initializationTask = InitializeAsync(existingTemplate); } - public CommandDesignerViewModel(CommandParameterDesignerFactory commandParameterDesignerFactory, MetadataPickerFactory metadataPickerFactory, DevicesService devicesClient) + public CommandDesignerViewModel( + CommandParameterDesignerFactory commandParameterDesignerFactory, + MetadataPickerFactory metadataPickerFactory, + DevicesService devicesClient, + ICustomCommandPersistenceService customCommandPersistenceService) + : this( + new CommandTemplate + { + UniqueId = Guid.NewGuid().ToString(), + DeviceCommand = new DeviceCommand() + }, + commandParameterDesignerFactory, + metadataPickerFactory, + devicesClient, + customCommandPersistenceService) { - ArgumentDesigners = []; - _commandParameterDesignerFactory = commandParameterDesignerFactory; - _metadataPickerFactory = metadataPickerFactory; - _devicesClient = devicesClient; - - CommandTemplate = new CommandTemplate - { - UniqueId = Guid.NewGuid().ToString() - }; } - public CommandTemplate CommandTemplate + public CommandTemplate CommandTemplate { get; } + + public int Index { get; set; } + + private string? TemplateDeviceName { get; set; } + + private string? MetadataDeviceName { get; set; } + + public string? TemplateCommandName => CommandTypeCase switch + { + CommandTemplate.CommandTypeOneofCase.DeviceCommand => CommandMetadata?.Name, + CommandTemplate.CommandTypeOneofCase.SystemCommand => SelectedSystemOperationDefinition?.DisplayName, + CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation => _selectedCustomCommand?.Name + ?? (string.IsNullOrWhiteSpace(SelectedCustomCommandId) ? null : "Unknown Custom Command"), + CommandTemplate.CommandTypeOneofCase.None => null, + _ => throw new ArgumentOutOfRangeException(nameof(CommandTypeCase), CommandTypeCase, null) + }; + + public string? TemplateCommandDescription => CommandTypeCase switch { - get => _commandTemplate; + CommandTemplate.CommandTypeOneofCase.DeviceCommand => CommandMetadata?.Description, + CommandTemplate.CommandTypeOneofCase.SystemCommand => SelectedSystemOperationDefinition?.Description, + CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation => _selectedCustomCommand?.Description, + CommandTemplate.CommandTypeOneofCase.None => null, + _ => throw new ArgumentOutOfRangeException(nameof(CommandTypeCase), CommandTypeCase, null) + }; + + public string? CommandTargetName => CommandTypeCase switch + { + CommandTemplate.CommandTypeOneofCase.DeviceCommand => MetadataDeviceName ?? TemplateDeviceName, + CommandTemplate.CommandTypeOneofCase.SystemCommand => "SYSTEM", + CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation => "CUSTOM", + CommandTemplate.CommandTypeOneofCase.None => null, + _ => throw new ArgumentOutOfRangeException(nameof(CommandTypeCase), CommandTypeCase, null) + }; + + public bool IsCommandUnavailable => CommandTypeCase switch + { + CommandTemplate.CommandTypeOneofCase.DeviceCommand => CommandMetadata is null, + CommandTemplate.CommandTypeOneofCase.SystemCommand => SelectedSystemOperationDefinition is null, + CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation => !string.IsNullOrWhiteSpace(SelectedCustomCommandId) + && _selectedCustomCommand is null, + CommandTemplate.CommandTypeOneofCase.None => true, + _ => throw new ArgumentOutOfRangeException(nameof(CommandTypeCase), CommandTypeCase, null) + }; - set + public CommandTemplate.CommandTypeOneofCase CommandTypeCase => CommandTemplate.CommandTypeCase; + + public int SelectedCommandTabIndex + { + get => CommandTypeCase switch { - _commandTemplate = value; - CommandMetadata = value.Metadata; - InitTemplate(value); - } + CommandTemplate.CommandTypeOneofCase.DeviceCommand => 0, + CommandTemplate.CommandTypeOneofCase.SystemCommand => 1, + CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation => 2, + CommandTemplate.CommandTypeOneofCase.None => 0, + _ => throw new ArgumentOutOfRangeException(nameof(CommandTypeCase), CommandTypeCase, null) + }; + set => SelectCommandType(value); } - public int Index { get; set; } + public static IReadOnlyList AvailableSystemOperations => SystemOperationCatalog.Definitions; + + public SystemOperation SelectedSystemOperation => CommandTemplate.SystemCommand?.Operation ?? SystemOperation.Undefined; + + private SystemOperationDefinition? SelectedSystemOperationDefinition + => SystemOperationCatalog.Find(SelectedSystemOperation); + + public IReadOnlyList AvailableCustomCommands { get; private set; } + + public string? CustomCommandLoadError { get; private set; } - public string? TemplateDeviceName { get; private set; } - public string? MetadataDeviceName { get; private set; } - public string? TemplateCommandName => CommandTemplate.Metadata?.Name; + public string? SelectedCustomCommandId => CommandTemplate.CustomCommandInvocation?.CustomCommandId; public bool TemplateOutputProvider => CommandTemplate.HasOutputVarName; public bool OutputProvider { get; set; } - public bool HasOutputMetadata => CommandMetadata?.OutputMetadata?.DataSchema is not null; + public AresValueSchema? OutputSchema => CommandTypeCase switch + { + CommandTemplate.CommandTypeOneofCase.DeviceCommand => CommandMetadata?.OutputMetadata?.DataSchema, + CommandTemplate.CommandTypeOneofCase.SystemCommand => SelectedSystemOperationDefinition?.OutputSchema, + CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation => _selectedCustomCommand?.OutputSchema, + CommandTemplate.CommandTypeOneofCase.None => null, + _ => throw new ArgumentOutOfRangeException(nameof(CommandTypeCase), CommandTypeCase, null) + }; + + public bool HasOutputMetadata => OutputSchema is not null + && OutputSchema.Type is not AresDataType.Unit and not AresDataType.UnspecifiedType; public string? OutputVariableName { get; set; } - public IEnumerable Arguments => CommandTemplate.Parameters; + public IEnumerable Arguments => CommandTemplate.ArgumentBindings; public CommandMetadata? CommandMetadata { get => _commandMetadata; - - set - { - _commandMetadata = value; - InitMetadata(value); - } + private set => this.RaiseAndSetIfChanged(ref _commandMetadata, value); } - public MetadataPickerViewModel? MetadataPickerViewModel { get; set; } + public MetadataPickerViewModel? MetadataPickerViewModel { get; private set; } [Reactive] public partial IEnumerable ArgumentDesigners { get; private set; } + public Task EnsureInitializedAsync() => _initializationTask; + public CommandTemplate Save() { - CommandTemplate.Parameters.Clear(); - CommandTemplate.Parameters.AddRange(ArgumentDesigners.Select(model => model.Save())); - if(CommandMetadata is not null) + CommandTemplate.ArgumentBindings.Clear(); + CommandTemplate.ArgumentBindings.AddRange(ArgumentDesigners.Select(model => model.Save())); + + if(CommandTypeCase == CommandTemplate.CommandTypeOneofCase.DeviceCommand && CommandMetadata is not null) { - CommandTemplate.Metadata = CommandMetadata; - CommandTemplate.Metadata.DeviceType = MetadataDeviceName; + CommandTemplate.DeviceCommand.Metadata = CommandMetadata; + if(!string.IsNullOrWhiteSpace(MetadataDeviceName)) + CommandTemplate.DeviceCommand.Metadata.DeviceType = MetadataDeviceName; } - CommandTemplate.Index = Index; CommandTemplate.ClearOutputVarName(); if(OutputProvider && HasOutputMetadata && !string.IsNullOrWhiteSpace(OutputVariableName)) - { CommandTemplate.OutputVarName = OutputVariableName.Trim(); - } return CommandTemplate; } - private async Task InitTemplate(CommandTemplate existingTemplate) + public async Task DeviceCommandMetadataUpdated(CommandMetadata? metadata) { - Index = Convert.ToInt32(existingTemplate.Index); - var existingParamDesigners = existingTemplate.Parameters.Select(_commandParameterDesignerFactory.Create).ToArray(); - ArgumentDesigners = [.. existingParamDesigners]; - ApplyAvailableVariableReferences(); - MetadataPickerViewModel = _metadataPickerFactory.Create(existingTemplate.Metadata); - - OutputProvider = existingTemplate.HasOutputVarName; - OutputVariableName = existingTemplate.HasOutputVarName ? existingTemplate.OutputVarName : null; - - // Revisit this once we have some sort of caching on the UI end. - // that way we don't have to bother the service every time - if(existingTemplate.Metadata?.DeviceId is not null) - { - - var deviceInfo = await _devicesClient.GetDeviceInfo(new DeviceInfoRequest { DeviceId = existingTemplate.Metadata.DeviceId }, null); - TemplateDeviceName = string.IsNullOrEmpty(deviceInfo.Name) ? null : deviceInfo.Name; - } + CommandTemplate.DeviceCommand ??= new DeviceCommand(); + CommandTemplate.DeviceCommand.Metadata = metadata; + CommandMetadata = metadata; + await ApplyDeviceMetadataAsync(metadata, preserveBindings: false); + RaiseCommandPropertiesChanged(); } - public async Task MetadataUpdated(CommandMetadata? metadata) + public void SelectSystemOperation(SystemOperation operation) { - CommandTemplate.Metadata = metadata; - CommandMetadata = metadata; + CommandTemplate.SystemCommand ??= new SystemCommand(); + CommandTemplate.SystemCommand.Operation = operation; + var definition = SystemOperationCatalog.Find(operation); + SetArgumentDefinitions(definition?.Parameters ?? []); + ResetOutputAssignment(); + RaiseCommandPropertiesChanged(); } - private async Task InitMetadata(CommandMetadata? existingMetadata) + public void SelectCustomCommand(string? customCommandId) { - var newArgumentDesigners = existingMetadata?.ParameterMetadatas - ?.Select(_commandParameterDesignerFactory.Create) - .ToArray() ?? []; - - ArgumentDesigners = newArgumentDesigners; - ApplyAvailableVariableReferences(); - if(existingMetadata?.OutputMetadata?.DataSchema is null) - { - OutputProvider = false; - OutputVariableName = null; - } - - var deviceId = existingMetadata?.DeviceId; - - if(deviceId is not null) - { - var deviceInfo = await _devicesClient.GetDeviceInfo(new DeviceInfoRequest { DeviceId = deviceId }, null); - MetadataDeviceName = string.IsNullOrEmpty(deviceInfo.Name) ? null : deviceInfo.Name; - } + CommandTemplate.CustomCommandInvocation ??= new CustomCommandInvocation(); + CommandTemplate.CustomCommandInvocation.CustomCommandId = customCommandId ?? string.Empty; + _selectedCustomCommand = AvailableCustomCommands.FirstOrDefault(command => command.CustomCommandId == customCommandId); + SetArgumentDefinitions(BuildCustomParameterMetadata(_selectedCustomCommand)); + ResetOutputAssignment(); + RaiseCommandPropertiesChanged(); } public void SetAvailableVariableReferences(IEnumerable references) @@ -183,13 +237,216 @@ public CommandOutputVariableReference[] GetOutputVariableReferences() ParameterSource.Variable when !argumentDesigner.HasSelectedVariableReference() => "Command output variable is no longer available.", - _ => null + ParameterSource.Planned or ParameterSource.Variable + => null, + + ParameterSource.Unspecified or ParameterSource.Value or ParameterSource.Environment + => null, + + _ => throw new ArgumentOutOfRangeException(nameof(parameter), parameter.GetParameterSource(), null) }; } + private async Task InitializeAsync(CommandTemplate existingTemplate) + { + Index = Convert.ToInt32(existingTemplate.Index); + OutputProvider = existingTemplate.HasOutputVarName; + OutputVariableName = existingTemplate.HasOutputVarName ? existingTemplate.OutputVarName : null; + try + { + AvailableCustomCommands = (await _customCommandPersistenceService.GetCommandsAsync()) + .OrderBy(command => command.Name) + .ToArray(); + } + catch(Exception exception) + { + AvailableCustomCommands = []; + CustomCommandLoadError = $"Failed to load custom commands. {exception.Message}"; + } + + switch(existingTemplate.CommandTypeCase) + { + case CommandTemplate.CommandTypeOneofCase.SystemCommand: + SetArgumentDefinitions(SelectedSystemOperationDefinition?.Parameters ?? [], existingTemplate.ArgumentBindings); + break; + + case CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation: + _selectedCustomCommand = AvailableCustomCommands.FirstOrDefault(command => command.CustomCommandId == SelectedCustomCommandId); + var customDefinitions = BuildCustomParameterMetadata(_selectedCustomCommand); + if(_selectedCustomCommand is null) + // The saved custom command may have been deleted or failed to load. Preserve its + // saved arguments so the campaign can still be displayed and edited. + SetExistingArguments(existingTemplate.ArgumentBindings); + else + SetArgumentDefinitions(customDefinitions, existingTemplate.ArgumentBindings); + break; + + case CommandTemplate.CommandTypeOneofCase.DeviceCommand: + CommandMetadata = existingTemplate.DeviceCommand?.Metadata; + MetadataPickerViewModel = _metadataPickerFactory.Create(CommandMetadata); + SetArgumentDefinitions(CommandMetadata?.ParameterMetadatas ?? [], existingTemplate.ArgumentBindings); + await LoadDeviceNamesAsync(CommandMetadata); + break; + + case CommandTemplate.CommandTypeOneofCase.None: + throw new ArgumentOutOfRangeException(nameof(existingTemplate.CommandTypeCase), existingTemplate.CommandTypeCase, null); + + default: + throw new ArgumentOutOfRangeException(nameof(existingTemplate.CommandTypeCase), existingTemplate.CommandTypeCase, null); + } + + if(!HasOutputMetadata) + ResetOutputAssignment(); + + RaiseCommandPropertiesChanged(); + } + + private void SelectCommandType(int tabIndex) + { + var commandType = tabIndex switch + { + 0 => CommandTemplate.CommandTypeOneofCase.DeviceCommand, + 1 => CommandTemplate.CommandTypeOneofCase.SystemCommand, + 2 => CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation, + _ => throw new ArgumentOutOfRangeException(nameof(tabIndex), tabIndex, null) + }; + + if(commandType == CommandTypeCase) + return; + + CommandTemplate.ClearCommandType(); + CommandMetadata = null; + _selectedCustomCommand = null; + TemplateDeviceName = null; + MetadataDeviceName = null; + ArgumentDesigners = []; + ResetOutputAssignment(); + + switch(commandType) + { + case CommandTemplate.CommandTypeOneofCase.SystemCommand: + CommandTemplate.SystemCommand = new SystemCommand(); + break; + + case CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation: + CommandTemplate.CustomCommandInvocation = new CustomCommandInvocation(); + break; + + case CommandTemplate.CommandTypeOneofCase.DeviceCommand: + CommandTemplate.DeviceCommand = new DeviceCommand(); + MetadataPickerViewModel = _metadataPickerFactory.Create(); + break; + + case CommandTemplate.CommandTypeOneofCase.None: + default: + throw new ArgumentOutOfRangeException(nameof(commandType), commandType, null); + } + + RaiseCommandPropertiesChanged(); + } + + private async Task ApplyDeviceMetadataAsync(CommandMetadata? metadata, bool preserveBindings) + { + var existingBindings = preserveBindings ? CommandTemplate.ArgumentBindings : []; + SetArgumentDefinitions(metadata?.ParameterMetadatas ?? [], existingBindings); + ResetOutputAssignment(); + await LoadDeviceNamesAsync(metadata); + } + + private async Task LoadDeviceNamesAsync(CommandMetadata? metadata) + { + MetadataDeviceName = null; + if(string.IsNullOrWhiteSpace(metadata?.DeviceId)) + return; + + try + { + var deviceInfo = await _devicesClient.GetDeviceInfo(new DeviceInfoRequest { DeviceId = metadata.DeviceId }, null); + var deviceName = string.IsNullOrWhiteSpace(deviceInfo.Name) ? null : deviceInfo.Name; + MetadataDeviceName = deviceName; + TemplateDeviceName ??= deviceName; + } + catch + { + MetadataDeviceName = null; + } + } + + private void SetArgumentDefinitions( + IEnumerable definitions, + IEnumerable? existingBindings = null) + { + var bindingsByName = (existingBindings ?? []) + .Where(binding => binding.Metadata is not null) + .GroupBy(binding => binding.Metadata.Name, StringComparer.Ordinal) + .ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal); + + ArgumentDesigners = definitions.Select(definition => + { + if(!bindingsByName.TryGetValue(definition.Name, out var binding)) + return _commandParameterDesignerFactory.Create(CloneBindingMetadata(definition)); + + var normalizedBinding = binding.Clone(); + normalizedBinding.Metadata = CloneBindingMetadata(definition, binding.Metadata?.UniqueId); + return _commandParameterDesignerFactory.Create(normalizedBinding); + }).ToArray(); + ApplyAvailableVariableReferences(); + } + + private static ParameterMetadata CloneBindingMetadata(ParameterMetadata definition, string? existingBindingMetadataId = null) + { + var metadata = definition.Clone(); + metadata.UniqueId = !string.IsNullOrWhiteSpace(existingBindingMetadataId) + && !string.Equals(existingBindingMetadataId, definition.UniqueId, StringComparison.OrdinalIgnoreCase) + ? existingBindingMetadataId + : Guid.NewGuid().ToString(); + return metadata; + } + + private void SetExistingArguments(IEnumerable bindings) + { + ArgumentDesigners = bindings + .Select(binding => _commandParameterDesignerFactory.Create(binding.Clone())) + .ToArray(); + ApplyAvailableVariableReferences(); + } + + private static ParameterMetadata[] BuildCustomParameterMetadata(CustomCommandVersion? command) + => command?.InputParameters + .Select((parameter, index) => new ParameterMetadata + { + UniqueId = Guid.NewGuid().ToString(), + Name = parameter.Name, + Index = index, + Schema = parameter.Schema?.Clone() ?? new AresValueSchema() + }) + .ToArray() ?? []; + + private void ResetOutputAssignment() + { + OutputProvider = false; + OutputVariableName = null; + CommandTemplate.ClearOutputVarName(); + } + private void ApplyAvailableVariableReferences() { foreach(var argumentDesigner in ArgumentDesigners) argumentDesigner.SetAvailableVariableReferences(_availableVariableReferences); } + + private void RaiseCommandPropertiesChanged() + { + this.RaisePropertyChanged(nameof(CommandTypeCase)); + this.RaisePropertyChanged(nameof(SelectedCommandTabIndex)); + this.RaisePropertyChanged(nameof(SelectedSystemOperation)); + this.RaisePropertyChanged(nameof(SelectedCustomCommandId)); + this.RaisePropertyChanged(nameof(CustomCommandLoadError)); + this.RaisePropertyChanged(nameof(TemplateCommandName)); + this.RaisePropertyChanged(nameof(TemplateCommandDescription)); + this.RaisePropertyChanged(nameof(CommandTargetName)); + this.RaisePropertyChanged(nameof(IsCommandUnavailable)); + this.RaisePropertyChanged(nameof(OutputSchema)); + this.RaisePropertyChanged(nameof(HasOutputMetadata)); + } } diff --git a/UI/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs b/UI/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs index fa9fb32a..b766b5cf 100644 --- a/UI/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs +++ b/UI/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs @@ -1,3 +1,4 @@ +using Ares.Core.Execution.Executors; using Ares.Datamodel; using Ares.Datamodel.Templates; @@ -17,25 +18,41 @@ public static CommandOutputVariableReference[] Build(CommandDesignerViewModel co if(!commandDesigner.OutputProvider || string.IsNullOrWhiteSpace(commandDesigner.OutputVariableName)) return []; - var outputSchema = commandDesigner.CommandMetadata?.OutputMetadata?.DataSchema; + var outputSchema = commandDesigner.OutputSchema; if(outputSchema is null) return []; return Build(commandDesigner.OutputVariableName, outputSchema).ToArray(); } - public static CommandOutputVariableReference[] Build(CommandTemplate commandTemplate) + public static CommandOutputVariableReference[] Build( + CommandTemplate commandTemplate, + IReadOnlyDictionary customCommandOutputSchemas) { if(!commandTemplate.HasOutputVarName) return []; - var outputSchema = commandTemplate.Metadata?.OutputMetadata?.DataSchema; - if(outputSchema is null) + var outputSchema = commandTemplate.CommandTypeCase switch + { + CommandTemplate.CommandTypeOneofCase.DeviceCommand => commandTemplate.DeviceCommand.Metadata?.OutputMetadata?.DataSchema, + CommandTemplate.CommandTypeOneofCase.SystemCommand => SystemOperationCatalog.Find(commandTemplate.SystemCommand.Operation)?.OutputSchema, + CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation + => ResolveCustomCommandOutputSchema(commandTemplate.CustomCommandInvocation.CustomCommandId, customCommandOutputSchemas), + _ => null + }; + if(outputSchema is null || outputSchema.Type is AresDataType.Unit or AresDataType.UnspecifiedType) return []; return Build(commandTemplate.OutputVarName, outputSchema).ToArray(); } + private static AresValueSchema? ResolveCustomCommandOutputSchema( + string customCommandId, + IReadOnlyDictionary customCommandOutputSchemas) + => customCommandOutputSchemas.TryGetValue(customCommandId, out var outputSchema) + ? outputSchema + : null; + public static CommandOutputVariableReference[] MarkCompatibility( IEnumerable references, AresValueSchema parameterSchema) diff --git a/UI/Features/CampaignEdit/ViewModels/StepDesignerViewModel.cs b/UI/Features/CampaignEdit/ViewModels/StepDesignerViewModel.cs index 6027493a..8ad1a69b 100644 --- a/UI/Features/CampaignEdit/ViewModels/StepDesignerViewModel.cs +++ b/UI/Features/CampaignEdit/ViewModels/StepDesignerViewModel.cs @@ -82,6 +82,12 @@ public StepTemplate Save() return StepTemplate; } + public async Task EnsureInitializedAsync() + { + await Task.WhenAll(CommandDesigners.Select(command => command.EnsureInitializedAsync())); + RefreshAvailableVariableReferences(); + } + public CommandDesignerViewModel AddCommandDesigner() { var newDesigner = _commandDesignerFactory.Create(); diff --git a/UI/Features/CampaignEdit/Views/CampaignList.razor b/UI/Features/CampaignEdit/Views/CampaignList.razor index 8e4df0f7..1079114b 100644 --- a/UI/Features/CampaignEdit/Views/CampaignList.razor +++ b/UI/Features/CampaignEdit/Views/CampaignList.razor @@ -2,20 +2,32 @@ @page "/automation/campaignlist" @using Ares.Services @using Newtonsoft.Json +@using UI.Application.Notifications @using UI.Features.CampaignEdit.ViewModels @inject DialogService DialogService @inherits ReactiveUI.Blazor.ReactiveInjectableComponentBase @inject IJSRuntime JS +@inject IUiNotificationService NotificationService

Campaigns

- + -
@@ -37,13 +49,13 @@ @template.UniqueId
- + - -
@@ -55,6 +67,8 @@
@code { + private bool _isImporting; + protected override async Task OnInitializedAsync() { await ViewModel!.RefreshCampaigns(); @@ -85,19 +99,45 @@ private async void ExportTemplate(CampaignTemplateSummary templateSummary) { - //Generate a copy of the template, create a new unique ID and change the name to avoid collisions if the user adds it to their templates folder - var response = await ViewModel!.GetCopyOfCampaignTemplate(templateSummary.UniqueId); + try + { + var export = await ViewModel!.ExportCampaignTemplate(templateSummary.UniqueId); + if(export is null) + { + NotificationService.Error("The selected campaign could not be found."); + return; + } - if(response.HasSerializedJsonData) + await JS.InvokeVoidAsync("downloadJsonFile", export.SuggestedFileName, export.Json); + } + catch(Exception exception) { - var templateCopy = response.Template; - var fileName = templateCopy.UniqueId; - await JS.InvokeVoidAsync("downloadJsonFile", fileName, response.SerializedJsonData); + NotificationService.Error($"Failed to export campaign. {exception.Message}"); } + } - else + private async Task ImportTemplate(InputFileChangeEventArgs args) + { + if(_isImporting) + return; + + _isImporting = true; + try + { + var result = await ViewModel!.ImportCampaignTemplate(args.File); + await ViewModel.RefreshCampaigns(); + NotificationService.Success($"Imported campaign '{result.Template.Name}'."); + if(result.Warnings.Count > 0) + NotificationService.Warning(string.Join(System.Environment.NewLine, result.Warnings), "Campaign imported with warnings"); + } + catch(Exception exception) + { + NotificationService.Error($"Failed to import campaign. {exception.Message}"); + } + finally { - //Notify of failure..? + _isImporting = false; + StateHasChanged(); } } diff --git a/UI/Features/CampaignEdit/Views/CommandDesigner.razor b/UI/Features/CampaignEdit/Views/CommandDesigner.razor index 40450037..5c92da42 100644 --- a/UI/Features/CampaignEdit/Views/CommandDesigner.razor +++ b/UI/Features/CampaignEdit/Views/CommandDesigner.razor @@ -1,24 +1,86 @@ -@using Ares.Datamodel.Templates +@using Ares.Datamodel.Automation +@using Ares.Datamodel.Templates @using UI.Features.CampaignEdit.ViewModels -@inherits ReactiveUI.Blazor.ReactiveComponentBase +@inherits ReactiveUI.Blazor.ReactiveComponentBase - + + + + @if(ViewModel.MetadataPickerViewModel is not null) + { + + } + else + { + Loading device commands... + } + - @foreach (var argumentDesigner in ViewModel!.ArgumentDesigners) + +
+ + + @if(!string.IsNullOrWhiteSpace(ViewModel.TemplateCommandDescription)) + { + @ViewModel.TemplateCommandDescription + } +
+
+ + +
+ + + @if(!string.IsNullOrWhiteSpace(ViewModel.CustomCommandLoadError)) + { + @ViewModel.CustomCommandLoadError + } + else if(ViewModel.IsCommandUnavailable && !string.IsNullOrWhiteSpace(ViewModel.SelectedCustomCommandId)) + { + The selected custom command is no longer available. + } + else if(!string.IsNullOrWhiteSpace(ViewModel.TemplateCommandDescription)) + { + @ViewModel.TemplateCommandDescription + } +
+
+
+
+ + @foreach(var argumentDesigner in ViewModel.ArgumentDesigners) { - + } - @if(!DisablePlanningAndOutputCheck && ViewModel!.HasOutputMetadata) + @if(!DisablePlanningAndOutputCheck && ViewModel.HasOutputMetadata) {
- - -
+ + +
} - @if(ViewModel!.OutputProvider && ViewModel.HasOutputMetadata) + @if(ViewModel.OutputProvider && ViewModel.HasOutputMetadata) {
- @if((CommandDesignerViewModel.MetadataDeviceName ?? CommandDesignerViewModel.TemplateDeviceName) is null) + @if(CommandDesignerViewModel.CommandTargetName is null) {
- UNKNOWN_DEVICE + UNKNOWN TARGET
} else {
- @(CommandDesignerViewModel.MetadataDeviceName ?? CommandDesignerViewModel.TemplateDeviceName) + @CommandDesignerViewModel.CommandTargetName
} -
+
@CommandDesignerViewModel.TemplateCommandName
@if (CommandDesignerViewModel.Arguments.Any()) @@ -78,6 +78,12 @@ [Parameter] public CommandDesignerViewModel? CommandDesignerViewModel { get; set; } + protected override async Task OnParametersSetAsync() + { + if(CommandDesignerViewModel is not null) + await CommandDesignerViewModel.EnsureInitializedAsync(); + } + public void NotifyOfParameterError(string message) { notificationService.Error(message, "Experiment Parameter Assignment Error"); @@ -111,4 +117,3 @@ return $"{argument.Metadata.Name}: Custom Data"; } } - diff --git a/UI/Features/CampaignEdit/Views/MetadataPicker.razor b/UI/Features/CampaignEdit/Views/MetadataPicker.razor index f92a7312..968fb14c 100644 --- a/UI/Features/CampaignEdit/Views/MetadataPicker.razor +++ b/UI/Features/CampaignEdit/Views/MetadataPicker.razor @@ -62,7 +62,7 @@
@code { [Parameter] - public EventCallback CommandMetadataChanged { get; set; } + public EventCallback DeviceCommandMetadataChanged { get; set; } protected override async Task OnInitializedAsync() { @@ -77,7 +77,7 @@ return; ViewModel!.SelectMetadata(metaName); - await CommandMetadataChanged.InvokeAsync(ViewModel!.SelectedCommandMetadata); + await DeviceCommandMetadataChanged.InvokeAsync(ViewModel!.SelectedCommandMetadata); } private async Task DeviceSelectionChanged(ChangeEventArgs obj) @@ -87,7 +87,7 @@ return; await ViewModel!.SelectDevice(deviceId); - await CommandMetadataChanged.InvokeAsync(ViewModel!.SelectedCommandMetadata); + await DeviceCommandMetadataChanged.InvokeAsync(ViewModel!.SelectedCommandMetadata); } -} \ No newline at end of file +} diff --git a/UI/Features/CampaignEdit/Views/StepDesigner.razor b/UI/Features/CampaignEdit/Views/StepDesigner.razor index 8aef80d7..0887ced2 100644 --- a/UI/Features/CampaignEdit/Views/StepDesigner.razor +++ b/UI/Features/CampaignEdit/Views/StepDesigner.razor @@ -76,8 +76,9 @@ private bool ContainsExperimentOutput => ViewModel!.CommandDesigners.Any(cmd => cmd.TemplateOutputProvider); - protected override void OnParametersSet() + protected override async Task OnParametersSetAsync() { + await ViewModel!.EnsureInitializedAsync(); ViewModel!.SetPriorVariableReferences(PriorVariableReferences); } @@ -146,7 +147,7 @@ private async void CommandDeleteClick(CommandDesignerViewModel vm) { - var result = await DialogService.OpenAsync($"Delete command {vm.CommandMetadata?.DeviceId}:{vm.CommandMetadata?.Name}?", ds => + var result = await DialogService.OpenAsync($"Delete command {vm.CommandTargetName}:{vm.TemplateCommandName}?", ds => @
diff --git a/UI/Features/CustomCommand/CustomCommandBuilder.razor b/UI/Features/CustomCommand/CustomCommandBuilder.razor new file mode 100644 index 00000000..d30f691f --- /dev/null +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor @@ -0,0 +1,514 @@ +@page "/automation/custom-command-builder" +@page "/automation/custom-command-builder/{CommandId:guid}" +@using Ares.Core.CustomCommands +@using Ares.Datamodel +@using Ares.Datamodel.Automation +@using AresScript.ScriptBuilding +@using AresScript.Symbols +@using UI.Application.Notifications +@using UI.Application.Scripting +@using CustomCommandModel = Ares.Datamodel.Automation.CustomCommandVersion +@using CustomCommandParameterModel = Ares.Datamodel.Automation.CustomCommandParameter +@inject IMonacoCompletionProvider CompletionProvider +@inject IMonacoDiagnosticsProvider DiagnosticsProvider +@inject IMonacoSemanticTokensProvider SemanticTokensProvider +@inject IMonacoHoverProvider HoverProvider +@inject ICustomCommandPersistenceService CustomCommandService +@inject IUiNotificationService NotificationService +@inject NavigationManager NavigationManager + +Custom Command Designer + +
+
+
+ + + Custom Commands + +

Custom Command Designer

+

@Subtitle

+
+ +
+ +
+
+ + @if(_isLoading) + { +
Loading custom command...
+ } + else if(!string.IsNullOrWhiteSpace(_loadError)) + { +
@_loadError
+ } + +
+ Command Metadata + + +
+ +
+
+ Input Parameters + +
+ @_inputParameters.Count parameter(s) + +
+ +
+ @foreach(var parameter in _inputParameters) + { +
+ + + + + +
+ } +
+ + @if(SelectedParameter is not null) + { + + } + else + { +
+ Add a parameter to define its schema. +
+ } +
+ +
+ Output Value Schema + +
+
+ +
+ Command Diagnostics + + @if(_commandDiagnostics.Count == 0) + { +
No command diagnostics.
+ } + else + { +
+ @foreach(var diagnostic in _commandDiagnostics) + { +
+ @GetDiagnosticSeverityText(diagnostic) + @diagnostic.Location + @diagnostic.Message +
+ } +
+ } +
+ +
+ Script Body +
+ @GeneratedFunctionSignature: +
+
+ +
+
+
+ +@code { + [Parameter] + public Guid? CommandId { get; set; } + + private Guid? _selectedParameterId; + private Guid? _loadedCommandId; + private ScriptEditor? _scriptEditor; + private CustomCommandMonacoProviderAdapter? _languageProviderAdapter; + private IReadOnlyList _commandDiagnostics = []; + private bool _isLoading; + private bool _isSaving; + private string? _loadError; + private string _commandName = "New Custom Command"; + private string _commandDescription = string.Empty; + private string _scriptBody = """ +return +"""; + + private readonly List _inputParameters = []; + + private AresValueSchema _outputSchema = new() + { + Type = AresDataType.Unit + }; + + private string Subtitle => CommandId is null + ? "Create a new custom command definition." + : $"Editing custom command {CommandId}."; + + private string SaveButtonText => _isSaving ? "Saving" : "Save"; + + private string GeneratedFunctionSignature + => CustomCommandScriptBuilder.BuildFunctionSignature(_commandName, BuildScriptParameters(), _outputSchema); + + private CustomCommandMonacoProviderAdapter LanguageProviderAdapter + { + get + { + _languageProviderAdapter ??= new CustomCommandMonacoProviderAdapter( + CompletionProvider, + DiagnosticsProvider, + SemanticTokensProvider, + HoverProvider, + BuildScriptContext, + OnCommandDiagnosticsChanged); + return _languageProviderAdapter; + } + } + + private string DiagnosticsPanelClass + => _commandDiagnostics.Count == 0 + ? "custom-command-builder__fieldset custom-command-builder__diagnostics-panel" + : "custom-command-builder__fieldset custom-command-builder__diagnostics-panel custom-command-builder__diagnostics-panel--has-items"; + + private CustomCommandParameterDraft? SelectedParameter + => _inputParameters.FirstOrDefault(parameter => parameter.Id == SelectedParameterId); + + private Guid? SelectedParameterId + { + get + { + _selectedParameterId ??= _inputParameters.FirstOrDefault()?.Id; + return _selectedParameterId; + } + set => _selectedParameterId = value; + } + + private void SelectParameter(CustomCommandParameterDraft parameter) + { + SelectedParameterId = parameter.Id; + } + + protected override async Task OnParametersSetAsync() + { + if(CommandId == _loadedCommandId) + { + return; + } + + if(CommandId is null) + { + ResetDraftState(); + _loadedCommandId = null; + _loadError = null; + return; + } + + await LoadCommandAsync(CommandId.Value); + } + + private void AddParameter() + { + var parameter = new CustomCommandParameterDraft(GetNextParameterName(), new AresValueSchema + { + Type = AresDataType.String, + Description = "Describe this parameter." + }); + + _inputParameters.Add(parameter); + SelectedParameterId = parameter.Id; + _ = RefreshLanguageFeaturesAsync(); + } + + private void RemoveParameter(CustomCommandParameterDraft parameter) + { + _inputParameters.Remove(parameter); + + if(SelectedParameterId == parameter.Id) + SelectedParameterId = _inputParameters.FirstOrDefault()?.Id; + + _ = RefreshLanguageFeaturesAsync(); + } + + private void OnParameterNameChanged(CustomCommandParameterDraft parameter, ChangeEventArgs args) + { + var newName = args.Value?.ToString()?.Trim() ?? string.Empty; + if(!string.IsNullOrWhiteSpace(newName)) + { + parameter.Name = newName; + _ = RefreshLanguageFeaturesAsync(); + } + } + + private void OnSelectedParameterSchemaChanged(AresValueSchema schema) + { + if(SelectedParameter is not null) + { + SelectedParameter.Schema = schema.Clone(); + _ = RefreshLanguageFeaturesAsync(); + } + } + + private void OnOutputSchemaChanged(AresValueSchema schema) + { + _outputSchema = schema.Clone(); + _ = RefreshLanguageFeaturesAsync(); + } + + private string GetParameterRowClass(CustomCommandParameterDraft parameter) + { + var selectedClass = parameter.Id == SelectedParameterId ? " custom-command-builder__parameter-row--selected" : string.Empty; + return $"custom-command-builder__parameter-row{selectedClass}"; + } + + private string GetNextParameterName() + { + var index = _inputParameters.Count + 1; + var name = $"parameter_{index}"; + + while(_inputParameters.Any(parameter => string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase))) + { + index++; + name = $"parameter_{index}"; + } + + return name; + } + + private IEnumerable BuildScriptParameters() + { + return _inputParameters.Select(parameter => new AresScriptParameter(parameter.Name, parameter.Schema)); + } + + private CustomCommandScriptContext BuildScriptContext() + { + return new CustomCommandScriptContext(_commandName, BuildScriptParameters().ToArray(), _outputSchema); + } + + private async Task OnCommandNameChanged(string _) + { + await RefreshLanguageFeaturesAsync(); + } + + private async Task LoadCommandAsync(Guid commandId) + { + _isLoading = true; + _loadError = null; + + try + { + var command = await CustomCommandService.GetAsync(commandId); + if(command is null) + { + _loadError = $"Custom command {commandId} was not found."; + ResetDraftState(); + return; + } + + ApplyCommand(command); + _loadedCommandId = commandId; + await RefreshLanguageFeaturesAsync(); + } + catch(Exception ex) + { + _loadError = $"Failed to load custom command. {ex.Message}"; + NotificationService.Error(_loadError); + } + finally + { + _isLoading = false; + } + } + + private async Task SaveAsync() + { + _isSaving = true; + + try + { + var command = await BuildCommandAsync(); + var savedId = await CustomCommandService.SaveAsync(CommandId, command); + CommandId = savedId; + _loadedCommandId = savedId; + NotificationService.Success("Custom command saved."); + NavigationManager.NavigateTo($"/automation/custom-command-builder/{savedId}", replace: true); + } + catch(Exception ex) + { + NotificationService.Error($"Failed to save custom command. {ex.Message}"); + } + finally + { + _isSaving = false; + } + } + + private async Task BuildCommandAsync() + { + if(_scriptEditor is not null) + { + _scriptBody = await _scriptEditor.GetScript(); + } + + var command = new CustomCommandModel + { + Name = _commandName, + Description = _commandDescription, + OutputSchema = _outputSchema.Clone(), + ScriptBody = _scriptBody + }; + + command.InputParameters.AddRange(_inputParameters.Select(parameter => new CustomCommandParameterModel + { + Name = parameter.Name, + Schema = parameter.Schema.Clone() + })); + + return command; + } + + private void ApplyCommand(CustomCommandModel command) + { + _commandName = command.Name; + _commandDescription = command.Description; + _scriptBody = command.ScriptBody; + _outputSchema = command.OutputSchema?.Clone() ?? new AresValueSchema { Type = AresDataType.Unit }; + + _inputParameters.Clear(); + _inputParameters.AddRange(command.InputParameters.Select(parameter => new CustomCommandParameterDraft( + parameter.Name, + parameter.Schema?.Clone() ?? new AresValueSchema { Type = AresDataType.String }))); + + SelectedParameterId = _inputParameters.FirstOrDefault()?.Id; + + if(_scriptEditor is not null) + { + _ = _scriptEditor.SetScript(_scriptBody); + } + } + + private void ResetDraftState() + { + _commandName = "New Custom Command"; + _commandDescription = string.Empty; + _scriptBody = """ +return +"""; + _outputSchema = new AresValueSchema + { + Type = AresDataType.Unit + }; + + _inputParameters.Clear(); + SelectedParameterId = _inputParameters.FirstOrDefault()?.Id; + + if(_scriptEditor is not null) + { + _ = _scriptEditor.SetScript(_scriptBody); + } + } + + private async Task RefreshLanguageFeaturesAsync() + { + if(_scriptEditor is not null) + { + await _scriptEditor.RefreshLanguageFeaturesAsync(); + } + } + + private void OnCommandDiagnosticsChanged(IReadOnlyList diagnostics) + { + _ = InvokeAsync(() => + { + _commandDiagnostics = diagnostics; + StateHasChanged(); + }); + } + + private static string GetDiagnosticClass(CustomCommandDiagnostic diagnostic) + { + return diagnostic.Severity switch + { + 0 => "custom-command-builder__diagnostic custom-command-builder__diagnostic--error", + 1 => "custom-command-builder__diagnostic custom-command-builder__diagnostic--warning", + _ => "custom-command-builder__diagnostic" + }; + } + + private static string GetDiagnosticSeverityText(CustomCommandDiagnostic diagnostic) + { + return diagnostic.Severity switch + { + 0 => "Error", + 1 => "Warning", + 2 => "Info", + 3 => "Hint", + _ => "Diagnostic" + }; + } + + private sealed class CustomCommandParameterDraft + { + public CustomCommandParameterDraft(string name, AresValueSchema schema) + { + Id = Guid.NewGuid(); + Name = name; + Schema = schema; + } + + public Guid Id { get; } + public string Name { get; set; } + public AresValueSchema Schema { get; set; } + } +} diff --git a/UI/Features/CustomCommand/CustomCommandBuilder.razor.css b/UI/Features/CustomCommand/CustomCommandBuilder.razor.css new file mode 100644 index 00000000..8f3146f0 --- /dev/null +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor.css @@ -0,0 +1,311 @@ +.custom-command-builder { + display: flex; + flex: 1; + flex-direction: column; + gap: 1rem; + min-width: 0; + overflow-x: hidden; +} + +.custom-command-builder__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + min-width: 0; +} + +.custom-command-builder__title { + margin: 0 0 0.25rem; +} + +.custom-command-builder__back-link { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin-bottom: 0.5rem; + color: inherit; + font-size: 0.9rem; + text-decoration: none; +} + +.custom-command-builder__back-link:hover { + color: var(--rz-primary, inherit); +} + +.custom-command-builder__subtitle { + margin: 0; + color: var(--rz-text-secondary-color, #6c757d); +} + +.custom-command-builder__actions, +.custom-command-builder__section-header { + display: flex; + align-items: flex-start; + gap: 0.5rem; +} + +.custom-command-builder__actions { + flex: 0 0 auto; +} + +.custom-command-builder__section-header { + align-items: center; + justify-content: space-between; + min-width: 0; +} + +.custom-command-builder__metadata-grid, +.custom-command-builder__schema-grid { + display: grid; + gap: 1rem; + min-width: 0; +} + +.custom-command-builder__metadata-grid { + grid-template-columns: minmax(16rem, 1fr) minmax(18rem, 2fr); +} + +.custom-command-builder__schema-grid { + grid-template-columns: minmax(22rem, 1fr) minmax(22rem, 1fr); +} + +.custom-command-builder__fieldset { + min-width: 0; + margin: 0; + padding: 1.125rem; + border: 1px solid var(--rz-base-300, #ced4da); + border-radius: 8px; + background: var(--rz-card-background-color, var(--rz-base-background-color, #fff)); + box-shadow: 0 0.25rem 0.85rem rgba(0, 0, 0, 0.08); +} + +.custom-command-builder__state { + padding: 1rem; + border: 1px solid var(--rz-base-300, #ced4da); + border-radius: 6px; + color: var(--rz-text-secondary-color, #6c757d); +} + +.custom-command-builder__state--error { + border-color: var(--rz-danger, #dc3545); + color: var(--rz-danger, #dc3545); +} + +.custom-command-builder__legend { + float: none; + width: auto; + margin: 0; + padding: 0 0.5rem; + color: var(--rz-text-color, inherit); + font-size: 1rem; + font-weight: 600; +} + +.custom-command-builder__field { + display: flex; + flex-direction: column; + gap: 0.375rem; + min-width: 0; +} + +.custom-command-builder__label { + margin: 0; +} + +.custom-command-builder__parameters-panel, +.custom-command-builder__output-panel, +.custom-command-builder__script-panel, +.custom-command-builder__diagnostics-panel { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.custom-command-builder__parameter-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + min-width: 0; +} + +.custom-command-builder__parameter-row { + display: grid; + grid-template-columns: minmax(9rem, 1fr) minmax(8rem, 1fr) auto; + gap: 0.5rem; + align-items: center; + min-width: 0; + padding: 0.5rem; + border: 1px solid var(--rz-base-300, #ced4da); + border-radius: 6px; +} + +.custom-command-builder__parameter-row--selected { + border-color: var(--rz-primary, #0d6efd); + background: var(--rz-primary-lighter, rgba(13, 110, 253, 0.12)); +} + +.custom-command-builder__parameter-select { + display: flex; + flex-direction: column; + align-items: flex-start; + min-width: 0; + border: 0; + background: transparent; + color: inherit; + cursor: pointer; + padding: 0; + text-align: left; +} + +.custom-command-builder__parameter-name, +.custom-command-builder__parameter-type { + max-width: 100%; + overflow-wrap: anywhere; +} + +.custom-command-builder__parameter-name { + font-weight: 600; +} + +.custom-command-builder__parameter-type { + color: var(--rz-text-secondary-color, #6c757d); + font-size: 0.85rem; +} + +.custom-command-builder__parameter-name-input { + min-width: 0; + width: 100%; + border: 1px solid var(--rz-base-400, #dee2e6); + border-radius: 6px; + background: var(--rz-input-background-color, transparent); + color: inherit; + padding: 0.45rem 0.55rem; +} + +.custom-command-builder__icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + width: 2.25rem; + height: 2.25rem; + border: 1px solid var(--rz-base-400, #dee2e6); + border-radius: 6px; + background: transparent; + color: inherit; + cursor: pointer; +} + +.custom-command-builder__icon-button--danger { + color: var(--rz-danger, #dc3545); +} + +.custom-command-builder__empty-state { + border: 1px dashed var(--rz-base-400, #dee2e6); + border-radius: 6px; + color: var(--rz-text-secondary-color, #6c757d); + padding: 1rem; +} + +.custom-command-builder__signature-preview { + min-width: 0; + padding: 0.65rem 0.75rem; + border: 1px solid var(--rz-base-300, #ced4da); + border-radius: 6px; + background: var(--rz-base-600, rgba(0, 0, 0, 0.03)); + font-family: Consolas, "Courier New", monospace; + font-size: 0.9rem; + overflow-wrap: anywhere; +} + +.custom-command-builder__script-editor { + min-width: 0; + min-height: 28rem; + height: 45vh; + overflow: hidden; + border: 1px solid var(--rz-base-300, #ced4da); + border-radius: 6px; +} + +.custom-command-builder__diagnostics-panel--has-items { + border-color: var(--rz-danger, #dc3545); +} + +.custom-command-builder__diagnostics-empty { + color: var(--rz-text-secondary-color, #6c757d); +} + +.custom-command-builder__diagnostics-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + min-width: 0; +} + +.custom-command-builder__diagnostic { + display: grid; + grid-template-columns: auto minmax(8rem, auto) minmax(0, 1fr); + align-items: start; + gap: 0.6rem; + min-width: 0; + padding: 0.65rem 0.75rem; + border: 1px solid var(--rz-base-300, #ced4da); + border-radius: 6px; + background: var(--rz-base-600, rgba(0, 0, 0, 0.03)); +} + +.custom-command-builder__diagnostic--error { + border-color: var(--rz-danger, #dc3545); +} + +.custom-command-builder__diagnostic--warning { + border-color: var(--rz-warning, #ffc107); +} + +.custom-command-builder__diagnostic-severity { + font-weight: 700; +} + +.custom-command-builder__diagnostic-location { + color: var(--rz-text-secondary-color, #6c757d); +} + +.custom-command-builder__diagnostic-message { + min-width: 0; + overflow-wrap: anywhere; +} + +@media (max-width: 1200px) { + .custom-command-builder__schema-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 840px) { + .custom-command-builder__metadata-grid, + .custom-command-builder__schema-grid { + grid-template-columns: 1fr; + } + + .custom-command-builder__header { + flex-direction: column; + } + + .custom-command-builder__actions { + flex-wrap: wrap; + } + + .custom-command-builder__parameter-row { + grid-template-columns: 1fr auto; + } + + .custom-command-builder__parameter-name-input { + grid-column: 1 / -1; + grid-row: 2; + } + + .custom-command-builder__diagnostic { + grid-template-columns: 1fr; + } +} diff --git a/UI/Features/CustomCommand/CustomCommandList.razor b/UI/Features/CustomCommand/CustomCommandList.razor new file mode 100644 index 00000000..db2f64a0 --- /dev/null +++ b/UI/Features/CustomCommand/CustomCommandList.razor @@ -0,0 +1,135 @@ +@page "/automation/custom-commands" +@using Ares.Core.CustomCommands +@using UI.Application.Notifications +@inject ICustomCommandPersistenceService CustomCommandService +@inject IUiNotificationService NotificationService + +Custom Commands + +
+
+
+

Custom Commands

+

Select a custom command to edit or start a new definition.

+
+ +
+ + + + +
+
+ + @if(_isLoading) + { +
Loading custom commands...
+ } + else if(!string.IsNullOrWhiteSpace(_loadError)) + { +
@_loadError
+ } + else if(_commands.Count == 0) + { +
No custom commands have been saved yet.
+ } + else + { +
+ + + + + + + + + + + + @foreach(var command in _commands) + { + + + + + + + + } + +
NameDescriptionInputsOutput
+ + @command.Name + + @command.Description@command.InputSummary@command.OutputSummary +
+ + + + +
+
+
+ } +
+ +@code { + private IReadOnlyList _commands = []; + private bool _isLoading; + private bool _isDeleting; + private string? _loadError; + + protected override async Task OnInitializedAsync() + { + await LoadCommandsAsync(); + } + + private async Task LoadCommandsAsync() + { + _isLoading = true; + _loadError = null; + + try + { + _commands = await CustomCommandService.GetSummariesAsync(); + } + catch(Exception ex) + { + _loadError = $"Failed to load custom commands. {ex.Message}"; + NotificationService.Error(_loadError); + } + finally + { + _isLoading = false; + } + } + + private async Task DeleteCommandAsync(CustomCommandSummary command) + { + _isDeleting = true; + + try + { + await CustomCommandService.DeleteAsync(command.Id); + NotificationService.Success($"Deleted custom command '{command.Name}'."); + await LoadCommandsAsync(); + } + catch(Exception ex) + { + NotificationService.Error($"Failed to delete custom command '{command.Name}'. {ex.Message}"); + } + finally + { + _isDeleting = false; + } + } +} diff --git a/UI/Features/CustomCommand/CustomCommandList.razor.css b/UI/Features/CustomCommand/CustomCommandList.razor.css new file mode 100644 index 00000000..169a7280 --- /dev/null +++ b/UI/Features/CustomCommand/CustomCommandList.razor.css @@ -0,0 +1,103 @@ +.custom-command-list { + display: flex; + flex: 1; + flex-direction: column; + gap: 1rem; + min-width: 0; + overflow-x: hidden; +} + +.custom-command-list__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + min-width: 0; +} + +.custom-command-list__title { + margin: 0 0 0.25rem; +} + +.custom-command-list__subtitle { + margin: 0; + color: var(--rz-text-secondary-color, #6c757d); +} + +.custom-command-list__actions, +.custom-command-list__row-actions { + display: flex; + align-items: center; +} + +.custom-command-list__actions { + flex: 0 0 auto; +} + +.custom-command-list__row-actions { + justify-content: flex-end; +} + +.custom-command-list__name-link { + color: var(--rz-primary, #0d6efd); + font-weight: 600; + text-decoration: none; +} + +.custom-command-list__name-link:hover { + text-decoration: underline; +} + +.custom-command-list__table-wrapper { + min-width: 0; + overflow-x: auto; + border: 1px solid var(--rz-base-400, #dee2e6); + border-radius: 6px; +} + +.custom-command-list__state { + padding: 1rem; + border: 1px solid var(--rz-base-300, #ced4da); + border-radius: 6px; + color: var(--rz-text-secondary-color, #6c757d); +} + +.custom-command-list__state--error { + border-color: var(--rz-danger, #dc3545); + color: var(--rz-danger, #dc3545); +} + +.custom-command-list__table { + width: 100%; + min-width: 42rem; + border-collapse: collapse; + table-layout: fixed; +} + +.custom-command-list__table th, +.custom-command-list__table td { + padding: 0.75rem; + border-bottom: 1px solid var(--rz-base-300, #ced4da); + text-align: left; + vertical-align: middle; + overflow-wrap: anywhere; +} + +.custom-command-list__table th { + font-weight: 600; +} + +.custom-command-list__table tbody tr:last-child td { + border-bottom: 0; +} + +.custom-command-list__table th:last-child, +.custom-command-list__table td:last-child { + width: 7.25rem; +} + +@media (max-width: 720px) { + .custom-command-list__header { + flex-direction: column; + } +} diff --git a/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs b/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs new file mode 100644 index 00000000..37af9bd3 --- /dev/null +++ b/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs @@ -0,0 +1,147 @@ +using Ares.Datamodel; +using AresScript.ScriptBuilding; +using AresScript.Symbols; +using Microsoft.JSInterop; +using UI.Application.Scripting; +using MonacoCompletionItem = BlazorMonaco.Languages.CompletionItem; + +namespace UI.Features.CustomCommand; + +internal sealed class CustomCommandMonacoProviderAdapter( + IMonacoCompletionProvider completionProvider, + IMonacoDiagnosticsProvider diagnosticsProvider, + IMonacoSemanticTokensProvider semanticTokensProvider, + IMonacoHoverProvider hoverProvider, + Func getContext, + Action>? diagnosticsChanged = null) + : IMonacoCompletionProvider, IMonacoDiagnosticsProvider, IMonacoSemanticTokensProvider, IMonacoHoverProvider +{ + private const int WrappedBodyLineOffset = 1; + private const int WrappedBodyColumnOffset = 2; + + [JSInvokable] + public Task GetCompletionItems(string script, int line, int column) + { + var wrappedScript = BuildWrappedScript(script); + return completionProvider.GetCompletionItems( + wrappedScript, + ToWrappedBodyLine(line), + ToWrappedBodyColumn(column)); + } + + [JSInvokable] + public async Task GetDiagnostics(string script) + { + var wrappedScript = BuildWrappedScript(script); + var diagnostics = await diagnosticsProvider.GetDiagnostics(wrappedScript); + diagnosticsChanged?.Invoke(diagnostics.Select(MapCommandDiagnostic).ToArray()); + return diagnostics.Select(MapDiagnostic).ToArray(); + } + + [JSInvokable] + public SemanticToken[] GetSemanticTokens(string script) + { + try + { + var wrappedScript = BuildWrappedScript(script); + return MapSemanticTokens(semanticTokensProvider.GetSemanticTokens(wrappedScript)); + } + catch + { + return []; + } + } + + internal static SemanticToken[] MapSemanticTokens(IEnumerable tokens) + { + return tokens + .Where(token => token.Line >= WrappedBodyLineOffset) + .Select(token => token with + { + Line = token.Line - WrappedBodyLineOffset, + StartColumn = Math.Max(0, token.StartColumn - WrappedBodyColumnOffset) + }) + .ToArray(); + } + + [JSInvokable] + public Task GetHoverText(string script, int line, int column, string identifier) + { + var wrappedScript = BuildWrappedScript(script); + return hoverProvider.GetHoverText( + wrappedScript, + ToWrappedBodyLine(line), + ToWrappedBodyColumn(column), + identifier); + } + + internal static int ToWrappedBodyLine(int bodyLine) => Math.Max(1, bodyLine) + WrappedBodyLineOffset; + + internal static int ToWrappedBodyColumn(int bodyColumn) => Math.Max(1, bodyColumn) + WrappedBodyColumnOffset; + + internal static int ToBodyLine(int wrappedLine) => Math.Max(1, wrappedLine - WrappedBodyLineOffset); + + internal static int ToBodyColumn(int wrappedColumn) => Math.Max(1, wrappedColumn - WrappedBodyColumnOffset); + + internal static MonacoDiagnostic MapDiagnostic(MonacoDiagnostic diagnostic) + { + if(diagnostic.StartLineNumber <= WrappedBodyLineOffset) + { + return diagnostic with + { + StartLineNumber = 1, + StartColumn = 1, + EndLineNumber = 1, + EndColumn = Math.Max(1, diagnostic.EndColumn), + Message = $"Generated signature: {diagnostic.Message}" + }; + } + + return diagnostic with + { + StartLineNumber = ToBodyLine(diagnostic.StartLineNumber), + StartColumn = ToBodyColumn(diagnostic.StartColumn), + EndLineNumber = ToBodyLine(diagnostic.EndLineNumber), + EndColumn = ToBodyColumn(diagnostic.EndColumn) + }; + } + + internal static CustomCommandDiagnostic MapCommandDiagnostic(MonacoDiagnostic diagnostic) + { + if(diagnostic.StartLineNumber <= WrappedBodyLineOffset) + { + return new CustomCommandDiagnostic( + "Generated Signature", + diagnostic.Message, + diagnostic.Severity, + diagnostic.Code); + } + + return new CustomCommandDiagnostic( + $"Script Body line {ToBodyLine(diagnostic.StartLineNumber)}", + diagnostic.Message, + diagnostic.Severity, + diagnostic.Code); + } + + private string BuildWrappedScript(string body) + { + var context = getContext(); + return CustomCommandScriptBuilder.BuildWrappedScript( + context.CommandName, + context.Parameters, + context.OutputSchema, + body); + } +} + +internal sealed record CustomCommandScriptContext( + string CommandName, + IReadOnlyList Parameters, + AresValueSchema OutputSchema); + +internal sealed record CustomCommandDiagnostic( + string Location, + string Message, + int Severity, + string? Code); diff --git a/UI/Infrastructure/Monaco/Interops/MonacoSemanticTokensProvider.cs b/UI/Infrastructure/Monaco/Interops/MonacoSemanticTokensProvider.cs index d81a3963..c36ea239 100644 --- a/UI/Infrastructure/Monaco/Interops/MonacoSemanticTokensProvider.cs +++ b/UI/Infrastructure/Monaco/Interops/MonacoSemanticTokensProvider.cs @@ -1,5 +1,5 @@ -using Microsoft.JSInterop; using AresScript.ScriptAnalysis; +using Microsoft.JSInterop; using UI.Application.Scripting; namespace UI.Infrastructure.Monaco.Interops; diff --git a/UI/Infrastructure/Monaco/Scripts/ares-diagnostics-setup.ts b/UI/Infrastructure/Monaco/Scripts/ares-diagnostics-setup.ts index 2b710ef4..6ab0ec1e 100644 --- a/UI/Infrastructure/Monaco/Scripts/ares-diagnostics-setup.ts +++ b/UI/Infrastructure/Monaco/Scripts/ares-diagnostics-setup.ts @@ -15,6 +15,7 @@ let diagnosticsModel: editor.ITextModel | null = null; let diagnosticsContentChangeDisposable: IDisposable | null = null; let diagnosticsModelDisposeDisposable: IDisposable | null = null; let diagnosticsTimer: number | undefined; +let refreshDiagnosticsCallback: (() => void) | null = null; export function setupDiagnostics(diagnosticsService: DotNet.DotNetObject, debounceMs = 250) { if (typeof monaco === 'undefined') { @@ -54,6 +55,7 @@ export function setupDiagnostics(diagnosticsService: DotNet.DotNetObject, deboun }) .catch((err) => console.error('Failed to fetch diagnostics', err)); }; + refreshDiagnosticsCallback = updateMarkers; const debouncedUpdate = () => { if (diagnosticsTimer !== undefined) { @@ -69,6 +71,10 @@ export function setupDiagnostics(diagnosticsService: DotNet.DotNetObject, deboun updateMarkers(); } +export function refreshDiagnostics() { + refreshDiagnosticsCallback?.(); +} + export function disposeDiagnostics() { if (diagnosticsTimer !== undefined) { clearTimeout(diagnosticsTimer); @@ -81,6 +87,8 @@ export function disposeDiagnostics() { diagnosticsModelDisposeDisposable?.dispose(); diagnosticsModelDisposeDisposable = null; + refreshDiagnosticsCallback = null; + if (diagnosticsModel && !diagnosticsModel.isDisposed()) { monaco.editor.setModelMarkers(diagnosticsModel, 'ares', []); } diff --git a/UI/Infrastructure/Monaco/Scripts/ares-semantic-setup.ts b/UI/Infrastructure/Monaco/Scripts/ares-semantic-setup.ts index 4327b6e5..2cd386cf 100644 --- a/UI/Infrastructure/Monaco/Scripts/ares-semantic-setup.ts +++ b/UI/Infrastructure/Monaco/Scripts/ares-semantic-setup.ts @@ -15,6 +15,7 @@ const legend: languages.SemanticTokensLegend = { }; let semanticTokensDisposable: IDisposable | null = null; +const semanticTokenListeners = new Set<() => void>(); export function setupSemanticTokens(provider: DotNet.DotNetObject) { if (typeof monaco === 'undefined') { @@ -27,10 +28,18 @@ export function setupSemanticTokens(provider: DotNet.DotNetObject) { getLegend() { return legend; }, + onDidChange(listener) { + semanticTokenListeners.add(listener); + return { + dispose() { + semanticTokenListeners.delete(listener); + } + }; + }, provideDocumentSemanticTokens(model: editor.ITextModel) { return provider.invokeMethodAsync('GetSemanticTokens', model.getValue()) .then((tokens) => { - const data = encodeSemanticTokens(tokens as SemanticToken[]); + const data = encodeSemanticTokens(model, tokens as SemanticToken[]); return { data }; }); }, @@ -40,14 +49,19 @@ export function setupSemanticTokens(provider: DotNet.DotNetObject) { }); } +export function refreshSemanticTokens() { + semanticTokenListeners.forEach(listener => listener()); +} + export function disposeSemanticTokens() { semanticTokensDisposable?.dispose(); semanticTokensDisposable = null; + semanticTokenListeners.clear(); } -function encodeSemanticTokens(tokens: SemanticToken[]): Uint32Array { +function encodeSemanticTokens(model: editor.ITextModel, tokens: SemanticToken[]): Uint32Array { const sorted = tokens - .filter(t => t.length > 0) + .filter(t => t.length > 0 && t.line >= 0 && t.line < model.getLineCount() && t.startColumn >= 0) .sort((a, b) => a.line === b.line ? a.startColumn - b.startColumn : a.line - b.line); const data: number[] = []; @@ -55,16 +69,22 @@ function encodeSemanticTokens(tokens: SemanticToken[]): Uint32Array { let lastChar = 0; for (const token of sorted) { - const line = Math.max(1, token.line); - const start = Math.max(1, token.startColumn); + const line = token.line; + const start = token.startColumn; + const lineLength = model.getLineLength(line + 1); + if (start >= lineLength) { + continue; + } + + const length = Math.min(token.length, lineLength - start); const tokenType = legend.tokenTypes.indexOf(token.type); if (tokenType < 0) { continue; } const lineDelta = line - lastLine; - const charDelta = lineDelta === 0 ? start - lastChar : start - 1; - data.push(lineDelta, charDelta, token.length, tokenType, 0); + const charDelta = lineDelta === 0 ? start - lastChar : start; + data.push(lineDelta, charDelta, length, tokenType, 0); lastLine = line; lastChar = start; diff --git a/UI/Infrastructure/Persistence/Scripts/AddMigration.zsh b/UI/Infrastructure/Persistence/Scripts/AddMigration.zsh old mode 100644 new mode 100755 index 1be9e844..6567c2ab --- a/UI/Infrastructure/Persistence/Scripts/AddMigration.zsh +++ b/UI/Infrastructure/Persistence/Scripts/AddMigration.zsh @@ -102,10 +102,10 @@ if ! dotnet ef --help >/dev/null 2>&1; then fi script_dir="$(cd "$(dirname "$0")" && pwd)" -root="$(cd "$script_dir/../../" && pwd)" +root="$(cd "$script_dir/../../../.." && pwd)" migration_project_base="AresService.Migrations." -startup_project="$script_dir/../AresService.csproj" -solution="$root/AresOS.sln" +startup_project="$script_dir/../../../UI.csproj" +solution="$root/AresOS.slnx" contexts=("AresDbContext" "AresIdentityContext") echo "Building project in $configuration mode..." diff --git a/UI/UI.csproj b/UI/UI.csproj index 3262f9ad..2fc41f2c 100644 --- a/UI/UI.csproj +++ b/UI/UI.csproj @@ -29,7 +29,7 @@ - + @@ -64,7 +64,8 @@ - + +