From ca144c86d4ec7367c6f4c46811bfde2aed58d168 Mon Sep 17 00:00:00 2001 From: "Babeckis, Arnas" Date: Thu, 25 Jun 2026 12:19:11 -0400 Subject: [PATCH 01/32] Initial command builder placeholder --- UI/Components/Layouts/MainLayout.razor | 4 +- .../CustomCommand/CustomCommandBuilder.razor | 146 ++++++++++++++++ .../CustomCommandBuilder.razor.css | 163 ++++++++++++++++++ .../CustomCommand/CustomCommandList.razor | 87 ++++++++++ .../CustomCommand/CustomCommandList.razor.css | 110 ++++++++++++ 5 files changed, 508 insertions(+), 2 deletions(-) create mode 100644 UI/Features/CustomCommand/CustomCommandBuilder.razor create mode 100644 UI/Features/CustomCommand/CustomCommandBuilder.razor.css create mode 100644 UI/Features/CustomCommand/CustomCommandList.razor create mode 100644 UI/Features/CustomCommand/CustomCommandList.razor.css diff --git a/UI/Components/Layouts/MainLayout.razor b/UI/Components/Layouts/MainLayout.razor index 5211faa6..e56db8a5 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/Features/CustomCommand/CustomCommandBuilder.razor b/UI/Features/CustomCommand/CustomCommandBuilder.razor new file mode 100644 index 00000000..3c685d2c --- /dev/null +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor @@ -0,0 +1,146 @@ +@page "/automation/custom-command-builder" +@page "/automation/custom-command-builder/{CommandId:guid}" + +Custom Command Designer + +
+
+
+ + + Custom Commands + +

Custom Command Designer

+

@Subtitle

+
+ +
+ + +
+
+ +
+
+
+ Command Metadata + +
+ + +
+ +
+ + +
+
+
+ +
+
+ Input Parameters + +
+ + + + + + + + + + + @foreach(var parameter in _inputParameters) + { + + + + + + + } + +
NameTypeOptionalDescription
@parameter.Name@parameter.Type@(parameter.Optional ? "Yes" : "No")@parameter.Description
+
+
+
+ +
+
+ Output Schema + +
+
Type
+
@_outputSchema.Type
+ +
Optional
+
@(_outputSchema.Optional ? "Yes" : "No")
+ +
Quantity
+
@_outputSchema.Quantity
+ +
Bounds
+
@_outputSchema.Bounds
+ +
Description
+
@_outputSchema.Description
+
+
+
+
+ +
+ Script Body + +
+
+ +@code { + [Parameter] + public Guid? CommandId { get; set; } + + private string _commandName = "Measure Temperature"; + private string _commandDescription = "Reads a temperature value from a configured source and returns it as a quantity."; + private string _scriptBody = """ +def main(sample_id, timeout_seconds): + # Placeholder script editor. + return { + "value": 21.5, + "unit": "degC" + } +"""; + + private readonly IReadOnlyList _inputParameters = + [ + new("sample_id", "STRING", false, "Identifier for the sample to measure."), + new("timeout_seconds", "NUMBER", true, "Maximum time to wait before failing."), + new("include_metadata", "BOOLEAN", true, "Include device metadata in the command context.") + ]; + + private readonly SchemaPlaceholder _outputSchema = new( + "QUANTITY", + false, + "Temperature", + "0 to 100 degC", + "Measured sample temperature."); + + private string Subtitle => CommandId is null + ? "Create a new custom command definition." + : $"Editing placeholder custom command {CommandId}."; + + private record ParameterPlaceholder(string Name, string Type, bool Optional, string Description); + + private record SchemaPlaceholder(string Type, bool Optional, string Quantity, string Bounds, string Description); +} diff --git a/UI/Features/CustomCommand/CustomCommandBuilder.razor.css b/UI/Features/CustomCommand/CustomCommandBuilder.razor.css new file mode 100644 index 00000000..b12f52c6 --- /dev/null +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor.css @@ -0,0 +1,163 @@ +.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 { + display: flex; + align-items: flex-start; + gap: 0.5rem; + flex: 0 0 auto; +} + +.custom-command-builder__summary-grid { + display: grid; + grid-template-columns: minmax(16rem, 4fr) minmax(22rem, 5fr) minmax(14rem, 3fr); + gap: 1rem; + min-width: 0; +} + +.custom-command-builder__panel, +.custom-command-builder__fieldset, +.custom-command-builder__table-wrapper, +.custom-command-builder__parameter-table { + min-width: 0; +} + +.custom-command-builder__panel { + display: flex; +} + +.custom-command-builder__fieldset { + flex: 1; + margin: 0; + padding: 1rem; + border: 1px solid var(--rz-base-400, #dee2e6); + border-radius: 6px; +} + +.custom-command-builder__legend { + float: none; + width: auto; + margin: 0; + padding: 0 0.5rem; + font-size: 1rem; +} + +.custom-command-builder__field { + display: flex; + flex-direction: column; + gap: 0.375rem; + min-width: 0; +} + +.custom-command-builder__field + .custom-command-builder__field { + margin-top: 1rem; +} + +.custom-command-builder__label { + margin: 0; +} + +.custom-command-builder__table-wrapper { + overflow-x: auto; +} + +.custom-command-builder__parameter-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; +} + +.custom-command-builder__parameter-table th, +.custom-command-builder__parameter-table td { + padding: 0.35rem 0.45rem; + border-bottom: 1px solid var(--rz-base-300, #ced4da); + text-align: left; + vertical-align: middle; + overflow-wrap: anywhere; +} + +.custom-command-builder__parameter-table th { + font-weight: 600; +} + +.custom-command-builder__parameter-table tbody tr:last-child td { + border-bottom: 0; +} + +.custom-command-builder__schema-list { + display: grid; + grid-template-columns: minmax(5rem, auto) minmax(0, 1fr); + gap: 0.5rem 0.75rem; + margin: 0; +} + +.custom-command-builder__schema-list dt, +.custom-command-builder__schema-list dd { + min-width: 0; + overflow-wrap: anywhere; +} + +.custom-command-builder__schema-list dd { + margin: 0; +} + +.custom-command-builder__script-panel { + flex: 1; +} + +@media (max-width: 1200px) { + .custom-command-builder__summary-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .custom-command-builder__header { + flex-direction: column; + } + + .custom-command-builder__actions { + flex-wrap: wrap; + } + + .custom-command-builder__schema-list { + grid-template-columns: 1fr; + } +} diff --git a/UI/Features/CustomCommand/CustomCommandList.razor b/UI/Features/CustomCommand/CustomCommandList.razor new file mode 100644 index 00000000..355cf565 --- /dev/null +++ b/UI/Features/CustomCommand/CustomCommandList.razor @@ -0,0 +1,87 @@ +@page "/automation/custom-commands" + +Custom Commands + +
+
+
+

Custom Commands

+

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

+
+ +
+ + + + +
+
+ +
+ + + + + + + + + + + + @foreach(var command in _commands) + { + + + + + + + + } + +
NameDescriptionInputsOutput
@command.Name@command.Description@command.InputSummary@command.OutputSummary +
+ + + + +
+
+
+
+ +@code { + private readonly IReadOnlyList _commands = + [ + new( + Guid.Parse("86f8d450-72da-4a40-9df8-01d106314b32"), + "Measure Temperature", + "Reads a temperature value from a configured source.", + "sample_id, timeout_seconds", + "QUANTITY Temperature"), + new( + Guid.Parse("6ad6fb0e-2754-4caa-9b5e-405340d8783d"), + "Capture Image", + "Captures an image from a selected device channel.", + "sample_id, channel_name", + "BYTE_ARRAY"), + new( + Guid.Parse("318d86bd-41d1-48b0-a7bc-57cb835adce2"), + "Calculate Dilution", + "Calculates reagent and solvent volumes for a target concentration.", + "target_concentration, final_volume", + "STRUCT") + ]; + + private record CustomCommandSummary( + Guid Id, + string Name, + string Description, + string InputSummary, + string OutputSummary); +} diff --git a/UI/Features/CustomCommand/CustomCommandList.razor.css b/UI/Features/CustomCommand/CustomCommandList.razor.css new file mode 100644 index 00000000..f7b5d0d0 --- /dev/null +++ b/UI/Features/CustomCommand/CustomCommandList.razor.css @@ -0,0 +1,110 @@ +.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; + gap: 0.5rem; +} + +.custom-command-list__actions { + flex: 0 0 auto; +} + +.custom-command-list__row-actions { + justify-content: flex-end; +} + +.custom-command-list__icon-link, +.custom-command-list__icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.25rem; + border: 1px solid var(--rz-base-400, #dee2e6); + border-radius: 6px; + background: transparent; + color: inherit; + cursor: pointer; + text-decoration: none; +} + +.custom-command-list__icon-button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +.custom-command-list__icon-button--danger { + color: var(--rz-danger, #dc3545); +} + +.custom-command-list__icon-link:hover { + background: var(--rz-base-200, rgba(255, 255, 255, 0.08)); +} + +.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__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; + } +} From 7fa98d1321e8206f2deb2b7d9a0f5baa34744e74 Mon Sep 17 00:00:00 2001 From: "Babeckis, Arnas" Date: Thu, 25 Jun 2026 14:14:29 -0400 Subject: [PATCH 02/32] Added a schema designer and integrated it into the custom command builder --- UI/Components/AresValueSchemaDesigner.razor | 643 ++++++++++++++++++ .../AresValueSchemaDesigner.razor.css | 182 +++++ .../CustomCommand/CustomCommandBuilder.razor | 274 +++++--- .../CustomCommandBuilder.razor.css | 151 ++-- 4 files changed, 1125 insertions(+), 125 deletions(-) create mode 100644 UI/Components/AresValueSchemaDesigner.razor create mode 100644 UI/Components/AresValueSchemaDesigner.razor.css diff --git a/UI/Components/AresValueSchemaDesigner.razor b/UI/Components/AresValueSchemaDesigner.razor new file mode 100644 index 00000000..a0e53333 --- /dev/null +++ b/UI/Components/AresValueSchemaDesigner.razor @@ -0,0 +1,643 @@ +@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 + .OrderBy(entry => entry.Key, StringComparer.OrdinalIgnoreCase) + .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 existing = StructSchema.Fields[oldName]; + StructSchema.Fields.Remove(oldName); + StructSchema.Fields[newName] = existing; + 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..77609ff7 --- /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-100, 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/Features/CustomCommand/CustomCommandBuilder.razor b/UI/Features/CustomCommand/CustomCommandBuilder.razor index 3c685d2c..dbcb27b8 100644 --- a/UI/Features/CustomCommand/CustomCommandBuilder.razor +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor @@ -1,5 +1,6 @@ @page "/automation/custom-command-builder" @page "/automation/custom-command-builder/{CommandId:guid}" +@using Ares.Datamodel Custom Command Designer @@ -20,82 +21,89 @@ -
-
-
- Command Metadata - -
- - -
+
+ Command Metadata -
- - -
-
-
+ @@ -113,11 +116,14 @@
Script Body +
+ @GeneratedFunctionSignature: +
-
@@ -130,15 +136,15 @@ private Guid? _selectedParameterId; private ScriptEditor? _scriptEditor; + private CustomCommandMonacoProviderAdapter? _languageProviderAdapter; private string _commandName = "Measure Temperature"; private string _commandDescription = "Reads a temperature value from a configured source and returns it as a quantity."; private string _scriptBody = """ -def main(sample_id, timeout_seconds): - # Placeholder script editor. - return { - "value": 21.5, - "unit": "degC" - } +# Placeholder custom command body. +return { + "value": 21.5, + "unit": "degC" +} """; private readonly List _inputParameters = @@ -180,6 +186,23 @@ def main(sample_id, timeout_seconds): ? "Create a new custom command definition." : $"Editing placeholder custom command {CommandId}."; + private string GeneratedFunctionSignature + => CustomCommandScriptBuilder.BuildFunctionSignature(_commandName, BuildScriptParameters(), _outputSchema); + + private CustomCommandMonacoProviderAdapter LanguageProviderAdapter + { + get + { + _languageProviderAdapter ??= new CustomCommandMonacoProviderAdapter( + CompletionProvider, + DiagnosticsProvider, + SemanticTokensProvider, + HoverProvider, + BuildScriptContext); + return _languageProviderAdapter; + } + } + private CustomCommandParameterDraft? SelectedParameter => _inputParameters.FirstOrDefault(parameter => parameter.Id == SelectedParameterId); @@ -208,6 +231,7 @@ def main(sample_id, timeout_seconds): _inputParameters.Add(parameter); SelectedParameterId = parameter.Id; + _ = RefreshLanguageFeaturesAsync(); } private void RemoveParameter(CustomCommandParameterDraft parameter) @@ -216,24 +240,33 @@ def main(sample_id, timeout_seconds): 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) @@ -256,6 +289,29 @@ def main(sample_id, timeout_seconds): 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 RefreshLanguageFeaturesAsync() + { + if(_scriptEditor is not null) + { + await _scriptEditor.RefreshLanguageFeaturesAsync(); + } + } + private sealed class CustomCommandParameterDraft { public CustomCommandParameterDraft(string name, AresValueSchema schema) diff --git a/UI/Features/CustomCommand/CustomCommandBuilder.razor.css b/UI/Features/CustomCommand/CustomCommandBuilder.razor.css index a8f208c4..10171626 100644 --- a/UI/Features/CustomCommand/CustomCommandBuilder.razor.css +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor.css @@ -195,6 +195,17 @@ 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-100, 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; diff --git a/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs b/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs new file mode 100644 index 00000000..7a3b6e56 --- /dev/null +++ b/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs @@ -0,0 +1,109 @@ +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) + : 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); + return diagnostics.Select(MapDiagnostic).ToArray(); + } + + [JSInvokable] + public SemanticToken[] GetSemanticTokens(string script) + { + var wrappedScript = BuildWrappedScript(script); + return semanticTokensProvider.GetSemanticTokens(wrappedScript) + .Where(token => token.Line > WrappedBodyLineOffset) + .Select(token => token with + { + Line = ToBodyLine(token.Line), + StartColumn = ToBodyColumn(token.StartColumn) + }) + .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) + }; + } + + 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); 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..f6f9bb90 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,6 +28,14 @@ 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) => { @@ -40,9 +49,14 @@ 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 { From 38305d4ed0caaa4d43aecff6f9ff286790074e0e Mon Sep 17 00:00:00 2001 From: "Babeckis, Arnas" Date: Thu, 25 Jun 2026 18:39:34 -0400 Subject: [PATCH 08/32] Added another card for diagnostics feedback of the command. --- AresScript.Tests/Program.cs | 12 ++++ .../CustomCommand/CustomCommandBuilder.razor | 62 ++++++++++++++++++- .../CustomCommandMonacoProviderAdapter.cs | 51 ++++++++++++++- .../Interops/MonacoSemanticTokensProvider.cs | 2 +- 4 files changed, 122 insertions(+), 5 deletions(-) diff --git a/AresScript.Tests/Program.cs b/AresScript.Tests/Program.cs index 28f9cc3e..581e7afe 100644 --- a/AresScript.Tests/Program.cs +++ b/AresScript.Tests/Program.cs @@ -423,6 +423,18 @@ public void SemanticTokens_ClassifyAssignedIdentifiersAsVariables() && 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() { diff --git a/UI/Features/CustomCommand/CustomCommandBuilder.razor b/UI/Features/CustomCommand/CustomCommandBuilder.razor index 5a4c2cf3..76ff7833 100644 --- a/UI/Features/CustomCommand/CustomCommandBuilder.razor +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor @@ -128,6 +128,28 @@ @ref="_scriptEditor" />
+ +
+ Command Diagnostics + + @if(_commandDiagnostics.Count == 0) + { +
No command diagnostics.
+ } + else + { +
+ @foreach(var diagnostic in _commandDiagnostics) + { +
+ @GetDiagnosticSeverityText(diagnostic) + @diagnostic.Location + @diagnostic.Message +
+ } +
+ } +
@code { @@ -137,6 +159,7 @@ private Guid? _selectedParameterId; private ScriptEditor? _scriptEditor; private CustomCommandMonacoProviderAdapter? _languageProviderAdapter; + private IReadOnlyList _commandDiagnostics = []; private string _commandName = "Measure Temperature"; private string _commandDescription = "Reads a temperature value from a configured source and returns it as a quantity."; private string _scriptBody = """ @@ -198,11 +221,17 @@ return { DiagnosticsProvider, SemanticTokensProvider, HoverProvider, - BuildScriptContext); + 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); @@ -312,6 +341,37 @@ return { } } + 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) diff --git a/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs b/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs index 7a3b6e56..3104532b 100644 --- a/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs +++ b/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs @@ -12,7 +12,8 @@ internal sealed class CustomCommandMonacoProviderAdapter( IMonacoDiagnosticsProvider diagnosticsProvider, IMonacoSemanticTokensProvider semanticTokensProvider, IMonacoHoverProvider hoverProvider, - Func getContext) + Func getContext, + Action>? diagnosticsChanged = null) : IMonacoCompletionProvider, IMonacoDiagnosticsProvider, IMonacoSemanticTokensProvider, IMonacoHoverProvider { private const int WrappedBodyLineOffset = 1; @@ -33,14 +34,34 @@ 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) { - var wrappedScript = BuildWrappedScript(script); - return semanticTokensProvider.GetSemanticTokens(wrappedScript) + try + { + var wrappedScript = BuildWrappedScript(script); + return semanticTokensProvider.GetSemanticTokens(wrappedScript) + .Where(token => token.Line > WrappedBodyLineOffset) + .Select(token => token with + { + Line = ToBodyLine(token.Line), + StartColumn = ToBodyColumn(token.StartColumn) + }) + .ToArray(); + } + catch + { + return []; + } + } + + internal static SemanticToken[] MapSemanticTokens(IEnumerable tokens) + { + return tokens .Where(token => token.Line > WrappedBodyLineOffset) .Select(token => token with { @@ -92,6 +113,24 @@ internal static MonacoDiagnostic MapDiagnostic(MonacoDiagnostic diagnostic) }; } + 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(); @@ -107,3 +146,9 @@ 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; From 85d108731f4943b1c17f4c3119dc9a990dad6ffd Mon Sep 17 00:00:00 2001 From: "Babeckis, Arnas" Date: Fri, 26 Jun 2026 10:12:28 -0400 Subject: [PATCH 09/32] Css update for schema designer and command builder --- .../AresValueSchemaDesigner.razor.css | 2 +- .../CustomCommand/CustomCommandBuilder.razor | 30 +++++----- .../CustomCommandBuilder.razor.css | 57 ++++++++++++++++++- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/UI/Components/AresValueSchemaDesigner.razor.css b/UI/Components/AresValueSchemaDesigner.razor.css index 77609ff7..0214e5bb 100644 --- a/UI/Components/AresValueSchemaDesigner.razor.css +++ b/UI/Components/AresValueSchemaDesigner.razor.css @@ -11,7 +11,7 @@ .ares-schema-designer--nested { margin-top: 0.5rem; - background: var(--rz-base-100, rgba(255, 255, 255, 0.03)); + background: var(--rz-base-600, rgba(255, 255, 255, 0.03)); } .ares-schema-designer__header { diff --git a/UI/Features/CustomCommand/CustomCommandBuilder.razor b/UI/Features/CustomCommand/CustomCommandBuilder.razor index 76ff7833..60dc8885 100644 --- a/UI/Features/CustomCommand/CustomCommandBuilder.razor +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor @@ -114,21 +114,6 @@ -
- Script Body -
- @GeneratedFunctionSignature: -
-
- -
-
-
Command Diagnostics @@ -150,6 +135,21 @@ }
+ +
+ Script Body +
+ @GeneratedFunctionSignature: +
+
+ +
+
@code { diff --git a/UI/Features/CustomCommand/CustomCommandBuilder.razor.css b/UI/Features/CustomCommand/CustomCommandBuilder.razor.css index 10171626..9f76a9a5 100644 --- a/UI/Features/CustomCommand/CustomCommandBuilder.razor.css +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor.css @@ -103,7 +103,8 @@ .custom-command-builder__parameters-panel, .custom-command-builder__output-panel, -.custom-command-builder__script-panel { +.custom-command-builder__script-panel, +.custom-command-builder__diagnostics-panel { display: flex; flex-direction: column; gap: 0.75rem; @@ -200,7 +201,7 @@ padding: 0.65rem 0.75rem; border: 1px solid var(--rz-base-300, #ced4da); border-radius: 6px; - background: var(--rz-base-100, rgba(0, 0, 0, 0.03)); + background: var(--rz-base-600, rgba(0, 0, 0, 0.03)); font-family: Consolas, "Courier New", monospace; font-size: 0.9rem; overflow-wrap: anywhere; @@ -215,6 +216,54 @@ 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; @@ -243,4 +292,8 @@ grid-column: 1 / -1; grid-row: 2; } + + .custom-command-builder__diagnostic { + grid-template-columns: 1fr; + } } From baf9667e4f53d4867d3ad427f9497a007815ca8e Mon Sep 17 00:00:00 2001 From: "Babeckis, Arnas" Date: Fri, 26 Jun 2026 14:53:50 -0400 Subject: [PATCH 10/32] Prevented struct fields from moving around in a struct schema builder. --- Directory.Build.targets | 8 +++++++- UI/Components/AresValueSchemaDesigner.razor | 14 ++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) 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/Components/AresValueSchemaDesigner.razor b/UI/Components/AresValueSchemaDesigner.razor index a0e53333..83cca836 100644 --- a/UI/Components/AresValueSchemaDesigner.razor +++ b/UI/Components/AresValueSchemaDesigner.razor @@ -337,7 +337,6 @@ } private IReadOnlyList StructFields => StructSchema.Fields - .OrderBy(entry => entry.Key, StringComparer.OrdinalIgnoreCase) .Select(entry => new StructField(entry.Key, entry.Value)) .ToArray(); @@ -520,9 +519,16 @@ if(string.IsNullOrWhiteSpace(newName) || newName == oldName || StructSchema.Fields.ContainsKey(newName)) return; - var existing = StructSchema.Fields[oldName]; - StructSchema.Fields.Remove(oldName); - StructSchema.Fields[newName] = existing; + 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(); } From 36260248f3ea9d988863d0e5125f0e455fe1ba5c Mon Sep 17 00:00:00 2001 From: "Babeckis, Arnas" Date: Fri, 26 Jun 2026 16:46:15 -0400 Subject: [PATCH 11/32] Added persistence and the migrations for the custom commands --- .../CustomCommandPersistenceService.cs | 132 + .../CustomCommands/CustomCommandSummary.cs | 8 + .../ICustomCommandPersistenceService.cs | 14 + Ares.Core/ServiceCollectionExtensions.cs | 4 +- ...2_CustomCommands_AresDbContext.Designer.cs | 2152 ++++++++++++++++ ...0626195922_CustomCommands_AresDbContext.cs | 69 + ...omCommands_AresIdentityContext.Designer.cs | 277 +++ ...5925_CustomCommands_AresIdentityContext.cs | 22 + .../Migrations/AresDbContextModelSnapshot.cs | 79 + ...9_CustomCommands_AresDbContext.Designer.cs | 2164 +++++++++++++++++ ...0626195929_CustomCommands_AresDbContext.cs | 69 + ...omCommands_AresIdentityContext.Designer.cs | 279 +++ ...5932_CustomCommands_AresIdentityContext.cs | 22 + .../Migrations/AresDbContextModelSnapshot.cs | 79 + ...6_CustomCommands_AresDbContext.Designer.cs | 2147 ++++++++++++++++ ...0626195916_CustomCommands_AresDbContext.cs | 69 + ...omCommands_AresIdentityContext.Designer.cs | 268 ++ ...5919_CustomCommands_AresIdentityContext.cs | 22 + .../Migrations/AresDbContextModelSnapshot.cs | 79 + UI/Components/AresValueSchemaDesigner.razor | 10 +- .../CustomCommand/CustomCommandBuilder.razor | 206 +- .../CustomCommandBuilder.razor.css | 12 + .../CustomCommand/CustomCommandList.razor | 172 +- .../CustomCommand/CustomCommandList.razor.css | 12 + UI/UI.csproj | 3 +- 25 files changed, 8259 insertions(+), 111 deletions(-) create mode 100644 Ares.Core/CustomCommands/CustomCommandPersistenceService.cs create mode 100644 Ares.Core/CustomCommands/CustomCommandSummary.cs create mode 100644 Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs create mode 100644 AresService.Migrations.Postgres/Migrations/20260626195922_CustomCommands_AresDbContext.Designer.cs create mode 100644 AresService.Migrations.Postgres/Migrations/20260626195922_CustomCommands_AresDbContext.cs create mode 100644 AresService.Migrations.Postgres/Migrations/20260626195925_CustomCommands_AresIdentityContext.Designer.cs create mode 100644 AresService.Migrations.Postgres/Migrations/20260626195925_CustomCommands_AresIdentityContext.cs create mode 100644 AresService.Migrations.SqlServer/Migrations/20260626195929_CustomCommands_AresDbContext.Designer.cs create mode 100644 AresService.Migrations.SqlServer/Migrations/20260626195929_CustomCommands_AresDbContext.cs create mode 100644 AresService.Migrations.SqlServer/Migrations/20260626195932_CustomCommands_AresIdentityContext.Designer.cs create mode 100644 AresService.Migrations.SqlServer/Migrations/20260626195932_CustomCommands_AresIdentityContext.cs create mode 100644 AresService.Migrations.Sqlite/Migrations/20260626195916_CustomCommands_AresDbContext.Designer.cs create mode 100644 AresService.Migrations.Sqlite/Migrations/20260626195916_CustomCommands_AresDbContext.cs create mode 100644 AresService.Migrations.Sqlite/Migrations/20260626195919_CustomCommands_AresIdentityContext.Designer.cs create mode 100644 AresService.Migrations.Sqlite/Migrations/20260626195919_CustomCommands_AresIdentityContext.cs diff --git a/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs b/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs new file mode 100644 index 00000000..6cd887e9 --- /dev/null +++ b/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs @@ -0,0 +1,132 @@ +using Ares.Datamodel.Extensions; +using Microsoft.EntityFrameworkCore; +using CustomCommandModel = Ares.Datamodel.Automation.CustomCommand; + +namespace Ares.Core.CustomCommands; + +internal sealed class CustomCommandPersistenceService(IDbContextFactory dbContextFactory) + : ICustomCommandPersistenceService +{ + private const string UniqueIdPropertyName = "UniqueId"; + + public async Task> GetSummariesAsync() + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + var rows = await context.CustomCommands + .AsNoTracking() + .Include(command => command.InputParameters) + .OrderBy(command => command.Name) + .Select(command => new + { + Id = EF.Property(command, UniqueIdPropertyName), + Command = command + }) + .ToListAsync(); + + return rows + .Select(row => new CustomCommandSummary( + ParseEntityId(row.Id), + string.IsNullOrWhiteSpace(row.Command.Name) ? "(Unnamed command)" : row.Command.Name, + row.Command.Description, + BuildInputSummary(row.Command), + BuildOutputSummary(row.Command))) + .ToArray(); + } + + public async Task GetAsync(Guid id) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + return await context.CustomCommands + .AsNoTracking() + .Include(command => command.InputParameters) + .FirstOrDefaultAsync(command => EF.Property(command, UniqueIdPropertyName) == id.ToString()); + } + + public async Task SaveAsync(Guid? id, CustomCommandModel command) + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + var commandId = id ?? Guid.NewGuid(); + var existingCommand = id is null + ? null + : await context.CustomCommands + .Include(command => command.InputParameters) + .FirstOrDefaultAsync(command => EF.Property(command, UniqueIdPropertyName) == id.Value.ToString()); + + if(existingCommand is null) + { + var commandToAdd = command.Clone(); + context.CustomCommands.Add(commandToAdd); + AssignEntityId(context, commandToAdd, commandId); + AssignParameterIds(context, commandToAdd); + } + else + { + existingCommand.Name = command.Name; + existingCommand.Description = command.Description; + existingCommand.OutputSchema = command.OutputSchema?.Clone(); + existingCommand.ScriptBody = command.ScriptBody; + + context.RemoveRange(existingCommand.InputParameters); + existingCommand.InputParameters.Clear(); + existingCommand.InputParameters.AddRange(command.InputParameters.Select(parameter => parameter.Clone())); + context.ChangeTracker.DetectChanges(); + AssignParameterIds(context, existingCommand); + } + + 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(); + } + + private static Guid ParseEntityId(string? id) + { + return Guid.TryParse(id, out var guid) ? guid : Guid.Empty; + } + + private static void AssignEntityId(CoreDatabaseContext context, CustomCommandModel command, Guid id) + { + context.Entry(command).Property(UniqueIdPropertyName).CurrentValue = id.ToString(); + } + + private static void AssignParameterIds(CoreDatabaseContext context, CustomCommandModel command) + { + foreach(var parameter in command.InputParameters) + { + var entry = context.Entry(parameter); + if(entry.State == EntityState.Detached) + { + entry.State = EntityState.Added; + } + + entry.Property(UniqueIdPropertyName).CurrentValue = Guid.NewGuid().ToString(); + } + } + + private static string BuildInputSummary(CustomCommandModel command) + { + return command.InputParameters.Count == 0 + ? "None" + : string.Join(", ", command.InputParameters.Select(parameter => parameter.Name)); + } + + private static string BuildOutputSummary(CustomCommandModel 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..0c4e5521 --- /dev/null +++ b/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs @@ -0,0 +1,14 @@ +using CustomCommandModel = Ares.Datamodel.Automation.CustomCommand; + +namespace Ares.Core.CustomCommands; + +public interface ICustomCommandPersistenceService +{ + Task> GetSummariesAsync(); + + Task GetAsync(Guid id); + + Task SaveAsync(Guid? id, CustomCommandModel command); + + Task DeleteAsync(Guid id); +} diff --git a/Ares.Core/ServiceCollectionExtensions.cs b/Ares.Core/ServiceCollectionExtensions.cs index cd590869..9528e2db 100644 --- a/Ares.Core/ServiceCollectionExtensions.cs +++ b/Ares.Core/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Ares.Core.Analyzing; using Ares.Core.AresEnvironment; +using Ares.Core.CustomCommands; using Ares.Core.DataManagement.DataMappers; using Ares.Core.Device.Managers; using Ares.Core.Device.Plugins.Drivers; @@ -68,6 +69,7 @@ public static void AddAresCoreComponents(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddScoped(); services.AddSingleton(); services.AddSingleton(); @@ -133,4 +135,4 @@ private static void BindRepositories(this IServiceCollection services) services.AddSingleton(); } -} \ No newline at end of file +} 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/AresDbContextModelSnapshot.cs b/AresService.Migrations.Postgres/Migrations/AresDbContextModelSnapshot.cs index 0d19c09f..e87b3bbf 100644 --- a/AresService.Migrations.Postgres/Migrations/AresDbContextModelSnapshot.cs +++ b/AresService.Migrations.Postgres/Migrations/AresDbContextModelSnapshot.cs @@ -310,6 +310,71 @@ 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("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") @@ -1716,6 +1781,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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") @@ -1961,6 +2035,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Capabilities"); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Navigation("InputParameters"); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.Navigation("ExecutionInfo"); 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/AresDbContextModelSnapshot.cs b/AresService.Migrations.SqlServer/Migrations/AresDbContextModelSnapshot.cs index 10fbf9df..e5e96969 100644 --- a/AresService.Migrations.SqlServer/Migrations/AresDbContextModelSnapshot.cs +++ b/AresService.Migrations.SqlServer/Migrations/AresDbContextModelSnapshot.cs @@ -311,6 +311,71 @@ 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("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") @@ -1728,6 +1793,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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") @@ -1973,6 +2047,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Capabilities"); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Navigation("InputParameters"); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.Navigation("ExecutionInfo"); 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/AresDbContextModelSnapshot.cs b/AresService.Migrations.Sqlite/Migrations/AresDbContextModelSnapshot.cs index 9c9b63cc..1a1c86b0 100644 --- a/AresService.Migrations.Sqlite/Migrations/AresDbContextModelSnapshot.cs +++ b/AresService.Migrations.Sqlite/Migrations/AresDbContextModelSnapshot.cs @@ -305,6 +305,71 @@ 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("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") @@ -1711,6 +1776,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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") @@ -1956,6 +2030,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Capabilities"); }); + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + { + b.Navigation("InputParameters"); + }); + modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => { b.Navigation("ExecutionInfo"); diff --git a/UI/Components/AresValueSchemaDesigner.razor b/UI/Components/AresValueSchemaDesigner.razor index 83cca836..18162f07 100644 --- a/UI/Components/AresValueSchemaDesigner.razor +++ b/UI/Components/AresValueSchemaDesigner.razor @@ -28,6 +28,11 @@ } else { + +
- - @if(IsNumericType(_schema.Type)) {
diff --git a/UI/Features/CustomCommand/CustomCommandBuilder.razor b/UI/Features/CustomCommand/CustomCommandBuilder.razor index 60dc8885..4983908a 100644 --- a/UI/Features/CustomCommand/CustomCommandBuilder.razor +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor @@ -1,13 +1,21 @@ @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.CustomCommand +@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 @@ -23,11 +31,19 @@
- - +
+ @if(_isLoading) + { +
Loading custom command...
+ } + else if(!string.IsNullOrWhiteSpace(_loadError)) + { +
@_loadError
+ } +
Command Metadata @@ -157,57 +173,31 @@ public Guid? CommandId { get; set; } private Guid? _selectedParameterId; + private Guid? _loadedCommandId; private ScriptEditor? _scriptEditor; private CustomCommandMonacoProviderAdapter? _languageProviderAdapter; private IReadOnlyList _commandDiagnostics = []; - private string _commandName = "Measure Temperature"; - private string _commandDescription = "Reads a temperature value from a configured source and returns it as a quantity."; + private bool _isLoading; + private bool _isSaving; + private string? _loadError; + private string _commandName = "New Custom Command"; + private string _commandDescription = string.Empty; private string _scriptBody = """ -# Placeholder custom command body. -return { - "value": 21.5, - "unit": "degC" -} +return """; - private readonly List _inputParameters = - [ - new("sample_id", new AresValueSchema - { - Type = AresDataType.String, - Description = "Identifier for the sample to measure." - }), - new("timeout_seconds", new AresValueSchema - { - Type = AresDataType.Number, - Optional = true, - Description = "Maximum time to wait before failing.", - MinNumberValue = 0 - }), - new("include_metadata", new AresValueSchema - { - Type = AresDataType.Boolean, - Optional = true, - Description = "Include device metadata in the command context." - }) - ]; + private readonly List _inputParameters = []; private AresValueSchema _outputSchema = new() { - Type = AresDataType.Quantity, - Description = "Measured sample temperature.", - QuantitySchema = new QuantitySchema - { - QuantityType = QuantityType.Temperature, - BoundsUnit = "degC", - MinScalarValue = 0, - MaxScalarValue = 100 - } + Type = AresDataType.Unit }; private string Subtitle => CommandId is null ? "Create a new custom command definition." - : $"Editing placeholder custom command {CommandId}."; + : $"Editing custom command {CommandId}."; + + private string SaveButtonText => _isSaving ? "Saving" : "Save"; private string GeneratedFunctionSignature => CustomCommandScriptBuilder.BuildFunctionSignature(_commandName, BuildScriptParameters(), _outputSchema); @@ -250,6 +240,24 @@ return { 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 @@ -333,6 +341,124 @@ return { 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) diff --git a/UI/Features/CustomCommand/CustomCommandBuilder.razor.css b/UI/Features/CustomCommand/CustomCommandBuilder.razor.css index 9f76a9a5..8f3146f0 100644 --- a/UI/Features/CustomCommand/CustomCommandBuilder.razor.css +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor.css @@ -80,6 +80,18 @@ 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; diff --git a/UI/Features/CustomCommand/CustomCommandList.razor b/UI/Features/CustomCommand/CustomCommandList.razor index fa37eb13..e6e4d7ba 100644 --- a/UI/Features/CustomCommand/CustomCommandList.razor +++ b/UI/Features/CustomCommand/CustomCommandList.razor @@ -1,4 +1,8 @@ @page "/automation/custom-commands" +@using Ares.Core.CustomCommands +@using UI.Application.Notifications +@inject ICustomCommandPersistenceService CustomCommandService +@inject IUiNotificationService NotificationService Custom Commands @@ -13,79 +17,119 @@ - -
- - - - - - - - - - - - @foreach(var command in _commands) - { + @if(_isLoading) + { +
Loading custom commands...
+ } + else if(!string.IsNullOrWhiteSpace(_loadError)) + { +
@_loadError
+ } + else if(_commands.Count == 0) + { +
No custom commands have been saved yet.
+ } + else + { +
+
NameDescriptionInputsOutput
+ - - - - - + + + + + - } - -
- - @command.Name - - @command.Description@command.InputSummary@command.OutputSummary -
- - - - -
-
NameDescriptionInputsOutput
-
+ + + @foreach(var command in _commands) + { + + + + @command.Name + + + @command.Description + @command.InputSummary + @command.OutputSummary + +
+ + + + +
+ + + } + + + + } @code { - private readonly IReadOnlyList _commands = - [ - new( - Guid.Parse("86f8d450-72da-4a40-9df8-01d106314b32"), - "Measure Temperature", - "Reads a temperature value from a configured source.", - "sample_id, timeout_seconds", - "QUANTITY Temperature"), - new( - Guid.Parse("6ad6fb0e-2754-4caa-9b5e-405340d8783d"), - "Capture Image", - "Captures an image from a selected device channel.", - "sample_id, channel_name", - "BYTE_ARRAY"), - new( - Guid.Parse("318d86bd-41d1-48b0-a7bc-57cb835adce2"), - "Calculate Dilution", - "Calculates reagent and solvent volumes for a target concentration.", - "target_concentration, final_volume", - "STRUCT") - ]; + 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; - private record CustomCommandSummary( - Guid Id, - string Name, - string Description, - string InputSummary, - string OutputSummary); + 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 index ef950123..35e53ab3 100644 --- a/UI/Features/CustomCommand/CustomCommandList.razor.css +++ b/UI/Features/CustomCommand/CustomCommandList.razor.css @@ -84,6 +84,18 @@ 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; diff --git a/UI/UI.csproj b/UI/UI.csproj index 14eb33cf..3d96772b 100644 --- a/UI/UI.csproj +++ b/UI/UI.csproj @@ -64,7 +64,8 @@ - + + From a2fadd4ab2accf505da49f3ce28cd7e466286c34 Mon Sep 17 00:00:00 2001 From: "Babeckis, Arnas" Date: Fri, 26 Jun 2026 18:18:21 -0400 Subject: [PATCH 12/32] Fixed some indexing issues --- AresScript.Tests/Program.cs | 8 ++++---- .../AresScriptAnalysis.SemanticTokens.cs | 2 +- .../CustomCommandMonacoProviderAdapter.cs | 15 ++++---------- .../Monaco/Scripts/ares-semantic-setup.ts | 20 ++++++++++++------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/AresScript.Tests/Program.cs b/AresScript.Tests/Program.cs index 581e7afe..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,17 +409,17 @@ 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)); } 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/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs b/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs index 3104532b..37af9bd3 100644 --- a/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs +++ b/UI/Features/CustomCommand/CustomCommandMonacoProviderAdapter.cs @@ -44,14 +44,7 @@ public SemanticToken[] GetSemanticTokens(string script) try { var wrappedScript = BuildWrappedScript(script); - return semanticTokensProvider.GetSemanticTokens(wrappedScript) - .Where(token => token.Line > WrappedBodyLineOffset) - .Select(token => token with - { - Line = ToBodyLine(token.Line), - StartColumn = ToBodyColumn(token.StartColumn) - }) - .ToArray(); + return MapSemanticTokens(semanticTokensProvider.GetSemanticTokens(wrappedScript)); } catch { @@ -62,11 +55,11 @@ public SemanticToken[] GetSemanticTokens(string script) internal static SemanticToken[] MapSemanticTokens(IEnumerable tokens) { return tokens - .Where(token => token.Line > WrappedBodyLineOffset) + .Where(token => token.Line >= WrappedBodyLineOffset) .Select(token => token with { - Line = ToBodyLine(token.Line), - StartColumn = ToBodyColumn(token.StartColumn) + Line = token.Line - WrappedBodyLineOffset, + StartColumn = Math.Max(0, token.StartColumn - WrappedBodyColumnOffset) }) .ToArray(); } diff --git a/UI/Infrastructure/Monaco/Scripts/ares-semantic-setup.ts b/UI/Infrastructure/Monaco/Scripts/ares-semantic-setup.ts index f6f9bb90..2cd386cf 100644 --- a/UI/Infrastructure/Monaco/Scripts/ares-semantic-setup.ts +++ b/UI/Infrastructure/Monaco/Scripts/ares-semantic-setup.ts @@ -39,7 +39,7 @@ export function setupSemanticTokens(provider: DotNet.DotNetObject) { 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 }; }); }, @@ -59,9 +59,9 @@ export function disposeSemanticTokens() { 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[] = []; @@ -69,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; From c55f93b89de07d871181f4212c4efc1b4c5e448a Mon Sep 17 00:00:00 2001 From: "Babeckis, Arnas" Date: Thu, 2 Jul 2026 19:01:53 -0400 Subject: [PATCH 13/32] Custom command execution device skeleton --- .../CustomCommandDevice/AresCommandDevice.cs | 55 +++++++++++++++++++ .../CustomCommandPersistenceService.cs | 9 +++ .../ICustomCommandPersistenceService.cs | 8 ++- 3 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 Ares.Core/CustomCommandDevice/AresCommandDevice.cs diff --git a/Ares.Core/CustomCommandDevice/AresCommandDevice.cs b/Ares.Core/CustomCommandDevice/AresCommandDevice.cs new file mode 100644 index 00000000..d62cce44 --- /dev/null +++ b/Ares.Core/CustomCommandDevice/AresCommandDevice.cs @@ -0,0 +1,55 @@ +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() + { + 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 index 6cd887e9..061d16a8 100644 --- a/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs +++ b/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs @@ -129,4 +129,13 @@ private static string BuildOutputSummary(CustomCommandModel command) ? "Unspecified" : command.OutputSchema.Stringify(); } + + public async Task> GetCommandsAsync() + { + await using var context = await dbContextFactory.CreateDbContextAsync(); + return await context.CustomCommands + .AsNoTracking() + .Include(command => command.InputParameters) + .ToListAsync(); + } } diff --git a/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs b/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs index 0c4e5521..8110a755 100644 --- a/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs +++ b/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs @@ -1,4 +1,4 @@ -using CustomCommandModel = Ares.Datamodel.Automation.CustomCommand; +using Ares.Datamodel.Automation; namespace Ares.Core.CustomCommands; @@ -6,9 +6,11 @@ public interface ICustomCommandPersistenceService { Task> GetSummariesAsync(); - Task GetAsync(Guid id); + Task> GetCommandsAsync(); - Task SaveAsync(Guid? id, CustomCommandModel command); + Task GetAsync(Guid id); + + Task SaveAsync(Guid? id, CustomCommand command); Task DeleteAsync(Guid id); } From 4cea14916940fc5184abbca36ce2759cfa695039 Mon Sep 17 00:00:00 2001 From: "Babeckis, Arnas" Date: Thu, 9 Jul 2026 18:23:54 -0400 Subject: [PATCH 14/32] Added support for multiple command types. The types included so far are system command, device command, and custom command. --- Ares.Core.Grpc/Services/DevicesService.cs | 4 +- Ares.Core/CoreDatabaseContext.cs | 1 + .../CustomCommandDevice/AresCommandDevice.cs | 5 +- .../CustomCommandPersistenceService.cs | 162 +++++++++++------- .../ICustomCommandPersistenceService.cs | 6 +- .../DataMappers/CampaignDatasetGenerator.cs | 4 +- .../CustomCommandEntityConfiguration.cs | 9 - ...CustomCommandVersionEntityConfiguration.cs | 33 ++++ .../CommandMetadataEntityConfiguration.cs | 8 - .../CommandTemplateEntityConfiguration.cs | 14 +- .../DeviceCommandEntityConfiguration.cs | 22 +++ Ares.Core/Execution/ExecutionManager.cs | 2 +- .../Execution/Executors/CommandExecutor.cs | 12 +- .../Executors/Composers/StepComposer.cs | 12 +- .../Executors/ExecutorSummaryHelpers.cs | 5 +- .../ExperimentTemplateExtensions.cs | 11 +- .../RequiredDeviceInterpretersValidator.cs | 4 +- .../Validators/GoodAnalyzerValidator.cs | 3 +- UI/Components/Experiments/CommandViewer.razor | 10 +- .../ExperimentTemplateExtensions.cs | 2 +- .../ViewModels/CommandDesignerViewModel.cs | 35 ++-- .../CommandOutputVariableReference.cs | 4 +- .../CustomCommand/CustomCommandBuilder.razor | 2 +- 23 files changed, 231 insertions(+), 139 deletions(-) create mode 100644 Ares.Core/EntityConfigurations/Automation/CustomCommandVersionEntityConfiguration.cs create mode 100644 Ares.Core/EntityConfigurations/Execution/Commands/DeviceCommandEntityConfiguration.cs 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/CoreDatabaseContext.cs b/Ares.Core/CoreDatabaseContext.cs index 5cda9ba3..5cf123e0 100644 --- a/Ares.Core/CoreDatabaseContext.cs +++ b/Ares.Core/CoreDatabaseContext.cs @@ -46,6 +46,7 @@ public CoreDatabaseContext(DbContextOptions options) : base(options) 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/CustomCommandDevice/AresCommandDevice.cs b/Ares.Core/CustomCommandDevice/AresCommandDevice.cs index d62cce44..a73e1637 100644 --- a/Ares.Core/CustomCommandDevice/AresCommandDevice.cs +++ b/Ares.Core/CustomCommandDevice/AresCommandDevice.cs @@ -49,7 +49,8 @@ public override Task UpdateSettings(AresStruct settings) protected override async Task> BuildCommandDescriptorsAsync() { - var commands = await _commandPersistenceService.GetSummariesAsync(); - var descriptors = commands.Select(cmd => new DeviceCommandDescriptor { Name = cmd.Name, Description = cmd.Description, OutputSchema = cmd.OutputSummary }) + 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 index 061d16a8..068ae621 100644 --- a/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs +++ b/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs @@ -1,6 +1,10 @@ +using Ares.Datamodel.Automation; using Ares.Datamodel.Extensions; +using Google.Protobuf; using Microsoft.EntityFrameworkCore; +using System.Security.Cryptography; using CustomCommandModel = Ares.Datamodel.Automation.CustomCommand; +using CustomCommandVersionModel = Ares.Datamodel.Automation.CustomCommandVersion; namespace Ares.Core.CustomCommands; @@ -11,68 +15,64 @@ internal sealed class CustomCommandPersistenceService(IDbContextFactory> GetSummariesAsync() { - await using var context = await dbContextFactory.CreateDbContextAsync(); - var rows = await context.CustomCommands - .AsNoTracking() - .Include(command => command.InputParameters) + 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) - .Select(command => new - { - Id = EF.Property(command, UniqueIdPropertyName), - Command = command - }) - .ToListAsync(); - - return rows - .Select(row => new CustomCommandSummary( - ParseEntityId(row.Id), - string.IsNullOrWhiteSpace(row.Command.Name) ? "(Unnamed command)" : row.Command.Name, - row.Command.Description, - BuildInputSummary(row.Command), - BuildOutputSummary(row.Command))) .ToArray(); } - public async Task GetAsync(Guid id) + public async Task GetAsync(Guid id) { await using var context = await dbContextFactory.CreateDbContextAsync(); - return await context.CustomCommands + var currentVersionId = await context.CustomCommands .AsNoTracking() - .Include(command => command.InputParameters) - .FirstOrDefaultAsync(command => EF.Property(command, UniqueIdPropertyName) == id.ToString()); + .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, CustomCommandModel command) + 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 - .Include(command => command.InputParameters) .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) { - var commandToAdd = command.Clone(); - context.CustomCommands.Add(commandToAdd); - AssignEntityId(context, commandToAdd, commandId); - AssignParameterIds(context, commandToAdd); - } - else - { - existingCommand.Name = command.Name; - existingCommand.Description = command.Description; - existingCommand.OutputSchema = command.OutputSchema?.Clone(); - existingCommand.ScriptBody = command.ScriptBody; - - context.RemoveRange(existingCommand.InputParameters); - existingCommand.InputParameters.Clear(); - existingCommand.InputParameters.AddRange(command.InputParameters.Select(parameter => parameter.Clone())); - context.ChangeTracker.DetectChanges(); - AssignParameterIds(context, existingCommand); + existingCommand = new CustomCommandModel(); + context.CustomCommands.Add(existingCommand); + AssignEntityId(context, existingCommand, commandId); } + existingCommand.CurrentVersionId = versionId.ToString(); await context.SaveChangesAsync(); return commandId; } @@ -84,58 +84,98 @@ public async Task DeleteAsync(Guid id) .FirstOrDefaultAsync(command => EF.Property(command, UniqueIdPropertyName) == id.ToString()); if(command is null) - { return; - } context.CustomCommands.Remove(command); await context.SaveChangesAsync(); } - private static Guid ParseEntityId(string? id) + public Task> GetCommandsAsync() => GetCurrentVersionsAsync(); + + private async Task> GetCurrentVersionsAsync() { - return Guid.TryParse(id, out var guid) ? guid : Guid.Empty; + 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) { - context.Entry(command).Property(UniqueIdPropertyName).CurrentValue = id.ToString(); + command.UniqueId = id.ToString(); + context.Entry(command).Property(UniqueIdPropertyName).CurrentValue = command.UniqueId; } - private static void AssignParameterIds(CoreDatabaseContext context, CustomCommandModel command) + private static Guid AssignEntityId(CoreDatabaseContext context, CustomCommandVersionModel version) { - foreach(var parameter in command.InputParameters) + 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 string BuildInputSummary(CustomCommandModel command) + 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; + version.ContentHash = ComputeContentHash(version); + return version; + } + + private static string ComputeContentHash(CustomCommandVersionModel version) + { + var hashInput = version.Clone(); + hashInput.UniqueId = string.Empty; + hashInput.ContentHash = string.Empty; + return Convert.ToHexString(SHA256.HashData(hashInput.ToByteArray())); + } + + private static string BuildInputSummary(CustomCommandVersionModel command) { return command.InputParameters.Count == 0 ? "None" : string.Join(", ", command.InputParameters.Select(parameter => parameter.Name)); } - private static string BuildOutputSummary(CustomCommandModel command) + private static string BuildOutputSummary(CustomCommandVersionModel command) { return command.OutputSchema is null ? "Unspecified" : command.OutputSchema.Stringify(); } - - public async Task> GetCommandsAsync() - { - await using var context = await dbContextFactory.CreateDbContextAsync(); - return await context.CustomCommands - .AsNoTracking() - .Include(command => command.InputParameters) - .ToListAsync(); - } } diff --git a/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs b/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs index 8110a755..9188b5f8 100644 --- a/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs +++ b/Ares.Core/CustomCommands/ICustomCommandPersistenceService.cs @@ -6,11 +6,11 @@ public interface ICustomCommandPersistenceService { Task> GetSummariesAsync(); - Task> GetCommandsAsync(); + Task> GetCommandsAsync(); - Task GetAsync(Guid id); + Task GetAsync(Guid id); - Task SaveAsync(Guid? id, CustomCommand command); + 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 c578867a..f8bf829b 100644 --- a/Ares.Core/DataManagement/DataMappers/CampaignDatasetGenerator.cs +++ b/Ares.Core/DataManagement/DataMappers/CampaignDatasetGenerator.cs @@ -266,7 +266,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(); @@ -464,7 +464,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/EntityConfigurations/Automation/CustomCommandEntityConfiguration.cs b/Ares.Core/EntityConfigurations/Automation/CustomCommandEntityConfiguration.cs index 1569d712..93ad6094 100644 --- a/Ares.Core/EntityConfigurations/Automation/CustomCommandEntityConfiguration.cs +++ b/Ares.Core/EntityConfigurations/Automation/CustomCommandEntityConfiguration.cs @@ -10,14 +10,5 @@ public override void Configure(EntityTypeBuilder builder) { base.Configure(builder); builder.ToTable("CustomCommands"); - - builder.HasMany(command => command.InputParameters) - .WithOne() - .HasForeignKey("CustomCommandId") - .IsRequired() - .OnDelete(DeleteBehavior.Cascade); - - builder.Navigation(command => command.InputParameters) - .AutoInclude(); } } 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..7ad161ad 100644 --- a/Ares.Core/EntityConfigurations/Execution/Commands/CommandTemplateEntityConfiguration.cs +++ b/Ares.Core/EntityConfigurations/Execution/Commands/CommandTemplateEntityConfiguration.cs @@ -10,18 +10,18 @@ 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"); - - builder.Navigation(commandTemplate => commandTemplate.Parameters) - .AutoInclude(); + .HasForeignKey("CommandTemplateId") + .OnDelete(DeleteBehavior.Cascade); + builder.OwnsOne(template => template.SystemCommand); + builder.OwnsOne(template => template.CustomCommandInvocation); - 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 e61a2282..1c6ce9e3 100644 --- a/Ares.Core/Execution/ExecutionManager.cs +++ b/Ares.Core/Execution/ExecutionManager.cs @@ -132,7 +132,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/CommandExecutor.cs b/Ares.Core/Execution/Executors/CommandExecutor.cs index 63a152bd..33eb3fea 100644 --- a/Ares.Core/Execution/Executors/CommandExecutor.cs +++ b/Ares.Core/Execution/Executors/CommandExecutor.cs @@ -14,8 +14,10 @@ public class CommandExecutor : IExecutor> _command; private readonly BehaviorSubject _stateSubject; private readonly INotifier _notifier; - private readonly ISystemSettingsManager _settingsManager; + private readonly ISystemSettingsManager _settingsManager; + + // TODO: Execute the system and custom commands in addition to device AB 7/9/2026 public CommandExecutor(Func> command, CommandTemplate template, INotifier notifier, ISystemSettingsManager settingsManager) { _command = command; @@ -24,8 +26,8 @@ public CommandExecutor(Func> command, Com var executionStatus = new CommandExecutionStatus { CommandId = template.UniqueId, - CommandName = template.Metadata.Name, - DeviceName = template.Metadata.DeviceType, + CommandName = template.DeviceCommand.Metadata.Name, + DeviceName = template.DeviceCommand.Metadata.DeviceType, State = ExecutionState.Undefined }; @@ -56,7 +58,7 @@ public async Task Execute(ExecutionControlToken token, await token.WaitForResumeAsync(); } catch(OperationCanceledException) - { + { } if(token.IsCancelled) @@ -69,7 +71,7 @@ public async Task Execute(ExecutionControlToken token, 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) diff --git a/Ares.Core/Execution/Executors/Composers/StepComposer.cs b/Ares.Core/Execution/Executors/Composers/StepComposer.cs index e8ad5808..d07011c9 100644 --- a/Ares.Core/Execution/Executors/Composers/StepComposer.cs +++ b/Ares.Core/Execution/Executors/Composers/StepComposer.cs @@ -21,27 +21,29 @@ public StepComposer(IAresDeviceRepo deviceRepo, INotifier notifier, ISystemSetti } public StepExecutor Compose(StepTemplate template) - { + { var executables = template .CommandTemplates .OrderBy(t => t.Index) .Select ( + // TODO make sure we handle the rest of command template types AB 7/9/2026 commandTemplate => { - var deviceId = commandTemplate.Metadata?.DeviceId; + 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); diff --git a/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs b/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs index f2a1f198..a61a6995 100644 --- a/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs +++ b/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs @@ -48,6 +48,7 @@ public static CommandExecutionSummary CreateCommandExecutionSummary(CommandTempl DateTime startTime, DateTime endTime) { + // TODO: Handle the other potential command template command types AB 7/9/2026 var commandExecutionSummary = new CommandExecutionSummary { UniqueId = Guid.NewGuid().ToString(), @@ -55,8 +56,8 @@ public static CommandExecutionSummary CreateCommandExecutionSummary(CommandTempl CommandId = Guid.NewGuid().ToString(), Result = deviceResult, TemplateId = template.UniqueId, - CommandDescription = template.Metadata.Description, - CommandName = template.Metadata.Name, + CommandDescription = template.DeviceCommand.Metadata.Description, + CommandName = template.DeviceCommand.Metadata.Name, StatusCode = deviceResult?.StatusCode ?? CommandStatusCode.StatusUnspecified }; diff --git a/Ares.Core/Execution/Extensions/ExperimentTemplateExtensions.cs b/Ares.Core/Execution/Extensions/ExperimentTemplateExtensions.cs index 2481d2ff..9a66882b 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 /> @@ -75,18 +75,19 @@ public static ExperimentTemplate CloneWithNewIds(this ExperimentTemplate templat foreach(var stepTemplate in newTemplate.StepTemplates) { stepTemplate.UniqueId = Guid.NewGuid().ToString(); + // TODO ensure all the commands not just device commands are taken care of AB 7/9/2026 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.DeviceCommand.Metadata.UniqueId = Guid.NewGuid().ToString(); commandTemplate.UniqueId = cmdTemplateId; - foreach(var metadataParameterMetadata in commandTemplate.Metadata.ParameterMetadatas) + 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 +109,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/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..21adf74d 100644 --- a/Ares.Core/Validation/Validators/GoodAnalyzerValidator.cs +++ b/Ares.Core/Validation/Validators/GoodAnalyzerValidator.cs @@ -53,7 +53,8 @@ public static async Task Validate(ExperimentTemplate experimen if(matchingCommand is null) continue; - var cmdSchema = matchingCommand.Metadata.OutputMetadata?.DataSchema; + // TODO: Handle other command template types as well AB 7/9/2026 + var cmdSchema = matchingCommand.DeviceCommand?.Metadata?.OutputMetadata?.DataSchema; if(cmdSchema is null) continue; 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/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/ViewModels/CommandDesignerViewModel.cs b/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs index 07bcfb1d..40101927 100644 --- a/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs +++ b/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs @@ -1,8 +1,7 @@ -using Ares.Datamodel; +using Ares.Core.Grpc.Services; 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; @@ -41,7 +40,8 @@ public CommandDesignerViewModel(CommandParameterDesignerFactory commandParameter CommandTemplate = new CommandTemplate { - UniqueId = Guid.NewGuid().ToString() + UniqueId = Guid.NewGuid().ToString(), + DeviceCommand = new DeviceCommand() }; } @@ -52,7 +52,7 @@ public CommandTemplate CommandTemplate set { _commandTemplate = value; - CommandMetadata = value.Metadata; + CommandMetadata = value.DeviceCommand?.Metadata; InitTemplate(value); } } @@ -61,7 +61,7 @@ public CommandTemplate CommandTemplate public string? TemplateDeviceName { get; private set; } public string? MetadataDeviceName { get; private set; } - public string? TemplateCommandName => CommandTemplate.Metadata?.Name; + public string? TemplateCommandName => CommandTemplate.DeviceCommand?.Metadata?.Name; public bool TemplateOutputProvider => CommandTemplate.HasOutputVarName; @@ -71,7 +71,7 @@ public CommandTemplate CommandTemplate public string? OutputVariableName { get; set; } - public IEnumerable Arguments => CommandTemplate.Parameters; + public IEnumerable Arguments => CommandTemplate.ArgumentBindings; public CommandMetadata? CommandMetadata { @@ -89,16 +89,18 @@ public CommandMetadata? CommandMetadata [Reactive] public partial IEnumerable ArgumentDesigners { get; private set; } + // TODO: Ensure we handle the other command template types AB 7/9/2026 public CommandTemplate Save() { - CommandTemplate.Parameters.Clear(); - CommandTemplate.Parameters.AddRange(ArgumentDesigners.Select(model => model.Save())); + CommandTemplate.ArgumentBindings.Clear(); + CommandTemplate.ArgumentBindings.AddRange(ArgumentDesigners.Select(model => model.Save())); if(CommandMetadata is not null) { - CommandTemplate.Metadata = CommandMetadata; - CommandTemplate.Metadata.DeviceType = MetadataDeviceName; + CommandTemplate.DeviceCommand ??= new DeviceCommand(); + CommandTemplate.DeviceCommand.Metadata = CommandMetadata; + CommandTemplate.DeviceCommand.Metadata.DeviceType = MetadataDeviceName; } - + CommandTemplate.Index = Index; CommandTemplate.ClearOutputVarName(); @@ -113,27 +115,28 @@ public CommandTemplate Save() private async Task InitTemplate(CommandTemplate existingTemplate) { Index = Convert.ToInt32(existingTemplate.Index); - var existingParamDesigners = existingTemplate.Parameters.Select(_commandParameterDesignerFactory.Create).ToArray(); + var existingParamDesigners = existingTemplate.ArgumentBindings.Select(_commandParameterDesignerFactory.Create).ToArray(); ArgumentDesigners = [.. existingParamDesigners]; ApplyAvailableVariableReferences(); - MetadataPickerViewModel = _metadataPickerFactory.Create(existingTemplate.Metadata); + MetadataPickerViewModel = _metadataPickerFactory.Create(existingTemplate.DeviceCommand?.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) + if(existingTemplate.DeviceCommand?.Metadata?.DeviceId is not null) { - var deviceInfo = await _devicesClient.GetDeviceInfo(new DeviceInfoRequest { DeviceId = existingTemplate.Metadata.DeviceId }, null); + var deviceInfo = await _devicesClient.GetDeviceInfo(new DeviceInfoRequest { DeviceId = existingTemplate.DeviceCommand.Metadata.DeviceId }, null); TemplateDeviceName = string.IsNullOrEmpty(deviceInfo.Name) ? null : deviceInfo.Name; } } public async Task MetadataUpdated(CommandMetadata? metadata) { - CommandTemplate.Metadata = metadata; + CommandTemplate.DeviceCommand ??= new DeviceCommand(); + CommandTemplate.DeviceCommand.Metadata = metadata; CommandMetadata = metadata; } diff --git a/UI/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs b/UI/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs index fa9fb32a..759e37e7 100644 --- a/UI/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs +++ b/UI/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs @@ -28,8 +28,8 @@ public static CommandOutputVariableReference[] Build(CommandTemplate commandTemp { if(!commandTemplate.HasOutputVarName) return []; - - var outputSchema = commandTemplate.Metadata?.OutputMetadata?.DataSchema; + // TODO: Check if we need to do this to other command template types AB 7/9/2026 + var outputSchema = commandTemplate.DeviceCommand?.Metadata?.OutputMetadata?.DataSchema; if(outputSchema is null) return []; diff --git a/UI/Features/CustomCommand/CustomCommandBuilder.razor b/UI/Features/CustomCommand/CustomCommandBuilder.razor index 4983908a..d30f691f 100644 --- a/UI/Features/CustomCommand/CustomCommandBuilder.razor +++ b/UI/Features/CustomCommand/CustomCommandBuilder.razor @@ -7,7 +7,7 @@ @using AresScript.Symbols @using UI.Application.Notifications @using UI.Application.Scripting -@using CustomCommandModel = Ares.Datamodel.Automation.CustomCommand +@using CustomCommandModel = Ares.Datamodel.Automation.CustomCommandVersion @using CustomCommandParameterModel = Ares.Datamodel.Automation.CustomCommandParameter @inject IMonacoCompletionProvider CompletionProvider @inject IMonacoDiagnosticsProvider DiagnosticsProvider From c0af44dbaef1e00f3de14d846e6300c3e0af3a86 Mon Sep 17 00:00:00 2001 From: "Babeckis, Arnas" Date: Thu, 9 Jul 2026 18:43:49 -0400 Subject: [PATCH 15/32] Gutted the ares core device and moved its functionality to a system operation executor. --- Ares.Core/CoreDevice/AresCoreDevice.cs | 220 ------------------ Ares.Core/CoreDevice/AresCoreDeviceCommand.cs | 11 - .../AresCoreDeviceCommandParameter.cs | 8 - .../CustomCommandPersistenceService.cs | 11 - Ares.Core/Device/Managers/DeviceManager.cs | 6 +- .../Execution/Executors/CommandExecutor.cs | 17 +- .../Executors/Composers/StepComposer.cs | 15 +- .../Executors/ExecutorSummaryHelpers.cs | 17 +- .../Executors/SystemOperationExecutor.cs | 92 ++++++++ .../ExperimentTemplateExtensions.cs | 9 +- AresOS.slnx | 1 + .../Repos/DeviceControlViewModelRepo.cs | 3 +- 12 files changed, 143 insertions(+), 267 deletions(-) delete mode 100644 Ares.Core/CoreDevice/AresCoreDevice.cs delete mode 100644 Ares.Core/CoreDevice/AresCoreDeviceCommand.cs delete mode 100644 Ares.Core/CoreDevice/AresCoreDeviceCommandParameter.cs create mode 100644 Ares.Core/Execution/Executors/SystemOperationExecutor.cs 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/CustomCommands/CustomCommandPersistenceService.cs b/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs index 068ae621..8db510be 100644 --- a/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs +++ b/Ares.Core/CustomCommands/CustomCommandPersistenceService.cs @@ -1,8 +1,6 @@ using Ares.Datamodel.Automation; using Ares.Datamodel.Extensions; -using Google.Protobuf; using Microsoft.EntityFrameworkCore; -using System.Security.Cryptography; using CustomCommandModel = Ares.Datamodel.Automation.CustomCommand; using CustomCommandVersionModel = Ares.Datamodel.Automation.CustomCommandVersion; @@ -153,18 +151,9 @@ private static CustomCommandVersionModel CreateVersion( version.UniqueId = string.Empty; version.CustomCommandId = commandId.ToString(); version.VersionNumber = versionNumber; - version.ContentHash = ComputeContentHash(version); return version; } - private static string ComputeContentHash(CustomCommandVersionModel version) - { - var hashInput = version.Clone(); - hashInput.UniqueId = string.Empty; - hashInput.ContentHash = string.Empty; - return Convert.ToHexString(SHA256.HashData(hashInput.ToByteArray())); - } - private static string BuildInputSummary(CustomCommandVersionModel command) { return command.InputParameters.Count == 0 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/Execution/Executors/CommandExecutor.cs b/Ares.Core/Execution/Executors/CommandExecutor.cs index 33eb3fea..b6351d5a 100644 --- a/Ares.Core/Execution/Executors/CommandExecutor.cs +++ b/Ares.Core/Execution/Executors/CommandExecutor.cs @@ -17,17 +17,28 @@ public class CommandExecutor : IExecutor> command, CommandTemplate template, INotifier notifier, ISystemSettingsManager settingsManager) { _command = command; Template = template; + var commandName = template.CommandTypeCase switch + { + CommandTemplate.CommandTypeOneofCase.DeviceCommand => template.DeviceCommand.Metadata.Name, + CommandTemplate.CommandTypeOneofCase.SystemCommand => template.SystemCommand.Operation.ToString(), + CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation => "Custom Command", + _ => "Undefined Command" + }; + + var executionTarget = template.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.DeviceCommand + ? template.DeviceCommand.Metadata.DeviceType + : "ARES"; + var executionStatus = new CommandExecutionStatus { CommandId = template.UniqueId, - CommandName = template.DeviceCommand.Metadata.Name, - DeviceName = template.DeviceCommand.Metadata.DeviceType, + CommandName = commandName, + DeviceName = executionTarget, State = ExecutionState.Undefined }; diff --git a/Ares.Core/Execution/Executors/Composers/StepComposer.cs b/Ares.Core/Execution/Executors/Composers/StepComposer.cs index d07011c9..7c0d3914 100644 --- a/Ares.Core/Execution/Executors/Composers/StepComposer.cs +++ b/Ares.Core/Execution/Executors/Composers/StepComposer.cs @@ -28,9 +28,22 @@ public StepExecutor Compose(StepTemplate template) .OrderBy(t => t.Index) .Select ( - // TODO make sure we handle the rest of command template types AB 7/9/2026 + // TODO Support Custom Commands AB 7/9/2026 commandTemplate => { + if(commandTemplate.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.SystemCommand) + { + Func> systemAction = ct => SystemOperationExecutor.Execute( + commandTemplate.SystemCommand.Operation, + commandTemplate.ArgumentBindings, + ct); + + return new CommandExecutor(systemAction, commandTemplate, _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) diff --git a/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs b/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs index a61a6995..53b5e055 100644 --- a/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs +++ b/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs @@ -48,7 +48,18 @@ public static CommandExecutionSummary CreateCommandExecutionSummary(CommandTempl DateTime startTime, DateTime endTime) { - // TODO: Handle the other potential command template command types AB 7/9/2026 + var commandName = template.CommandTypeCase switch + { + CommandTemplate.CommandTypeOneofCase.DeviceCommand => template.DeviceCommand.Metadata.Name, + CommandTemplate.CommandTypeOneofCase.SystemCommand => template.SystemCommand.Operation.ToString(), + CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation => "Custom Command", // TODO More descriptive AB 7/9/2026 + _ => "Undefined Command" + }; + + var commandDescription = template.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.DeviceCommand + ? template.DeviceCommand.Metadata.Description + : string.Empty; + var commandExecutionSummary = new CommandExecutionSummary { UniqueId = Guid.NewGuid().ToString(), @@ -56,8 +67,8 @@ public static CommandExecutionSummary CreateCommandExecutionSummary(CommandTempl CommandId = Guid.NewGuid().ToString(), Result = deviceResult, TemplateId = template.UniqueId, - CommandDescription = template.DeviceCommand.Metadata.Description, - CommandName = template.DeviceCommand.Metadata.Name, + CommandDescription = commandDescription, + CommandName = commandName, StatusCode = deviceResult?.StatusCode ?? CommandStatusCode.StatusUnspecified }; 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 9a66882b..c3b62148 100644 --- a/Ares.Core/Execution/Extensions/ExperimentTemplateExtensions.cs +++ b/Ares.Core/Execution/Extensions/ExperimentTemplateExtensions.cs @@ -81,11 +81,14 @@ public static ExperimentTemplate CloneWithNewIds(this ExperimentTemplate templat var cmdTemplateId = Guid.NewGuid().ToString(); var outputCmd = template.GetAllOutputCommands().FirstOrDefault(oc => oc.UniqueId == commandTemplate.UniqueId); - commandTemplate.DeviceCommand.Metadata.UniqueId = Guid.NewGuid().ToString(); commandTemplate.UniqueId = cmdTemplateId; - foreach(var metadataParameterMetadata in commandTemplate.DeviceCommand.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.ArgumentBindings) { diff --git a/AresOS.slnx b/AresOS.slnx index 1200f484..439e66c8 100644 --- a/AresOS.slnx +++ b/AresOS.slnx @@ -4,6 +4,7 @@ + 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) From 9a2b78fc36cfa3b25afe6a8b8e1a9f8670ca78c8 Mon Sep 17 00:00:00 2001 From: "Babeckis, Arnas" Date: Fri, 10 Jul 2026 00:26:17 -0400 Subject: [PATCH 16/32] Added an executor for the custom command. --- Ares.Core.Tests/Data/TestCampaignProvider.cs | 4 +- .../CampaignDatasetGeneratorTests.cs | 11 +- .../CommandVariableResolutionTests.cs | 24 ++-- .../Execution/Composers/StepComposerTests.cs | 8 +- .../Execution/CustomCommandExecutorTests.cs | 53 +++++++ .../Validation/GoodAnalyzerValidatorTests.cs | 9 +- .../Executors/Composers/StepComposer.cs | 22 ++- .../Executors/CustomCommandExecutor.cs | 133 ++++++++++++++++++ Ares.Core/ServiceCollectionExtensions.cs | 3 +- .../CustomCommandScriptBuilder.cs | 1 + 10 files changed, 243 insertions(+), 25 deletions(-) create mode 100644 Ares.Core.Tests/Execution/CustomCommandExecutorTests.cs create mode 100644 Ares.Core/Execution/Executors/CustomCommandExecutor.cs 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 2d8130a2..7ec55188 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/CommandVariableResolutionTests.cs b/Ares.Core.Tests/Execution/CommandVariableResolutionTests.cs index 7e883a4f..8b5d80a4 100644 --- a/Ares.Core.Tests/Execution/CommandVariableResolutionTests.cs +++ b/Ares.Core.Tests/Execution/CommandVariableResolutionTests.cs @@ -85,13 +85,16 @@ private static CommandTemplate CreateSourceCommand() 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 +105,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..da19d71a 100644 --- a/Ares.Core.Tests/Execution/Composers/StepComposerTests.cs +++ b/Ares.Core.Tests/Execution/Composers/StepComposerTests.cs @@ -26,28 +26,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 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/Validation/GoodAnalyzerValidatorTests.cs b/Ares.Core.Tests/Validation/GoodAnalyzerValidatorTests.cs index 6a49ba36..c33d6254 100644 --- a/Ares.Core.Tests/Validation/GoodAnalyzerValidatorTests.cs +++ b/Ares.Core.Tests/Validation/GoodAnalyzerValidatorTests.cs @@ -96,11 +96,14 @@ private static ExperimentTemplate CreateExperimentTemplate(AresValueSchema outpu stepTemplate.CommandTemplates.Add(new CommandTemplate { OutputVarName = "result", - Metadata = new CommandMetadata + DeviceCommand = new DeviceCommand { - OutputMetadata = new OutputMetadata + Metadata = new CommandMetadata { - DataSchema = outputSchema + OutputMetadata = new OutputMetadata + { + DataSchema = outputSchema + } } } }); diff --git a/Ares.Core/Execution/Executors/Composers/StepComposer.cs b/Ares.Core/Execution/Executors/Composers/StepComposer.cs index 7c0d3914..ac706057 100644 --- a/Ares.Core/Execution/Executors/Composers/StepComposer.cs +++ b/Ares.Core/Execution/Executors/Composers/StepComposer.cs @@ -13,11 +13,17 @@ 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; + public StepComposer( + IAresDeviceRepo deviceRepo, + INotifier notifier, + ISystemSettingsManager settingsManager, + CustomCommandExecutor? customCommandExecutor = null) { _deviceRepo = deviceRepo; _notifier = notifier; _settingsManager = settingsManager; + _customCommandExecutor = customCommandExecutor; } public StepExecutor Compose(StepTemplate template) @@ -28,7 +34,6 @@ public StepExecutor Compose(StepTemplate template) .OrderBy(t => t.Index) .Select ( - // TODO Support Custom Commands AB 7/9/2026 commandTemplate => { if(commandTemplate.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.SystemCommand) @@ -41,6 +46,19 @@ public StepExecutor Compose(StepTemplate template) return new CommandExecutor(systemAction, commandTemplate, _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, _notifier, _settingsManager); + } + if(commandTemplate.CommandTypeCase != CommandTemplate.CommandTypeOneofCase.DeviceCommand) throw new InvalidOperationException($"Unsupported command type: {commandTemplate.CommandTypeCase}"); 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/ServiceCollectionExtensions.cs b/Ares.Core/ServiceCollectionExtensions.cs index 9528e2db..ed5b4506 100644 --- a/Ares.Core/ServiceCollectionExtensions.cs +++ b/Ares.Core/ServiceCollectionExtensions.cs @@ -69,7 +69,8 @@ public static void AddAresCoreComponents(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs b/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs index dd28a49c..5a7d778c 100644 --- a/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs +++ b/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs @@ -5,6 +5,7 @@ namespace AresScript.ScriptBuilding; +// TODO: maybe move to the core instead also check if it can use some of the existing builders. public static class CustomCommandScriptBuilder { private const string FunctionPrefix = "custom_command_"; From 81b597b20e186ac940f91f454af723a3dfa6c5d3 Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Fri, 10 Jul 2026 19:34:10 -0400 Subject: [PATCH 17/32] Add support for actually creating the different available command template types. --- AGENTS.md | 4 +- .../CustomCommandScriptBuilder.cs | 7 - .../Factories/CommandDesignerFactory.cs | 21 +- .../ViewModels/AnalyzerDesignerViewModel.cs | 6 +- .../ViewModels/CommandDesignerViewModel.cs | 407 ++++++++++++++---- .../CommandOutputVariableReference.cs | 14 +- .../ViewModels/StepDesignerViewModel.cs | 6 + .../CampaignEdit/Views/CommandDesigner.razor | 114 ++++- .../Views/CommandDesignerQuickView.razor | 15 +- .../CampaignEdit/Views/MetadataPicker.razor | 8 +- .../CampaignEdit/Views/StepDesigner.razor | 5 +- .../Persistence/Scripts/AddMigration.zsh | 6 +- 12 files changed, 483 insertions(+), 130 deletions(-) mode change 100644 => 100755 UI/Infrastructure/Persistence/Scripts/AddMigration.zsh 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/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs b/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs index 5a7d778c..a2e9c33a 100644 --- a/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs +++ b/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs @@ -5,7 +5,6 @@ namespace AresScript.ScriptBuilding; -// TODO: maybe move to the core instead also check if it can use some of the existing builders. public static class CustomCommandScriptBuilder { private const string FunctionPrefix = "custom_command_"; @@ -27,12 +26,6 @@ public static string BuildFunctionName(string commandName) return $"{FunctionPrefix}{safeName}"; } - public static string ToTypeHint(AresValueSchema schema) - { - ArgumentNullException.ThrowIfNull(schema); - return AresScriptSchemaFormatter.ToTypeHint(schema); - } - public static string BuildFunctionSignature( string commandName, IEnumerable parameters, 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/CommandDesignerViewModel.cs b/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs index 40101927..2a7272e7 100644 --- a/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs +++ b/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs @@ -1,4 +1,8 @@ -using Ares.Core.Grpc.Services; +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; @@ -13,61 +17,136 @@ 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(), - DeviceCommand = new DeviceCommand() - }; } - 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 + { + 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 { - get => _commandTemplate; + 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.DeviceCommand?.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 string? TemplateDeviceName { get; private set; } - public string? MetadataDeviceName { get; private set; } - public string? TemplateCommandName => CommandTemplate.DeviceCommand?.Metadata?.Name; + public IReadOnlyList AvailableCustomCommands { get; private set; } + + public string? CustomCommandLoadError { get; private set; } + + 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; } @@ -76,91 +155,63 @@ public CommandTemplate CommandTemplate 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; } - // TODO: Ensure we handle the other command template types AB 7/9/2026 + public Task EnsureInitializedAsync() => _initializationTask; + public CommandTemplate Save() { CommandTemplate.ArgumentBindings.Clear(); CommandTemplate.ArgumentBindings.AddRange(ArgumentDesigners.Select(model => model.Save())); - if(CommandMetadata is not null) + + if(CommandTypeCase == CommandTemplate.CommandTypeOneofCase.DeviceCommand && CommandMetadata is not null) { - CommandTemplate.DeviceCommand ??= new DeviceCommand(); CommandTemplate.DeviceCommand.Metadata = CommandMetadata; - CommandTemplate.DeviceCommand.Metadata.DeviceType = MetadataDeviceName; + 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) - { - Index = Convert.ToInt32(existingTemplate.Index); - var existingParamDesigners = existingTemplate.ArgumentBindings.Select(_commandParameterDesignerFactory.Create).ToArray(); - ArgumentDesigners = [.. existingParamDesigners]; - ApplyAvailableVariableReferences(); - MetadataPickerViewModel = _metadataPickerFactory.Create(existingTemplate.DeviceCommand?.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.DeviceCommand?.Metadata?.DeviceId is not null) - { - - var deviceInfo = await _devicesClient.GetDeviceInfo(new DeviceInfoRequest { DeviceId = existingTemplate.DeviceCommand.Metadata.DeviceId }, null); - TemplateDeviceName = string.IsNullOrEmpty(deviceInfo.Name) ? null : deviceInfo.Name; - } - } - - public async Task MetadataUpdated(CommandMetadata? metadata) + public async Task DeviceCommandMetadataUpdated(CommandMetadata? metadata) { CommandTemplate.DeviceCommand ??= new DeviceCommand(); CommandTemplate.DeviceCommand.Metadata = metadata; CommandMetadata = metadata; + await ApplyDeviceMetadataAsync(metadata, preserveBindings: false); + RaiseCommandPropertiesChanged(); } - private async Task InitMetadata(CommandMetadata? existingMetadata) + public void SelectSystemOperation(SystemOperation operation) { - 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; + CommandTemplate.SystemCommand ??= new SystemCommand(); + CommandTemplate.SystemCommand.Operation = operation; + var definition = SystemOperationCatalog.Find(operation); + SetArgumentDefinitions(definition?.Parameters ?? []); + ResetOutputAssignment(); + RaiseCommandPropertiesChanged(); + } - if(deviceId is not null) - { - var deviceInfo = await _devicesClient.GetDeviceInfo(new DeviceInfoRequest { DeviceId = deviceId }, null); - MetadataDeviceName = string.IsNullOrEmpty(deviceInfo.Name) ? null : deviceInfo.Name; - } + public void SelectCustomCommand(string? customCommandId) + { + 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) @@ -186,13 +237,203 @@ public CommandOutputVariableReference[] GetOutputVariableReferences() ParameterSource.Variable when !argumentDesigner.HasSelectedVariableReference() => "Command output variable is no longer available.", - _ => 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(definition.Clone()); + + var normalizedBinding = binding.Clone(); + normalizedBinding.Metadata = definition.Clone(); + return _commandParameterDesignerFactory.Create(normalizedBinding); + }).ToArray(); + ApplyAvailableVariableReferences(); + } + + 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 759e37e7..cd95154c 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,7 +18,7 @@ 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 []; @@ -28,9 +29,14 @@ public static CommandOutputVariableReference[] Build(CommandTemplate commandTemp { if(!commandTemplate.HasOutputVarName) return []; - // TODO: Check if we need to do this to other command template types AB 7/9/2026 - var outputSchema = commandTemplate.DeviceCommand?.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, + _ => null + }; + if(outputSchema is null || outputSchema.Type is AresDataType.Unit or AresDataType.UnspecifiedType) return []; return Build(commandTemplate.OutputVarName, outputSchema).ToArray(); 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/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/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..." From 081a28b81cfa979c3d091f37fdfba5167d4ffb73 Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Fri, 10 Jul 2026 20:04:57 -0400 Subject: [PATCH 18/32] Forgot to commit the system operations catalog --- .../Execution/SystemOperationCatalogTests.cs | 76 +++++++++++++++++ .../Executors/SystemOperationCatalog.cs | 84 +++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 Ares.Core.Tests/Execution/SystemOperationCatalogTests.cs create mode 100644 Ares.Core/Execution/Executors/SystemOperationCatalog.cs 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/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() + }; +} From b135a7f701eedf822553fc1740d8e1ef71a6f3e0 Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Fri, 10 Jul 2026 20:06:48 -0400 Subject: [PATCH 19/32] Added migrations for the custom command data stuff --- ...ddCustomCommands_AresDbContext.Designer.cs | 2269 ++++++++++++++++ ...0160725_AddCustomCommands_AresDbContext.cs | 277 ++ ...omCommands_AresIdentityContext.Designer.cs | 277 ++ ...6_AddCustomCommands_AresIdentityContext.cs | 22 + .../Migrations/AresDbContextModelSnapshot.cs | 163 +- ...ddCustomCommands_AresDbContext.Designer.cs | 2283 +++++++++++++++++ ...0160728_AddCustomCommands_AresDbContext.cs | 280 ++ ...omCommands_AresIdentityContext.Designer.cs | 279 ++ ...9_AddCustomCommands_AresIdentityContext.cs | 22 + .../Migrations/AresDbContextModelSnapshot.cs | 167 +- ...ddCustomCommands_AresDbContext.Designer.cs | 2264 ++++++++++++++++ ...0160722_AddCustomCommands_AresDbContext.cs | 283 ++ ...omCommands_AresIdentityContext.Designer.cs | 268 ++ ...3_AddCustomCommands_AresIdentityContext.cs | 22 + .../Migrations/AresDbContextModelSnapshot.cs | 161 +- 15 files changed, 8968 insertions(+), 69 deletions(-) create mode 100644 AresService.Migrations.Postgres/Migrations/20260710160725_AddCustomCommands_AresDbContext.Designer.cs create mode 100644 AresService.Migrations.Postgres/Migrations/20260710160725_AddCustomCommands_AresDbContext.cs create mode 100644 AresService.Migrations.Postgres/Migrations/20260710160726_AddCustomCommands_AresIdentityContext.Designer.cs create mode 100644 AresService.Migrations.Postgres/Migrations/20260710160726_AddCustomCommands_AresIdentityContext.cs create mode 100644 AresService.Migrations.SqlServer/Migrations/20260710160728_AddCustomCommands_AresDbContext.Designer.cs create mode 100644 AresService.Migrations.SqlServer/Migrations/20260710160728_AddCustomCommands_AresDbContext.cs create mode 100644 AresService.Migrations.SqlServer/Migrations/20260710160729_AddCustomCommands_AresIdentityContext.Designer.cs create mode 100644 AresService.Migrations.SqlServer/Migrations/20260710160729_AddCustomCommands_AresIdentityContext.cs create mode 100644 AresService.Migrations.Sqlite/Migrations/20260710160722_AddCustomCommands_AresDbContext.Designer.cs create mode 100644 AresService.Migrations.Sqlite/Migrations/20260710160722_AddCustomCommands_AresDbContext.cs create mode 100644 AresService.Migrations.Sqlite/Migrations/20260710160723_AddCustomCommands_AresIdentityContext.Designer.cs create mode 100644 AresService.Migrations.Sqlite/Migrations/20260710160723_AddCustomCommands_AresIdentityContext.cs 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 e87b3bbf..fc9d1512 100644 --- a/AresService.Migrations.Postgres/Migrations/AresDbContextModelSnapshot.cs +++ b/AresService.Migrations.Postgres/Migrations/AresDbContextModelSnapshot.cs @@ -321,7 +321,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasDefaultValueSql("NOW()"); - b.Property("Description") + b.Property("CurrentVersionId") .HasColumnType("text"); b.Property("LastModified") @@ -329,21 +329,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone") .HasDefaultValueSql("NOW()"); - b.Property("Name") - .HasColumnType("text"); + b.HasKey("UniqueId"); - b.Property("OutputSchema") + 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("ScriptBody") + b.Property("Schema") .HasColumnType("text"); b.HasKey("UniqueId"); - b.ToTable("CustomCommands", (string)null); + b.HasIndex("CustomCommandVersionId"); + + b.ToTable("CustomCommandParameters", (string)null); }); - modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => { b.Property("UniqueId") .ValueGeneratedOnAdd() @@ -357,6 +380,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CustomCommandId") .HasColumnType("uuid"); + b.Property("Description") + .HasColumnType("text"); + b.Property("LastModified") .ValueGeneratedOnAddOrUpdate() .HasColumnType("timestamp with time zone") @@ -365,14 +391,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Name") .HasColumnType("text"); - b.Property("Schema") + b.Property("OutputSchema") .HasColumnType("text"); + b.Property("ScriptBody") + .HasColumnType("text"); + + b.Property("VersionNumber") + .HasColumnType("bigint"); + b.HasKey("UniqueId"); - b.HasIndex("CustomCommandId"); + b.HasIndex("CustomCommandId", "VersionNumber") + .IsUnique(); - b.ToTable("CustomCommandParameters", (string)null); + b.ToTable("CustomCommandVersions", (string)null); }); modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => @@ -1361,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") @@ -1372,6 +1402,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description") .HasColumnType("text"); + b.Property("DeviceCommandId") + .HasColumnType("uuid"); + b.Property("DeviceId") .HasColumnType("text"); @@ -1388,7 +1421,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("UniqueId"); - b.HasIndex("CommandTemplateId") + b.HasIndex("DeviceCommandId") .IsUnique(); b.ToTable("CommandMetadata"); @@ -1426,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") @@ -1783,8 +1843,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => { - b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + 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(); @@ -1949,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 => @@ -1963,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 => @@ -1995,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); @@ -2035,7 +2147,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Capabilities"); }); - modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => { b.Navigation("InputParameters"); }); @@ -2119,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/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 e5e96969..2df66b6b 100644 --- a/AresService.Migrations.SqlServer/Migrations/AresDbContextModelSnapshot.cs +++ b/AresService.Migrations.SqlServer/Migrations/AresDbContextModelSnapshot.cs @@ -322,7 +322,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime2") .HasDefaultValueSql("getdate()"); - b.Property("Description") + b.Property("CurrentVersionId") .HasColumnType("nvarchar(max)"); b.Property("LastModified") @@ -330,21 +330,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime2") .HasDefaultValueSql("getdate()"); - b.Property("Name") - .HasColumnType("nvarchar(max)"); + b.HasKey("UniqueId"); - b.Property("OutputSchema") + 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("ScriptBody") + b.Property("Schema") .HasColumnType("nvarchar(max)"); b.HasKey("UniqueId"); - b.ToTable("CustomCommands", (string)null); + b.HasIndex("CustomCommandVersionId"); + + b.ToTable("CustomCommandParameters", (string)null); }); - modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => { b.Property("UniqueId") .ValueGeneratedOnAdd() @@ -358,6 +381,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CustomCommandId") .HasColumnType("uniqueidentifier"); + b.Property("Description") + .HasColumnType("nvarchar(max)"); + b.Property("LastModified") .ValueGeneratedOnAddOrUpdate() .HasColumnType("datetime2") @@ -366,14 +392,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Name") .HasColumnType("nvarchar(max)"); - b.Property("Schema") + b.Property("OutputSchema") .HasColumnType("nvarchar(max)"); + b.Property("ScriptBody") + .HasColumnType("nvarchar(max)"); + + b.Property("VersionNumber") + .HasColumnType("bigint"); + b.HasKey("UniqueId"); - b.HasIndex("CustomCommandId"); + b.HasIndex("CustomCommandId", "VersionNumber") + .IsUnique(); - b.ToTable("CustomCommandParameters", (string)null); + b.ToTable("CustomCommandVersions", (string)null); }); modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => @@ -1369,9 +1402,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("CommandTemplateId") - .HasColumnType("uniqueidentifier"); - b.Property("CreationTime") .ValueGeneratedOnAdd() .HasColumnType("datetime2") @@ -1380,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)"); @@ -1396,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"); }); @@ -1434,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") @@ -1795,8 +1857,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => { - b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + 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(); @@ -1961,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 => @@ -1975,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 => @@ -2007,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); @@ -2047,7 +2161,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Capabilities"); }); - modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => { b.Navigation("InputParameters"); }); @@ -2131,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/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 1a1c86b0..683b7476 100644 --- a/AresService.Migrations.Sqlite/Migrations/AresDbContextModelSnapshot.cs +++ b/AresService.Migrations.Sqlite/Migrations/AresDbContextModelSnapshot.cs @@ -316,7 +316,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasDefaultValueSql("DATETIME('now')"); - b.Property("Description") + b.Property("CurrentVersionId") .HasColumnType("TEXT"); b.Property("LastModified") @@ -324,21 +324,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasDefaultValueSql("DATETIME('now')"); - b.Property("Name") + b.HasKey("UniqueId"); + + b.ToTable("CustomCommands", (string)null); + }); + + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + { + b.Property("UniqueId") + .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.Property("OutputSchema") + b.Property("CreationTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("CustomCommandVersionId") .HasColumnType("TEXT"); - b.Property("ScriptBody") + b.Property("LastModified") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT") + .HasDefaultValueSql("DATETIME('now')"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Schema") .HasColumnType("TEXT"); b.HasKey("UniqueId"); - b.ToTable("CustomCommands", (string)null); + b.HasIndex("CustomCommandVersionId"); + + b.ToTable("CustomCommandParameters", (string)null); }); - modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => { b.Property("UniqueId") .ValueGeneratedOnAdd() @@ -352,6 +375,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CustomCommandId") .HasColumnType("TEXT"); + b.Property("Description") + .HasColumnType("TEXT"); + b.Property("LastModified") .ValueGeneratedOnAddOrUpdate() .HasColumnType("TEXT") @@ -360,14 +386,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Name") .HasColumnType("TEXT"); - b.Property("Schema") + b.Property("OutputSchema") + .HasColumnType("TEXT"); + + b.Property("ScriptBody") .HasColumnType("TEXT"); + b.Property("VersionNumber") + .HasColumnType("INTEGER"); + b.HasKey("UniqueId"); - b.HasIndex("CustomCommandId"); + b.HasIndex("CustomCommandId", "VersionNumber") + .IsUnique(); - b.ToTable("CustomCommandParameters", (string)null); + b.ToTable("CustomCommandVersions", (string)null); }); modelBuilder.Entity("Ares.Datamodel.CampaignExecutionSummary", b => @@ -1356,9 +1389,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.Property("CommandTemplateId") - .HasColumnType("TEXT"); - b.Property("CreationTime") .ValueGeneratedOnAdd() .HasColumnType("TEXT") @@ -1367,6 +1397,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description") .HasColumnType("TEXT"); + b.Property("DeviceCommandId") + .HasColumnType("TEXT"); + b.Property("DeviceId") .HasColumnType("TEXT"); @@ -1383,7 +1416,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("UniqueId"); - b.HasIndex("CommandTemplateId") + b.HasIndex("DeviceCommandId") .IsUnique(); b.ToTable("CommandMetadata"); @@ -1421,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") @@ -1778,8 +1838,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandParameter", b => { - b.HasOne("Ares.Datamodel.Automation.CustomCommand", null) + 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(); @@ -1944,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 => @@ -1958,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 => @@ -1990,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); @@ -2030,7 +2142,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Capabilities"); }); - modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommand", b => + modelBuilder.Entity("Ares.Datamodel.Automation.CustomCommandVersion", b => { b.Navigation("InputParameters"); }); @@ -2114,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 => From 2356017253bbccfac13af47e8af488e26b48c951 Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Fri, 10 Jul 2026 20:20:06 -0400 Subject: [PATCH 20/32] Fixed custom commands not having proper output schema for later reference. --- AresOS.slnx | 1 + ...mandOutputVariableReferenceBuilderTests.cs | 145 ++++++++++++++++++ UI.Tests/UI.Tests.csproj | 28 ++++ UI.Tests/Usings.cs | 1 + .../Experiments/ExperimentViewer.razor | 37 ++++- UI/Components/Experiments/StepViewer.razor | 7 +- .../CommandOutputVariableReference.cs | 13 +- 7 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 UI.Tests/Features/CampaignEdit/ViewModels/CommandOutputVariableReferenceBuilderTests.cs create mode 100644 UI.Tests/UI.Tests.csproj create mode 100644 UI.Tests/Usings.cs diff --git a/AresOS.slnx b/AresOS.slnx index 439e66c8..44a8bd01 100644 --- a/AresOS.slnx +++ b/AresOS.slnx @@ -15,4 +15,5 @@ + 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..7ec72b7b --- /dev/null +++ b/UI.Tests/UI.Tests.csproj @@ -0,0 +1,28 @@ + + + + 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/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/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs b/UI/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs index cd95154c..b766b5cf 100644 --- a/UI/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs +++ b/UI/Features/CampaignEdit/ViewModels/CommandOutputVariableReference.cs @@ -25,7 +25,9 @@ public static CommandOutputVariableReference[] Build(CommandDesignerViewModel co return Build(commandDesigner.OutputVariableName, outputSchema).ToArray(); } - public static CommandOutputVariableReference[] Build(CommandTemplate commandTemplate) + public static CommandOutputVariableReference[] Build( + CommandTemplate commandTemplate, + IReadOnlyDictionary customCommandOutputSchemas) { if(!commandTemplate.HasOutputVarName) return []; @@ -34,6 +36,8 @@ public static CommandOutputVariableReference[] Build(CommandTemplate commandTemp { 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) @@ -42,6 +46,13 @@ public static CommandOutputVariableReference[] Build(CommandTemplate commandTemp 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) From b1ad7b985175a5a3849bc13cdac672e7aa71e001 Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Fri, 10 Jul 2026 21:16:23 -0400 Subject: [PATCH 21/32] Fixed funny script translation issues when it comes to custom command creation. --- AresScript.Tests/AresScriptBuilderTests.cs | 26 ++++++++++++++++--- .../CustomCommandScriptBuilder.cs | 25 +++++++----------- UI/Components/ScriptEditor.razor | 2 +- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/AresScript.Tests/AresScriptBuilderTests.cs b/AresScript.Tests/AresScriptBuilderTests.cs index 9f56e259..8c76de83 100644 --- a/AresScript.Tests/AresScriptBuilderTests.cs +++ b/AresScript.Tests/AresScriptBuilderTests.cs @@ -326,12 +326,12 @@ public void CustomCommandScriptBuilder_BuildWrappedScript_WritesNestedSchemaType 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(script, Does.Contain($"{System.Environment.NewLine} {System.Environment.NewLine} return measurement")); Assert.That(() => Parse(script), Throws.Nothing); } [Test] - public void CustomCommandScriptBuilder_BuildWrappedScript_UsesReturnFallbackForEmptyBody() + public void CustomCommandScriptBuilder_BuildWrappedScript_PreservesWhitespaceBeforeReturnFallback() { var script = CustomCommandScriptBuilder.BuildWrappedScript( "No Op", @@ -339,7 +339,27 @@ public void CustomCommandScriptBuilder_BuildWrappedScript_UsesReturnFallbackForE AresSchemaBuilder.Entry(AresDataType.Unit).Build(), " "); - Assert.That(script, Is.EqualTo($"def custom_command_No_Op() -> Unit:{System.Environment.NewLine} return")); + 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); } diff --git a/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs b/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs index a2e9c33a..4dc971e0 100644 --- a/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs +++ b/AresScript/ScriptBuilding/CustomCommandScriptBuilder.cs @@ -48,29 +48,22 @@ public static string BuildWrappedScript( foreach(var line in normalizedBody.Split('\n')) { - if(line.Length == 0) - { - output.AppendLine(); - } - else - { - output.Append(" ").AppendLine(line); - } + output.Append(" ").AppendLine(line); } - return output.ToString().TrimEnd('\r', '\n'); + if(string.IsNullOrWhiteSpace(normalizedBody)) + { + output.Append(" ").Append(EmptyBodyFallback); + } + + return output.ToString(); } private static string NormalizeBody(string? scriptBody) { - if(string.IsNullOrWhiteSpace(scriptBody)) - { - return EmptyBodyFallback; - } - - return scriptBody + return scriptBody? .Replace("\r\n", "\n", StringComparison.Ordinal) .Replace('\r', '\n') - .Trim('\n'); + ?? string.Empty; } } diff --git a/UI/Components/ScriptEditor.razor b/UI/Components/ScriptEditor.razor index 82977105..aa31bc41 100644 --- a/UI/Components/ScriptEditor.razor +++ b/UI/Components/ScriptEditor.razor @@ -158,7 +158,7 @@ private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor) { - var initialScript = string.IsNullOrWhiteSpace(InitialScript) ? DefaultScript : InitialScript; + var initialScript = InitialScript ?? DefaultScript; return new StandaloneEditorConstructionOptions { From 199c6f7a2a01b767925e5bf224c07a5711ce8bd0 Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Fri, 10 Jul 2026 21:23:58 -0400 Subject: [PATCH 22/32] Fixed false positive parameter assignment popping up. --- .../CampaignEdit/ViewModels/CommandDesignerViewModel.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs b/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs index 2a7272e7..b052a3cf 100644 --- a/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs +++ b/UI/Features/CampaignEdit/ViewModels/CommandDesignerViewModel.cs @@ -237,6 +237,9 @@ public CommandOutputVariableReference[] GetOutputVariableReferences() ParameterSource.Variable when !argumentDesigner.HasSelectedVariableReference() => "Command output variable is no longer available.", + ParameterSource.Planned or ParameterSource.Variable + => null, + ParameterSource.Unspecified or ParameterSource.Value or ParameterSource.Environment => null, From c6d79e28676e2a0f78f9004a4cd7dab7769085c8 Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Fri, 10 Jul 2026 21:42:50 -0400 Subject: [PATCH 23/32] Fixed naming of Custom Commands within the experiment execution page. --- .../Execution/CampaignExecutorTests.cs | 4 +- .../CommandDisplayNameResolverTests.cs | 86 +++++++++++++++++++ .../Execution/CommandExecutorNameTests.cs | 34 ++++++++ .../CommandVariableResolutionTests.cs | 10 ++- .../Execution/Composers/StepComposerTests.cs | 33 ++++++- .../Execution/ExecutionManagerTests.cs | 8 +- Ares.Core/Execution/ExecutionManager.cs | 4 + .../Executors/CommandDisplayNameResolver.cs | 50 +++++++++++ .../Execution/Executors/CommandExecutor.cs | 16 ++-- .../Executors/Composers/StepComposer.cs | 11 ++- .../Executors/ExecutorSummaryHelpers.cs | 9 +- .../Executors/ICommandDisplayNameResolver.cs | 10 +++ Ares.Core/ServiceCollectionExtensions.cs | 1 + 13 files changed, 246 insertions(+), 30 deletions(-) create mode 100644 Ares.Core.Tests/Execution/CommandDisplayNameResolverTests.cs create mode 100644 Ares.Core.Tests/Execution/CommandExecutorNameTests.cs create mode 100644 Ares.Core/Execution/Executors/CommandDisplayNameResolver.cs create mode 100644 Ares.Core/Execution/Executors/ICommandDisplayNameResolver.cs diff --git a/Ares.Core.Tests/Execution/CampaignExecutorTests.cs b/Ares.Core.Tests/Execution/CampaignExecutorTests.cs index f0f74be2..5c76c946 100644 --- a/Ares.Core.Tests/Execution/CampaignExecutorTests.cs +++ b/Ares.Core.Tests/Execution/CampaignExecutorTests.cs @@ -87,7 +87,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/CommandVariableResolutionTests.cs b/Ares.Core.Tests/Execution/CommandVariableResolutionTests.cs index 8b5d80a4..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,6 +80,13 @@ 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() { diff --git a/Ares.Core.Tests/Execution/Composers/StepComposerTests.cs b/Ares.Core.Tests/Execution/Composers/StepComposerTests.cs index da19d71a..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() @@ -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/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/Execution/ExecutionManager.cs b/Ares.Core/Execution/ExecutionManager.cs index 1c6ce9e3..0ac008d8 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; 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 b6351d5a..a61d4dce 100644 --- a/Ares.Core/Execution/Executors/CommandExecutor.cs +++ b/Ares.Core/Execution/Executors/CommandExecutor.cs @@ -15,21 +15,15 @@ public class CommandExecutor : IExecutor _stateSubject; private readonly INotifier _notifier; 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 commandName = template.CommandTypeCase switch - { - CommandTemplate.CommandTypeOneofCase.DeviceCommand => template.DeviceCommand.Metadata.Name, - CommandTemplate.CommandTypeOneofCase.SystemCommand => template.SystemCommand.Operation.ToString(), - CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation => "Custom Command", - _ => "Undefined Command" - }; - var executionTarget = template.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.DeviceCommand ? template.DeviceCommand.Metadata.DeviceType : "ARES"; @@ -77,7 +71,7 @@ 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; @@ -124,7 +118,7 @@ public async Task Execute(ExecutionControlToken token, _stateSubject.OnNext(Status); _stateSubject.OnCompleted(); - 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 ac706057..34bc8e85 100644 --- a/Ares.Core/Execution/Executors/Composers/StepComposer.cs +++ b/Ares.Core/Execution/Executors/Composers/StepComposer.cs @@ -14,15 +14,18 @@ public class StepComposer : ICommandComposer private readonly IAresDeviceRepo _deviceRepo; private readonly 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; } @@ -36,6 +39,8 @@ public StepExecutor Compose(StepTemplate template) ( commandTemplate => { + var commandName = _commandDisplayNameResolver.Resolve(commandTemplate); + if(commandTemplate.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.SystemCommand) { Func> systemAction = ct => SystemOperationExecutor.Execute( @@ -43,7 +48,7 @@ public StepExecutor Compose(StepTemplate template) commandTemplate.ArgumentBindings, ct); - return new CommandExecutor(systemAction, commandTemplate, _notifier, _settingsManager); + return new CommandExecutor(systemAction, commandTemplate, commandName, _notifier, _settingsManager); } if(commandTemplate.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation) @@ -56,7 +61,7 @@ public StepExecutor Compose(StepTemplate template) commandTemplate.ArgumentBindings, ct); - return new CommandExecutor(customCommandAction, commandTemplate, _notifier, _settingsManager); + return new CommandExecutor(customCommandAction, commandTemplate, commandName, _notifier, _settingsManager); } if(commandTemplate.CommandTypeCase != CommandTemplate.CommandTypeOneofCase.DeviceCommand) @@ -77,7 +82,7 @@ public StepExecutor Compose(StepTemplate template) 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/ExecutorSummaryHelpers.cs b/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs index 53b5e055..f4a2fe83 100644 --- a/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs +++ b/Ares.Core/Execution/Executors/ExecutorSummaryHelpers.cs @@ -44,18 +44,11 @@ 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 commandName = template.CommandTypeCase switch - { - CommandTemplate.CommandTypeOneofCase.DeviceCommand => template.DeviceCommand.Metadata.Name, - CommandTemplate.CommandTypeOneofCase.SystemCommand => template.SystemCommand.Operation.ToString(), - CommandTemplate.CommandTypeOneofCase.CustomCommandInvocation => "Custom Command", // TODO More descriptive AB 7/9/2026 - _ => "Undefined Command" - }; - var commandDescription = template.CommandTypeCase == CommandTemplate.CommandTypeOneofCase.DeviceCommand ? template.DeviceCommand.Metadata.Description : string.Empty; 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/ServiceCollectionExtensions.cs b/Ares.Core/ServiceCollectionExtensions.cs index ed5b4506..50e3cd77 100644 --- a/Ares.Core/ServiceCollectionExtensions.cs +++ b/Ares.Core/ServiceCollectionExtensions.cs @@ -70,6 +70,7 @@ public static void AddAresCoreComponents(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); From 7ac58be6e704513913e1c60f57c58bb72cf11460 Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Fri, 10 Jul 2026 21:43:53 -0400 Subject: [PATCH 24/32] Removed datamodel project reference from the solution file. --- AresOS.slnx | 1 - 1 file changed, 1 deletion(-) diff --git a/AresOS.slnx b/AresOS.slnx index 44a8bd01..6fe27cf5 100644 --- a/AresOS.slnx +++ b/AresOS.slnx @@ -4,7 +4,6 @@ - From 253d453fa8f9d7f1fe89724e9c29c044b56476a2 Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Fri, 10 Jul 2026 22:08:00 -0400 Subject: [PATCH 25/32] Fixed persistence issues. --- .../CommandTemplatePersistenceTests.cs | 46 +++++++++++++++++++ .../CommandTemplateEntityConfiguration.cs | 3 ++ ...CorruptedParameterSources_AresDbContext.cs | 2 +- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 Ares.Core.Tests/Execution/CommandTemplatePersistenceTests.cs 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/EntityConfigurations/Execution/Commands/CommandTemplateEntityConfiguration.cs b/Ares.Core/EntityConfigurations/Execution/Commands/CommandTemplateEntityConfiguration.cs index 7ad161ad..47d9c847 100644 --- a/Ares.Core/EntityConfigurations/Execution/Commands/CommandTemplateEntityConfiguration.cs +++ b/Ares.Core/EntityConfigurations/Execution/Commands/CommandTemplateEntityConfiguration.cs @@ -21,6 +21,9 @@ public override void Configure(EntityTypeBuilder builder) builder.OwnsOne(template => template.SystemCommand); builder.OwnsOne(template => template.CustomCommandInvocation); + builder.Navigation(template => template.DeviceCommand) + .AutoInclude(); + builder.Navigation(commandTemplate => commandTemplate.ArgumentBindings) .AutoInclude(); } 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';"); } /// From ef4d5b524e44f45d19d9a86ec908df6074416683 Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Sat, 11 Jul 2026 13:56:18 -0400 Subject: [PATCH 26/32] Made campaign templates be stored and retrieved from the database. --- Ares.Core.Grpc/Services/AutomationService.cs | 181 ++++++------------ Ares.Core.Tests/Ares.Core.Tests.csproj | 10 +- ...tomationServiceCampaignPersistenceTests.cs | 96 ++++++++++ ...CampaignTemplatePersistenceServiceTests.cs | 165 ++++++++++++++++ .../CampaignTemplatePersistenceService.cs | 112 +++++++++++ .../ICampaignTemplatePersistenceService.cs | 23 +++ .../ExperimentTemplateExtensions.cs | 2 - Ares.Core/ServiceCollectionExtensions.cs | 2 + .../Validators/GoodAnalyzerValidator.cs | 1 - 9 files changed, 463 insertions(+), 129 deletions(-) create mode 100644 Ares.Core.Tests/Campaigns/AutomationServiceCampaignPersistenceTests.cs create mode 100644 Ares.Core.Tests/Campaigns/CampaignTemplatePersistenceServiceTests.cs create mode 100644 Ares.Core/Campaigns/CampaignTemplatePersistenceService.cs create mode 100644 Ares.Core/Campaigns/ICampaignTemplatePersistenceService.cs diff --git a/Ares.Core.Grpc/Services/AutomationService.cs b/Ares.Core.Grpc/Services/AutomationService.cs index 626b3629..3f8ba65a 100644 --- a/Ares.Core.Grpc/Services/AutomationService.cs +++ b/Ares.Core.Grpc/Services/AutomationService.cs @@ -1,13 +1,13 @@ 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.Analyzing; +using Ares.Core.Campaigns; using Ares.Core.EntityConfigurations.Helpers; using Ares.Core.Execution; using Ares.Core.Execution.StartConditions; @@ -39,7 +39,8 @@ public class AutomationService : AresAutomation.AresAutomationBase private JsonSerializerOptions _serializerSettings; private readonly IPlannerServiceRepo _plannerServiceRepo; private readonly IPlannerTransactionProvider _plannerTransactionProvider; - private readonly IAnalyzerTransactionProvider _analyzerTransactionProvider; + private readonly IAnalyzerTransactionProvider _analyzerTransactionProvider; + private readonly ICampaignTemplatePersistenceService _campaignTemplatePersistenceService; public AutomationService(IDbContextFactory coreContextFactory, IExecutionManager executionManager, @@ -48,9 +49,10 @@ public AutomationService(IDbContextFactory coreContextFacto IEnumerable startConditions, IEnumerable notificationHandlers, IDesiredAnalysisResultFactory desiredAnalysisResultFactory, - IPlannerServiceRepo plannerServiceRepo, - IPlannerTransactionProvider plannerTransactionProvider, - IAnalyzerTransactionProvider analyzerTransactionProvider) + IPlannerServiceRepo plannerServiceRepo, + IPlannerTransactionProvider plannerTransactionProvider, + IAnalyzerTransactionProvider analyzerTransactionProvider, + ICampaignTemplatePersistenceService campaignTemplatePersistenceService) { _desiredAnalysisResultFactory = desiredAnalysisResultFactory; _coreContextFactory = coreContextFactory; @@ -62,7 +64,8 @@ public AutomationService(IDbContextFactory coreContextFacto _notificationHandlers = notificationHandlers; _plannerServiceRepo = plannerServiceRepo; _plannerTransactionProvider = plannerTransactionProvider; - _analyzerTransactionProvider = analyzerTransactionProvider; + _analyzerTransactionProvider = analyzerTransactionProvider; + _campaignTemplatePersistenceService = campaignTemplatePersistenceService; } public override async Task GetAllProjects(Empty request, ServerCallContext? context) @@ -74,86 +77,38 @@ public override async Task GetAllProjects(Empty request, Serve return response; } - 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); - } - } - - return campaignResponse; - } - - public override Task CampaignExists(CampaignRequest request, ServerCallContext? context) - { - if(request.HasUniqueId) - return Task.FromResult(FindCampaignById(request)); - - 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 }; - } + public override async Task GetAllCampaigns(GetAllCampaignsRequest request, ServerCallContext? context) + { + var campaignResponse = new GetAllCampaignsResponse(); + var campaigns = await _campaignTemplatePersistenceService.GetSummariesAsync(context?.CancellationToken ?? CancellationToken.None); + campaignResponse.Campaigns.AddRange(campaigns); + + return campaignResponse; + } + + public override async Task CampaignExists(CampaignRequest request, ServerCallContext? context) + { + var cancellationToken = context?.CancellationToken ?? CancellationToken.None; + if(request.HasUniqueId) + return new BoolValue { Value = await _campaignTemplatePersistenceService.ExistsByIdAsync(request.UniqueId, cancellationToken) }; + + else + 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()); - } - - 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()); + return new Empty(); + } + + await _campaignTemplatePersistenceService.DeleteAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None); + return new Empty(); } public override async Task GetProject(ProjectRequest request, ServerCallContext? context) @@ -187,51 +142,33 @@ public override async Task AddProject(Project request, ServerCallContext? /// /// /// - public override 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()); - } - - public override 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 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); - } - - 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; - } + public override async Task AddCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context) + { + await _campaignTemplatePersistenceService.AddAsync(request.Template, context?.CancellationToken ?? CancellationToken.None); + return new Empty(); + } + + public override async Task UpdateCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context) + { + 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 request.Template; + } + + private async Task GetCampaignTemplate(CampaignRequest request, ServerCallContext? context) + { + 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; } 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..478604c4 --- /dev/null +++ b/Ares.Core.Tests/Campaigns/AutomationServiceCampaignPersistenceTests.cs @@ -0,0 +1,96 @@ +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; + +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(); + persistence.Setup(service => service.GetByIdAsync(campaign.UniqueId, It.IsAny())).ReturnsAsync(campaign); + var service = CreateService(persistence.Object, new ActiveCampaignTemplateStore()); + + var response = await service.GetCopyOfCampaign(new CampaignRequest { UniqueId = campaign.UniqueId }, null!); + + using(Assert.EnterMultipleScope()) + { + Assert.That(response.Template.UniqueId, Is.Not.EqualTo(campaign.UniqueId)); + Assert.That(response.Template.Name, Is.EqualTo("Campaign-Copy")); + Assert.That(response.SerializedJsonData, Does.Contain(response.Template.UniqueId)); + } + } + + private static AutomationService CreateService( + ICampaignTemplatePersistenceService persistence, + IActiveCampaignTemplateStore activeStore) + => new( + Mock.Of>(), + Mock.Of(), + Mock.Of(), + activeStore, + [], + [], + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + persistence); +} 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/Campaigns/CampaignTemplatePersistenceService.cs b/Ares.Core/Campaigns/CampaignTemplatePersistenceService.cs new file mode 100644 index 00000000..da13cb5f --- /dev/null +++ b/Ares.Core/Campaigns/CampaignTemplatePersistenceService.cs @@ -0,0 +1,112 @@ +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); + 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); + 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); + } +} 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/Execution/Extensions/ExperimentTemplateExtensions.cs b/Ares.Core/Execution/Extensions/ExperimentTemplateExtensions.cs index c3b62148..1bd8c645 100644 --- a/Ares.Core/Execution/Extensions/ExperimentTemplateExtensions.cs +++ b/Ares.Core/Execution/Extensions/ExperimentTemplateExtensions.cs @@ -75,11 +75,9 @@ public static ExperimentTemplate CloneWithNewIds(this ExperimentTemplate templat foreach(var stepTemplate in newTemplate.StepTemplates) { stepTemplate.UniqueId = Guid.NewGuid().ToString(); - // TODO ensure all the commands not just device commands are taken care of AB 7/9/2026 foreach(var commandTemplate in stepTemplate.CommandTemplates) { var cmdTemplateId = Guid.NewGuid().ToString(); - var outputCmd = template.GetAllOutputCommands().FirstOrDefault(oc => oc.UniqueId == commandTemplate.UniqueId); commandTemplate.UniqueId = cmdTemplateId; diff --git a/Ares.Core/ServiceCollectionExtensions.cs b/Ares.Core/ServiceCollectionExtensions.cs index 50e3cd77..07074f68 100644 --- a/Ares.Core/ServiceCollectionExtensions.cs +++ b/Ares.Core/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ 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; @@ -70,6 +71,7 @@ public static void AddAresCoreComponents(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Ares.Core/Validation/Validators/GoodAnalyzerValidator.cs b/Ares.Core/Validation/Validators/GoodAnalyzerValidator.cs index 21adf74d..1c05d649 100644 --- a/Ares.Core/Validation/Validators/GoodAnalyzerValidator.cs +++ b/Ares.Core/Validation/Validators/GoodAnalyzerValidator.cs @@ -53,7 +53,6 @@ public static async Task Validate(ExperimentTemplate experimen if(matchingCommand is null) continue; - // TODO: Handle other command template types as well AB 7/9/2026 var cmdSchema = matchingCommand.DeviceCommand?.Metadata?.OutputMetadata?.DataSchema; if(cmdSchema is null) continue; From daa781ce14e93e1677c0423e0b69607df112131a Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Sat, 11 Jul 2026 14:52:19 -0400 Subject: [PATCH 27/32] Campaign template import/export support. Added legacy transformer to support the pre command template rework template imports. --- Ares.Core.Grpc/Services/AutomationService.cs | 31 +- ...tomationServiceCampaignPersistenceTests.cs | 17 +- .../CampaignTemplateTransferServiceTests.cs | 266 ++++++++++++++++++ .../CampaignTemplateImportException.cs | 4 + .../CampaignTemplateLegacyJsonConverter.cs | 57 ++++ .../CampaignTemplatePersistenceService.cs | 19 ++ .../CampaignTemplateTransferService.cs | 218 ++++++++++++++ .../ICampaignTemplateTransferService.cs | 14 + Ares.Core/ServiceCollectionExtensions.cs | 1 + .../ViewModels/CampaignListViewModelTests.cs | 40 +++ UI.Tests/UI.Tests.csproj | 1 + .../ViewModels/CampaignListViewModel.cs | 23 +- .../CampaignEdit/Views/CampaignList.razor | 53 +++- 13 files changed, 707 insertions(+), 37 deletions(-) create mode 100644 Ares.Core.Tests/Campaigns/CampaignTemplateTransferServiceTests.cs create mode 100644 Ares.Core/Campaigns/CampaignTemplateImportException.cs create mode 100644 Ares.Core/Campaigns/CampaignTemplateLegacyJsonConverter.cs create mode 100644 Ares.Core/Campaigns/CampaignTemplateTransferService.cs create mode 100644 Ares.Core/Campaigns/ICampaignTemplateTransferService.cs create mode 100644 UI.Tests/Features/CampaignEdit/ViewModels/CampaignListViewModelTests.cs diff --git a/Ares.Core.Grpc/Services/AutomationService.cs b/Ares.Core.Grpc/Services/AutomationService.cs index 3f8ba65a..1663fe4c 100644 --- a/Ares.Core.Grpc/Services/AutomationService.cs +++ b/Ares.Core.Grpc/Services/AutomationService.cs @@ -4,11 +4,9 @@ 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.Campaigns; -using Ares.Core.EntityConfigurations.Helpers; using Ares.Core.Execution; using Ares.Core.Execution.StartConditions; using Ares.Core.Execution.StopConditions; @@ -36,11 +34,11 @@ public class AutomationService : AresAutomation.AresAutomationBase private readonly IEnumerable _startConditions; private readonly IEnumerable _notificationHandlers; readonly IDesiredAnalysisResultFactory _desiredAnalysisResultFactory; - 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, @@ -52,7 +50,8 @@ public AutomationService(IDbContextFactory coreContextFacto IPlannerServiceRepo plannerServiceRepo, IPlannerTransactionProvider plannerTransactionProvider, IAnalyzerTransactionProvider analyzerTransactionProvider, - ICampaignTemplatePersistenceService campaignTemplatePersistenceService) + ICampaignTemplatePersistenceService campaignTemplatePersistenceService, + ICampaignTemplateTransferService campaignTemplateTransferService) { _desiredAnalysisResultFactory = desiredAnalysisResultFactory; _coreContextFactory = coreContextFactory; @@ -60,12 +59,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) @@ -473,19 +472,15 @@ public override async Task GetCampaignSummary(Campaign return summary; } - 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 templateCopy = template.Clone(); - templateCopy.UniqueId = Guid.NewGuid().ToString(); - templateCopy.Name = $"{template.Name}-Copy"; - response.Template = templateCopy; - response.SerializedJsonData = JsonSerializer.Serialize(templateCopy, _serializerSettings); - } + public override async Task GetCopyOfCampaign(CampaignRequest request, ServerCallContext context) + { + var response = new GetCopyOfCampaignResponse(); + var export = await _campaignTemplateTransferService.ExportAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None); + if(export is not null) + { + response.Template = export.Template; + response.SerializedJsonData = export.Json; + } return response; } diff --git a/Ares.Core.Tests/Campaigns/AutomationServiceCampaignPersistenceTests.cs b/Ares.Core.Tests/Campaigns/AutomationServiceCampaignPersistenceTests.cs index 478604c4..2cf91fd1 100644 --- a/Ares.Core.Tests/Campaigns/AutomationServiceCampaignPersistenceTests.cs +++ b/Ares.Core.Tests/Campaigns/AutomationServiceCampaignPersistenceTests.cs @@ -65,22 +65,24 @@ public async Task GetCopyOfCampaign_ExportsDatabaseCampaignAsJson() { var campaign = new CampaignTemplate { UniqueId = Guid.NewGuid().ToString(), Name = "Campaign" }; var persistence = new Mock(); - persistence.Setup(service => service.GetByIdAsync(campaign.UniqueId, It.IsAny())).ReturnsAsync(campaign); - var service = CreateService(persistence.Object, new ActiveCampaignTemplateStore()); + 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.UniqueId, Is.Not.EqualTo(campaign.UniqueId)); - Assert.That(response.Template.Name, Is.EqualTo("Campaign-Copy")); - Assert.That(response.SerializedJsonData, Does.Contain(response.Template.UniqueId)); + Assert.That(response.Template, Is.SameAs(campaign)); + Assert.That(response.SerializedJsonData, Does.Contain("campaign-id")); } } private static AutomationService CreateService( ICampaignTemplatePersistenceService persistence, - IActiveCampaignTemplateStore activeStore) + IActiveCampaignTemplateStore activeStore, + ICampaignTemplateTransferService transferService = null) => new( Mock.Of>(), Mock.Of(), @@ -92,5 +94,6 @@ private static AutomationService CreateService( Mock.Of(), Mock.Of(), Mock.Of(), - persistence); + persistence, + transferService ?? Mock.Of()); } 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/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 index da13cb5f..2d268df2 100644 --- a/Ares.Core/Campaigns/CampaignTemplatePersistenceService.cs +++ b/Ares.Core/Campaigns/CampaignTemplatePersistenceService.cs @@ -55,6 +55,7 @@ public async Task ExistsByNameAsync(string name, CancellationToken cancell 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); } @@ -70,6 +71,7 @@ public async Task ReplaceAsync(CampaignTemplate template, CancellationToke RemoveCampaignGraph(context, existingTemplate); await context.SaveChangesAsync(cancellationToken); + await CanonicalizePlannerReferencesAsync(context, template, cancellationToken); context.CampaignTemplates.Add(template); await context.SaveChangesAsync(cancellationToken); await transaction.CommitAsync(cancellationToken); @@ -109,4 +111,21 @@ private static void RemoveCampaignGraph(CoreDatabaseContext context, CampaignTem 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/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/ServiceCollectionExtensions.cs b/Ares.Core/ServiceCollectionExtensions.cs index 07074f68..d2720104 100644 --- a/Ares.Core/ServiceCollectionExtensions.cs +++ b/Ares.Core/ServiceCollectionExtensions.cs @@ -72,6 +72,7 @@ public static void AddAresCoreComponents(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); 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/UI.Tests.csproj b/UI.Tests/UI.Tests.csproj index 7ec72b7b..36a92d12 100644 --- a/UI.Tests/UI.Tests.csproj +++ b/UI.Tests/UI.Tests.csproj @@ -9,6 +9,7 @@ + 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/Views/CampaignList.razor b/UI/Features/CampaignEdit/Views/CampaignList.razor index 8e4df0f7..e9ac2714 100644 --- a/UI/Features/CampaignEdit/Views/CampaignList.razor +++ b/UI/Features/CampaignEdit/Views/CampaignList.razor @@ -2,10 +2,12 @@ @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
@@ -15,6 +17,13 @@ + @@ -55,6 +64,8 @@
@code { + private bool _isImporting; + protected override async Task OnInitializedAsync() { await ViewModel!.RefreshCampaigns(); @@ -85,19 +96,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(UploadChangeEventArgs args) + { + if(_isImporting || args.Files is null || !args.Files.Any()) + return; + + _isImporting = true; + try + { + var result = await ViewModel!.ImportCampaignTemplate(args.Files.Single()); + 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(); } } From dfd57bfd9c67807b17401c5f81af15e96b418ef5 Mon Sep 17 00:00:00 2001 From: Arnas Babeckis Date: Sat, 11 Jul 2026 15:12:43 -0400 Subject: [PATCH 28/32] Styling tweaks --- Ares.Core.Grpc/Services/AutomationService.cs | 156 +++++++++--------- .../CampaignEdit/Views/CampaignList.razor | 33 ++-- .../CustomCommand/CustomCommandList.razor | 8 +- .../CustomCommand/CustomCommandList.razor.css | 29 ---- 4 files changed, 100 insertions(+), 126 deletions(-) diff --git a/Ares.Core.Grpc/Services/AutomationService.cs b/Ares.Core.Grpc/Services/AutomationService.cs index 1663fe4c..4e109273 100644 --- a/Ares.Core.Grpc/Services/AutomationService.cs +++ b/Ares.Core.Grpc/Services/AutomationService.cs @@ -5,8 +5,8 @@ using System.Reactive.Linq; using System.Reactive.Threading.Tasks; using System.Threading.Tasks; -using Ares.Core.Analyzing; -using Ares.Core.Campaigns; +using Ares.Core.Analyzing; +using Ares.Core.Campaigns; using Ares.Core.Execution; using Ares.Core.Execution.StartConditions; using Ares.Core.Execution.StopConditions; @@ -19,9 +19,9 @@ using Microsoft.EntityFrameworkCore; using Ares.Datamodel.Planning; using Ares.Core.Execution.Extensions; -using Ares.Core.Planning; -using Ares.Datamodel.Analyzing; -using Ares.Datamodel.Extensions; +using Ares.Core.Planning; +using Ares.Datamodel.Analyzing; +using Ares.Datamodel.Extensions; namespace Ares.Core.Grpc.Services; @@ -36,9 +36,9 @@ public class AutomationService : AresAutomation.AresAutomationBase readonly IDesiredAnalysisResultFactory _desiredAnalysisResultFactory; private readonly IPlannerServiceRepo _plannerServiceRepo; private readonly IPlannerTransactionProvider _plannerTransactionProvider; - private readonly IAnalyzerTransactionProvider _analyzerTransactionProvider; - private readonly ICampaignTemplatePersistenceService _campaignTemplatePersistenceService; - private readonly ICampaignTemplateTransferService _campaignTemplateTransferService; + private readonly IAnalyzerTransactionProvider _analyzerTransactionProvider; + private readonly ICampaignTemplatePersistenceService _campaignTemplatePersistenceService; + private readonly ICampaignTemplateTransferService _campaignTemplateTransferService; public AutomationService(IDbContextFactory coreContextFactory, IExecutionManager executionManager, @@ -47,11 +47,11 @@ public AutomationService(IDbContextFactory coreContextFacto IEnumerable startConditions, IEnumerable notificationHandlers, IDesiredAnalysisResultFactory desiredAnalysisResultFactory, - IPlannerServiceRepo plannerServiceRepo, - IPlannerTransactionProvider plannerTransactionProvider, - IAnalyzerTransactionProvider analyzerTransactionProvider, - ICampaignTemplatePersistenceService campaignTemplatePersistenceService, - ICampaignTemplateTransferService campaignTemplateTransferService) + IPlannerServiceRepo plannerServiceRepo, + IPlannerTransactionProvider plannerTransactionProvider, + IAnalyzerTransactionProvider analyzerTransactionProvider, + ICampaignTemplatePersistenceService campaignTemplatePersistenceService, + ICampaignTemplateTransferService campaignTemplateTransferService) { _desiredAnalysisResultFactory = desiredAnalysisResultFactory; _coreContextFactory = coreContextFactory; @@ -62,9 +62,9 @@ public AutomationService(IDbContextFactory coreContextFacto _notificationHandlers = notificationHandlers; _plannerServiceRepo = plannerServiceRepo; _plannerTransactionProvider = plannerTransactionProvider; - _analyzerTransactionProvider = analyzerTransactionProvider; - _campaignTemplatePersistenceService = campaignTemplatePersistenceService; - _campaignTemplateTransferService = campaignTemplateTransferService; + _analyzerTransactionProvider = analyzerTransactionProvider; + _campaignTemplatePersistenceService = campaignTemplatePersistenceService; + _campaignTemplateTransferService = campaignTemplateTransferService; } public override async Task GetAllProjects(Empty request, ServerCallContext? context) @@ -76,38 +76,38 @@ public override async Task GetAllProjects(Empty request, Serve return response; } - public override async Task GetAllCampaigns(GetAllCampaignsRequest request, ServerCallContext? context) - { - var campaignResponse = new GetAllCampaignsResponse(); - var campaigns = await _campaignTemplatePersistenceService.GetSummariesAsync(context?.CancellationToken ?? CancellationToken.None); - campaignResponse.Campaigns.AddRange(campaigns); - - return campaignResponse; - } - - public override async Task CampaignExists(CampaignRequest request, ServerCallContext? context) - { - var cancellationToken = context?.CancellationToken ?? CancellationToken.None; - if(request.HasUniqueId) - return new BoolValue { Value = await _campaignTemplatePersistenceService.ExistsByIdAsync(request.UniqueId, cancellationToken) }; - - else - return new BoolValue { Value = await _campaignTemplatePersistenceService.ExistsByNameAsync(request.CampaignName, cancellationToken) }; - } + public override async Task GetAllCampaigns(GetAllCampaignsRequest request, ServerCallContext? context) + { + var campaignResponse = new GetAllCampaignsResponse(); + var campaigns = await _campaignTemplatePersistenceService.GetSummariesAsync(context?.CancellationToken ?? CancellationToken.None); + campaignResponse.Campaigns.AddRange(campaigns); + + return campaignResponse; + } + + public override async Task CampaignExists(CampaignRequest request, ServerCallContext? context) + { + var cancellationToken = context?.CancellationToken ?? CancellationToken.None; + if(request.HasUniqueId) + return new BoolValue { Value = await _campaignTemplatePersistenceService.ExistsByIdAsync(request.UniqueId, cancellationToken) }; + + else + return new BoolValue { Value = await _campaignTemplatePersistenceService.ExistsByNameAsync(request.CampaignName, cancellationToken) }; + } public override Task GetSingleCampaign(CampaignRequest request, ServerCallContext? context) => GetCampaignTemplate(request, context); - public override async 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 new Empty(); - } - - await _campaignTemplatePersistenceService.DeleteAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None); - return 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(); + } + + await _campaignTemplatePersistenceService.DeleteAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None); + return new Empty(); } public override async Task GetProject(ProjectRequest request, ServerCallContext? context) @@ -141,33 +141,33 @@ public override async Task AddProject(Project request, ServerCallContext? /// /// /// - public override async Task AddCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context) - { - await _campaignTemplatePersistenceService.AddAsync(request.Template, context?.CancellationToken ?? CancellationToken.None); - return new Empty(); - } - - public override async Task UpdateCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context) - { - 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 request.Template; - } - - private async Task GetCampaignTemplate(CampaignRequest request, ServerCallContext? context) - { - var campaign = await _campaignTemplatePersistenceService.GetByIdAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None); - if(campaign is not null) - return campaign; + public override async Task AddCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context) + { + await _campaignTemplatePersistenceService.AddAsync(request.Template, context?.CancellationToken ?? CancellationToken.None); + return new Empty(); + } + + public override async Task UpdateCampaign(AddOrUpdateCampaignRequest request, ServerCallContext? context) + { + 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 request.Template; + } + + private async Task GetCampaignTemplate(CampaignRequest request, ServerCallContext? context) + { + 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 campaign in the database. 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; } @@ -472,15 +472,15 @@ public override async Task GetCampaignSummary(Campaign return summary; } - public override async Task GetCopyOfCampaign(CampaignRequest request, ServerCallContext context) - { - var response = new GetCopyOfCampaignResponse(); - var export = await _campaignTemplateTransferService.ExportAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None); - if(export is not null) - { - response.Template = export.Template; - response.SerializedJsonData = export.Json; - } + public override async Task GetCopyOfCampaign(CampaignRequest request, ServerCallContext context) + { + var response = new GetCopyOfCampaignResponse(); + var export = await _campaignTemplateTransferService.ExportAsync(request.UniqueId, context?.CancellationToken ?? CancellationToken.None); + if(export is not null) + { + response.Template = export.Template; + response.SerializedJsonData = export.Json; + } return response; } @@ -504,10 +504,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/UI/Features/CampaignEdit/Views/CampaignList.razor b/UI/Features/CampaignEdit/Views/CampaignList.razor index e9ac2714..1079114b 100644 --- a/UI/Features/CampaignEdit/Views/CampaignList.razor +++ b/UI/Features/CampaignEdit/Views/CampaignList.razor @@ -14,17 +14,20 @@

Campaigns

- + - -
@@ -46,13 +49,13 @@ @template.UniqueId
- + - -
@@ -113,15 +116,15 @@ } } - private async Task ImportTemplate(UploadChangeEventArgs args) + private async Task ImportTemplate(InputFileChangeEventArgs args) { - if(_isImporting || args.Files is null || !args.Files.Any()) + if(_isImporting) return; _isImporting = true; try { - var result = await ViewModel!.ImportCampaignTemplate(args.Files.Single()); + var result = await ViewModel!.ImportCampaignTemplate(args.File); await ViewModel.RefreshCampaigns(); NotificationService.Success($"Imported campaign '{result.Template.Name}'."); if(result.Warnings.Count > 0) diff --git a/UI/Features/CustomCommand/CustomCommandList.razor b/UI/Features/CustomCommand/CustomCommandList.razor index e6e4d7ba..db2f64a0 100644 --- a/UI/Features/CustomCommand/CustomCommandList.razor +++ b/UI/Features/CustomCommand/CustomCommandList.razor @@ -14,10 +14,10 @@
- + -
@@ -62,10 +62,10 @@ @command.OutputSummary
- + -