From 236c314abb8c0a325c90d6b014fa6b78a584cc66 Mon Sep 17 00:00:00 2001 From: Taylor Marvin Date: Wed, 11 Mar 2026 23:44:49 -0700 Subject: [PATCH 01/11] fix: resolve workflow editor bugs and improve creation UX - Rename ControlStatement.Sequential to Default with backward-compatible DB converter that reads legacy Sequential values as Default - Replace freeform condition input with structured ConditionBuilder component supporting exit code comparisons and success status checks, with raw/advanced mode and regex validation - Add TargetTags (string[] as JSON) to Workflow entity with custom ValueComparer; expose via TagInput in workflow editor - Add schedule association UI to workflow editor with associate/ disassociate actions and available-schedule dropdown - Add TaskCreateModal for inline task creation from workflow editor with full form support (action types, arguments, tags, success criteria, timeout) - Update DagView node coloring to match renamed default control type - Regenerate Postgres and SQLite InitialCreate migrations with target_tags column on workflows table --- .../ControlStatementConverterTests.cs | 160 +++ .../Unit/Workflows/WorkflowServiceTests.cs | 90 ++ .../Components/ConditionBuilderTests.cs | 190 ++++ .../Components/TaskCreateModalTests.cs | 132 +++ .../Integration/WorkflowIntegrationTests.cs | 150 ++- .../Scheduling/WorkflowExecutionService.cs | 54 +- src/Werkr.Api/Endpoints/WorkflowEndpoints.cs | 87 ++ src/Werkr.Api/Models/WorkflowMapper.cs | 5 +- .../Services/ScheduleSyncGrpcService.cs | 13 +- .../Models/WorkflowCreateRequest.cs | 3 +- src/Werkr.Common/Models/WorkflowDto.cs | 3 +- .../WorkflowScheduleAssociateRequest.cs | 6 + .../Models/WorkflowStepCreateRequest.cs | 2 +- .../Models/WorkflowStepUpdateRequest.cs | 2 +- .../Models/WorkflowUpdateRequest.cs | 3 +- src/Werkr.Core/Workflows/WorkflowService.cs | 1 + .../Postgres/20260310023641_InitialCreate.cs | 341 ------ ... 20260312050754_InitialCreate.Designer.cs} | 4 +- .../Postgres/20260312050754_InitialCreate.cs | 366 +++++++ ...gresWerkrIdentityDbContextModelSnapshot.cs | 2 +- .../Sqlite/20260310023738_InitialCreate.cs | 298 ------ ... 20260312050800_InitialCreate.Designer.cs} | 4 +- .../Sqlite/20260312050800_InitialCreate.cs | 323 ++++++ ...liteWerkrIdentityDbContextModelSnapshot.cs | 2 +- .../Entities/Workflows/ControlStatement.cs | 6 +- src/Werkr.Data/Entities/Workflows/Workflow.cs | 3 + .../Entities/Workflows/WorkflowStep.cs | 4 +- .../Postgres/20260310023423_InitialCreate.cs | 923 ----------------- ... 20260312050739_InitialCreate.Designer.cs} | 8 +- .../Postgres/20260312050739_InitialCreate.cs | 977 ++++++++++++++++++ .../PostgresWerkrDbContextModelSnapshot.cs | 6 +- .../Sqlite/20260310023534_InitialCreate.cs | 814 --------------- ... 20260312050748_InitialCreate.Designer.cs} | 8 +- .../Sqlite/20260312050748_InitialCreate.cs | 868 ++++++++++++++++ .../SqliteWerkrDbContextModelSnapshot.cs | 6 +- src/Werkr.Data/WerkrDbContext.cs | 20 +- .../Components/Pages/Workflows/Edit.razor | 157 ++- .../Components/Shared/ConditionBuilder.razor | 170 +++ .../Components/Shared/DagView.razor | 2 +- .../Components/Shared/TaskCreateModal.razor | 195 ++++ 40 files changed, 3955 insertions(+), 2453 deletions(-) create mode 100644 src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs create mode 100644 src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs create mode 100644 src/Test/Werkr.Tests.Server/Components/TaskCreateModalTests.cs create mode 100644 src/Werkr.Common/Models/WorkflowScheduleAssociateRequest.cs delete mode 100644 src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.cs rename src/Werkr.Data.Identity/Migrations/Postgres/{20260310023641_InitialCreate.Designer.cs => 20260312050754_InitialCreate.Designer.cs} (99%) create mode 100644 src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.cs delete mode 100644 src/Werkr.Data.Identity/Migrations/Sqlite/20260310023738_InitialCreate.cs rename src/Werkr.Data.Identity/Migrations/Sqlite/{20260310023738_InitialCreate.Designer.cs => 20260312050800_InitialCreate.Designer.cs} (99%) create mode 100644 src/Werkr.Data.Identity/Migrations/Sqlite/20260312050800_InitialCreate.cs delete mode 100644 src/Werkr.Data/Migrations/Postgres/20260310023423_InitialCreate.cs rename src/Werkr.Data/Migrations/Postgres/{20260310023423_InitialCreate.Designer.cs => 20260312050739_InitialCreate.Designer.cs} (99%) create mode 100644 src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.cs delete mode 100644 src/Werkr.Data/Migrations/Sqlite/20260310023534_InitialCreate.cs rename src/Werkr.Data/Migrations/Sqlite/{20260310023534_InitialCreate.Designer.cs => 20260312050748_InitialCreate.Designer.cs} (99%) create mode 100644 src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.cs create mode 100644 src/Werkr.Server/Components/Shared/ConditionBuilder.razor create mode 100644 src/Werkr.Server/Components/Shared/TaskCreateModal.razor diff --git a/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs new file mode 100644 index 0000000..c8bb8c9 --- /dev/null +++ b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs @@ -0,0 +1,160 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Werkr.Data; +using Werkr.Data.Entities.Tasks; +using Werkr.Data.Entities.Workflows; + +namespace Werkr.Tests.Data.Unit.Workflows; + +/// +/// Tests that the ControlStatementStringConverter in +/// correctly round-trips enum values through the database +/// as strings, including backward-compatible reading of the legacy "Sequential" value. +/// +[TestClass] +public class ControlStatementConverterTests { + private SqliteConnection _connection = null!; + private SqliteWerkrDbContext _dbContext = null!; + + public TestContext TestContext { get; set; } = null!; + + [TestInitialize] + public void TestInit( ) { + _connection = new SqliteConnection( "DataSource=:memory:" ); + _connection.Open( ); + + DbContextOptions options = new DbContextOptionsBuilder( ) + .UseSqlite( _connection ) + .UseSnakeCaseNamingConvention( ) + .Options; + + _dbContext = new SqliteWerkrDbContext( options ); + _ = _dbContext.Database.EnsureCreated( ); + } + + [TestCleanup] + public void TestCleanup( ) { + _dbContext?.Dispose( ); + _connection?.Dispose( ); + } + + /// + /// Creates a workflow step with , saves, reloads, + /// and verifies it round-trips correctly and is stored as the string "Default". + /// + [TestMethod] + public async Task Default_RoundTrips_AsDefaultString( ) { + CancellationToken ct = TestContext.CancellationToken; + + // Arrange — create a workflow and task to host the step + Workflow workflow = new( ) { Name = "RoundTrip_WF", Description = "test" }; + _dbContext.Workflows.Add( workflow ); + WerkrTask task = new( ) { Name = "RoundTrip_Task", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] }; + _dbContext.Tasks.Add( task ); + _ = await _dbContext.SaveChangesAsync( ct ); + + WorkflowStep step = new( ) { + WorkflowId = workflow.Id, + TaskId = task.Id, + Order = 0, + ControlStatement = ControlStatement.Default, + }; + _dbContext.WorkflowSteps.Add( step ); + _ = await _dbContext.SaveChangesAsync( ct ); + + // Detach so the next query hits the database + _dbContext.ChangeTracker.Clear( ); + + // Act — reload from DB + WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync( s => s.Id == step.Id, ct ); + + // Assert — enum value round-trips + Assert.AreEqual( ControlStatement.Default, loaded.ControlStatement ); + + // Assert — raw string in the database is "Default" + long stepId = step.Id; + string? raw = await _dbContext.Database + .SqlQuery( $"SELECT control_statement AS Value FROM workflow_steps WHERE id = {stepId}" ) + .SingleAsync( ct ); + Assert.AreEqual( "Default", raw ); + } + + /// + /// Verifies that every non-Default enum member round-trips through the database correctly. + /// + [TestMethod] + [DataRow( ControlStatement.If, "If" )] + [DataRow( ControlStatement.Else, "Else" )] + [DataRow( ControlStatement.ElseIf, "ElseIf" )] + [DataRow( ControlStatement.While, "While" )] + [DataRow( ControlStatement.Do, "Do" )] + public async Task AllEnumValues_RoundTrip_Correctly( ControlStatement value, string expectedString ) { + CancellationToken ct = TestContext.CancellationToken; + + Workflow workflow = new( ) { Name = $"RoundTrip_{value}", Description = "test" }; + _dbContext.Workflows.Add( workflow ); + WerkrTask task = new( ) { Name = $"Task_{value}", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] }; + _dbContext.Tasks.Add( task ); + _ = await _dbContext.SaveChangesAsync( ct ); + + WorkflowStep step = new( ) { + WorkflowId = workflow.Id, + TaskId = task.Id, + Order = 0, + ControlStatement = value, + ConditionExpression = value is ControlStatement.If or ControlStatement.ElseIf or ControlStatement.While or ControlStatement.Do + ? "$? -eq $true" : null, + }; + _dbContext.WorkflowSteps.Add( step ); + _ = await _dbContext.SaveChangesAsync( ct ); + + _dbContext.ChangeTracker.Clear( ); + + WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync( s => s.Id == step.Id, ct ); + Assert.AreEqual( value, loaded.ControlStatement ); + + long stepId = step.Id; + string? raw = await _dbContext.Database + .SqlQuery( $"SELECT control_statement AS Value FROM workflow_steps WHERE id = {stepId}" ) + .SingleAsync( ct ); + Assert.AreEqual( expectedString, raw ); + } + + /// + /// Verifies that the legacy "Sequential" string value in the database is correctly + /// read as by the converter. + /// + [TestMethod] + public async Task LegacySequential_ReadsAs_Default( ) { + CancellationToken ct = TestContext.CancellationToken; + + Workflow workflow = new( ) { Name = "Legacy_WF", Description = "test" }; + _dbContext.Workflows.Add( workflow ); + WerkrTask task = new( ) { Name = "Legacy_Task", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] }; + _dbContext.Tasks.Add( task ); + _ = await _dbContext.SaveChangesAsync( ct ); + + // Insert a step with "Default" first (to get a valid row) + WorkflowStep step = new( ) { + WorkflowId = workflow.Id, + TaskId = task.Id, + Order = 0, + ControlStatement = ControlStatement.Default, + }; + _dbContext.WorkflowSteps.Add( step ); + _ = await _dbContext.SaveChangesAsync( ct ); + + // Manually overwrite the stored string to the legacy "Sequential" value + long stepId = step.Id; + _ = await _dbContext.Database.ExecuteSqlAsync( + $"UPDATE workflow_steps SET control_statement = 'Sequential' WHERE id = {stepId}", ct ); + + _dbContext.ChangeTracker.Clear( ); + + // Act — reload via EF + WorkflowStep loaded = await _dbContext.WorkflowSteps.SingleAsync( s => s.Id == step.Id, ct ); + + // Assert — converter maps "Sequential" → Default + Assert.AreEqual( ControlStatement.Default, loaded.ControlStatement ); + } +} diff --git a/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs b/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs index 51e9fae..1b9b200 100644 --- a/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs +++ b/src/Test/Werkr.Tests.Data/Unit/Workflows/WorkflowServiceTests.cs @@ -625,6 +625,96 @@ await _service.GetTopologicalLevelsAsync( ); // Level 1: B, C } + /// + /// Verifies that three independent steps (no dependencies) are all grouped at level 0. + /// + [TestMethod] + public async Task GetTopologicalLevelsAsync_ThreeIndependentSteps_AllAtLevelZero( ) { + CancellationToken ct = TestContext.CancellationToken; + Workflow workflow = new( ) { Name = "ThreeIndependent", Description = string.Empty }; + _ = await _service.CreateAsync( workflow, ct ); + await SeedTaskAsync( ct ); + + _ = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 0 }, ct ); + _ = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 1 }, ct ); + _ = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 2 }, ct ); + + IReadOnlyList> levels = + await _service.GetTopologicalLevelsAsync( workflow.Id, ct ); + + Assert.HasCount( 1, levels ); + Assert.HasCount( 3, levels[0] ); + } + + /// + /// Verifies a diamond-shaped DAG: A → B, A → C, B → D, C → D produces + /// three levels: [A], [B, C], [D]. B and C are parallelizable at level 1. + /// + [TestMethod] + public async Task GetTopologicalLevelsAsync_DiamondDag_GroupsCorrectly( ) { + CancellationToken ct = TestContext.CancellationToken; + Workflow workflow = new( ) { Name = "Diamond", Description = string.Empty }; + _ = await _service.CreateAsync( workflow, ct ); + await SeedTaskAsync( ct ); + + WorkflowStep a = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 0 }, ct ); + WorkflowStep b = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 1 }, ct ); + WorkflowStep c = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 2 }, ct ); + WorkflowStep d = await _service.AddStepAsync( workflow.Id, new WorkflowStep { TaskId = 1, Order = 3 }, ct ); + + await _service.AddStepDependencyAsync( b.Id, a.Id, ct ); + await _service.AddStepDependencyAsync( c.Id, a.Id, ct ); + await _service.AddStepDependencyAsync( d.Id, b.Id, ct ); + await _service.AddStepDependencyAsync( d.Id, c.Id, ct ); + + IReadOnlyList> levels = + await _service.GetTopologicalLevelsAsync( workflow.Id, ct ); + + Assert.HasCount( 3, levels ); + Assert.HasCount( 1, levels[0] ); // Level 0: A + Assert.HasCount( 2, levels[1] ); // Level 1: B, C (parallel) + Assert.HasCount( 1, levels[2] ); // Level 2: D + } + + /// + /// Verifies that If/ElseIf/Else steps at the same level are separated from Default steps + /// for independent chain-bound processing. Both sets share the same topological level + /// but the execution engine will partition them for sequential vs. parallel execution. + /// + [TestMethod] + public async Task GetTopologicalLevelsAsync_MixedControlStatements_SameLevelGrouped( ) { + CancellationToken ct = TestContext.CancellationToken; + Workflow workflow = new( ) { Name = "MixedControl", Description = string.Empty }; + _ = await _service.CreateAsync( workflow, ct ); + await SeedTaskAsync( ct ); + + // Root step + WorkflowStep root = await _service.AddStepAsync( + workflow.Id, new WorkflowStep { TaskId = 1, Order = 0 }, ct ); + + // Default step depends on root + WorkflowStep defaultStep = await _service.AddStepAsync( + workflow.Id, new WorkflowStep { TaskId = 1, Order = 1, ControlStatement = ControlStatement.Default }, ct ); + await _service.AddStepDependencyAsync( defaultStep.Id, root.Id, ct ); + + // If step depends on root + WorkflowStep ifStep = await _service.AddStepAsync( + workflow.Id, new WorkflowStep { TaskId = 1, Order = 2, ControlStatement = ControlStatement.If, ConditionExpression = "$? -eq $true" }, ct ); + await _service.AddStepDependencyAsync( ifStep.Id, root.Id, ct ); + + IReadOnlyList> levels = + await _service.GetTopologicalLevelsAsync( workflow.Id, ct ); + + Assert.HasCount( 2, levels ); + Assert.HasCount( 1, levels[0] ); // Level 0: root + Assert.HasCount( 2, levels[1] ); // Level 1: defaultStep + ifStep (execution engine partitions them) + + // Verify both steps are at level 1 with their correct control statements + HashSet controlStatements = [.. levels[1].Select( s => s.ControlStatement )]; + Assert.Contains( ControlStatement.Default, controlStatements ); + Assert.Contains( ControlStatement.If, controlStatements ); + } + // ── Control Flow Validation ── /// diff --git a/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs b/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs new file mode 100644 index 0000000..76b7505 --- /dev/null +++ b/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs @@ -0,0 +1,190 @@ +using Bunit; +using Microsoft.AspNetCore.Components; +using Werkr.Server.Components.Shared; + +namespace Werkr.Tests.Server.Components; + +/// +/// bUnit tests for the component. Validates structured mode +/// expression generation, operator/type switching, advanced/raw toggle, and expression validation. +/// +[TestClass] +public class ConditionBuilderTests : BunitContext { + + /// + /// Renders the component with no initial expression and verifies the structured + /// mode is shown by default with the ExitCode type selected. + /// + [TestMethod] + public void Renders_StructuredMode_ByDefault( ) { + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.Expression, null ) + .Add( p => p.ExpressionChanged, EventCallback.Factory.Create( this, _ => { } ) ) ); + + // Should not show the raw input + Assert.ThrowsExactly( ( ) => cut.Find( "input[type=text]" ) ); + + // Should show the structured dropdowns — Type select should exist + IReadOnlyList selects = cut.FindAll( "select" ); + Assert.IsGreaterThanOrEqualTo( 1, selects.Count, "Should render at least the Type dropdown." ); + } + + /// + /// Applies a structured exit code expression and verifies the expression callback fires + /// with the correct value. + /// + [TestMethod] + public void Apply_ExitCodeEquals_GeneratesCorrectExpression( ) { + string? captured = null; + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.Expression, null ) + .Add( p => p.ExpressionChanged, EventCallback.Factory.Create( this, v => captured = v ) ) ); + + // The defaults are ExitCode, ==, 0 — just click Apply + AngleSharp.Dom.IElement applyButton = cut.Find( "button.btn-outline-primary" ); + applyButton.Click( ); + + Assert.AreEqual( "$exitCode == 0", captured ); + } + + /// + /// Verifies that changing the operator dropdown changes the generated expression. + /// + [TestMethod] + public void Apply_ExitCodeNotEqual_GeneratesCorrectExpression( ) { + string? captured = null; + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.Expression, null ) + .Add( p => p.ExpressionChanged, EventCallback.Factory.Create( this, v => captured = v ) ) ); + + // Change operator to != + IReadOnlyList selects = cut.FindAll( "select" ); + // Second select is the operator dropdown (first is Type) + AngleSharp.Dom.IElement operatorSelect = selects[1]; + operatorSelect.Change( "!=" ); + + cut.Find( "button.btn-outline-primary" ).Click( ); + + Assert.AreEqual( "$exitCode != 0", captured ); + } + + /// + /// Verifies that selecting SuccessStatus type and applying generates the correct expression. + /// + [TestMethod] + public void Apply_SuccessTrue_GeneratesCorrectExpression( ) { + string? captured = null; + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.Expression, null ) + .Add( p => p.ExpressionChanged, EventCallback.Factory.Create( this, v => captured = v ) ) ); + + // Change type to SuccessStatus + AngleSharp.Dom.IElement typeSelect = cut.FindAll( "select" )[0]; + typeSelect.Change( "SuccessStatus" ); + + // Default success value is "true" — apply + cut.Find( "button.btn-outline-primary" ).Click( ); + + Assert.AreEqual( "$? -eq $true", captured ); + } + + /// + /// Verifies switching to advanced mode reveals the raw text input. + /// + [TestMethod] + public void SwitchToAdvanced_ShowsRawInput( ) { + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.Expression, null ) + .Add( p => p.ExpressionChanged, EventCallback.Factory.Create( this, _ => { } ) ) ); + + // Click "Advanced / Raw" button + AngleSharp.Dom.IElement advancedButton = cut.Find( "button.btn-outline-secondary" ); + advancedButton.Click( ); + + // Now the raw text input should be present + AngleSharp.Dom.IElement rawInput = cut.Find( "input[type=text]" ); + Assert.IsNotNull( rawInput ); + } + + /// + /// Verifies that a valid raw expression is accepted and fires the callback. + /// + [TestMethod] + public void ApplyRaw_ValidExpression_FiresCallback( ) { + string? captured = null; + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.Expression, null ) + .Add( p => p.ExpressionChanged, EventCallback.Factory.Create( this, v => captured = v ) ) ); + + // Switch to advanced mode + cut.Find( "button.btn-outline-secondary" ).Click( ); + + // Type a valid expression + cut.Find( "input[type=text]" ).Input( "$exitCode >= 1" ); + + // Click Apply + IReadOnlyList buttons = cut.FindAll( "button.btn-outline-primary" ); + AngleSharp.Dom.IElement applyButton = buttons[^1]; // last outline-primary is the Apply button + applyButton.Click( ); + + Assert.AreEqual( "$exitCode >= 1", captured ); + } + + /// + /// Verifies that an invalid raw expression shows a validation error and does not fire the callback. + /// + [TestMethod] + public void ApplyRaw_InvalidExpression_ShowsError( ) { + string? captured = null; + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.Expression, null ) + .Add( p => p.ExpressionChanged, EventCallback.Factory.Create( this, v => captured = v ) ) ); + + // Switch to advanced mode + cut.Find( "button.btn-outline-secondary" ).Click( ); + + // Type an invalid expression + cut.Find( "input[type=text]" ).Input( "invalid garbage" ); + + // Click Apply + IReadOnlyList buttons = cut.FindAll( "button.btn-outline-primary" ); + buttons[^1].Click( ); + + // Should show validation error + AngleSharp.Dom.IElement errorDiv = cut.Find( ".text-danger" ); + Assert.IsNotNull( errorDiv ); + Assert.IsFalse( string.IsNullOrWhiteSpace( errorDiv.TextContent ) ); + + // Callback should NOT have been fired (expression stays null) + Assert.IsNull( captured ); + } + + /// + /// Verifies that an existing expression is parsed into the structured UI on initial render. + /// + [TestMethod] + public void ExistingExpression_ParsedIntoStructuredUI( ) { + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.Expression, "$? -eq $false" ) + .Add( p => p.ExpressionChanged, EventCallback.Factory.Create( this, _ => { } ) ) ); + + // Should show the expression badge + AngleSharp.Dom.IElement badge = cut.Find( ".badge" ); + Assert.Contains( "$? -eq $false", badge.TextContent ); + } + + /// + /// Verifies the documentation section with supported patterns is rendered. + /// + [TestMethod] + public void SupportedPatterns_Documentation_IsRendered( ) { + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.Expression, null ) + .Add( p => p.ExpressionChanged, EventCallback.Factory.Create( this, _ => { } ) ) ); + + // Should have a
element with pattern documentation + AngleSharp.Dom.IElement details = cut.Find( "details" ); + Assert.IsNotNull( details ); + Assert.Contains( "$? -eq $true", details.TextContent ); + } +} diff --git a/src/Test/Werkr.Tests.Server/Components/TaskCreateModalTests.cs b/src/Test/Werkr.Tests.Server/Components/TaskCreateModalTests.cs new file mode 100644 index 0000000..1a930f1 --- /dev/null +++ b/src/Test/Werkr.Tests.Server/Components/TaskCreateModalTests.cs @@ -0,0 +1,132 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using Bunit; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; +using Werkr.Common.Models; +using Werkr.Server.Components.Shared; + +namespace Werkr.Tests.Server.Components; + +/// +/// bUnit tests for the component. Validates modal +/// visibility, form rendering, successful submission, and cancellation. +/// +[TestClass] +public class TaskCreateModalTests : BunitContext { + + /// + /// Registers a fake that returns an + /// backed by the given handler. + /// + private void RegisterHttpClient( HttpMessageHandler handler ) { + HttpClient client = new( handler ) { BaseAddress = new Uri( "http://localhost" ) }; + IHttpClientFactory factory = new FakeHttpClientFactory( client ); + Services.AddSingleton( factory ); + } + + /// + /// Verifies that the modal is hidden by default (no modal markup rendered). + /// + [TestMethod] + public void Modal_IsHidden_ByDefault( ) { + RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); + + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.WorkflowId, 42L ) + .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); + + // Modal should not be visible + Assert.ThrowsExactly( ( ) => cut.Find( ".modal" ) ); + } + + /// + /// Verifies that calling Show() makes the modal visible with the expected form fields. + /// + [TestMethod] + public void Show_RendersModalWithFormFields( ) { + RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); + + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.WorkflowId, 42L ) + .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); + + cut.InvokeAsync( ( ) => cut.Instance.Show( ) ); + + // Modal should be visible + AngleSharp.Dom.IElement modal = cut.Find( ".modal" ); + Assert.IsNotNull( modal ); + + // Should have a Name input + Assert.Contains( "Name", cut.Markup, "Should render Name field." ); + + // Should have Action Type select + Assert.Contains( "Action Type", cut.Markup, "Should render Action Type field." ); + + // Should have Create Task submit button + Assert.Contains( "Create Task", cut.Markup, "Should render submit button." ); + + // Should have Cancel button + Assert.Contains( "Cancel", cut.Markup, "Should render cancel button." ); + } + + /// + /// Verifies that clicking Cancel closes the modal. + /// + [TestMethod] + public void Cancel_ClosesModal( ) { + RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); + + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.WorkflowId, 42L ) + .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); + + cut.InvokeAsync( ( ) => cut.Instance.Show( ) ); + + // Click Cancel + AngleSharp.Dom.IElement cancelButton = cut.Find( "button.btn-outline-secondary" ); + cancelButton.Click( ); + + // Modal should be hidden again + Assert.ThrowsExactly( ( ) => cut.Find( ".modal" ) ); + } + + /// + /// Verifies that the Action Type dropdown contains the expected options. + /// + [TestMethod] + public void ActionTypeDropdown_HasExpectedOptions( ) { + RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); + + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.WorkflowId, 42L ) + .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); + + cut.InvokeAsync( ( ) => cut.Instance.Show( ) ); + + IReadOnlyList options = cut.FindAll( ".form-select option" ); + string[] optionValues = [.. options.Select( o => o.GetAttribute( "value" ) ?? "" )]; + + Assert.Contains( "PowerShellCommand", optionValues, "Should have PowerShellCommand." ); + Assert.Contains( "ShellCommand", optionValues, "Should have ShellCommand." ); + Assert.Contains( "ShellScript", optionValues, "Should have ShellScript." ); + Assert.Contains( "Action", optionValues, "Should have Action." ); + } + + // ── Helpers ── + + /// Minimal fake that always returns the given response. + private sealed class StubHandler( HttpStatusCode statusCode, string content ) : HttpMessageHandler { + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken ) => + Task.FromResult( new HttpResponseMessage( statusCode ) { + Content = new StringContent( content, System.Text.Encoding.UTF8, "application/json" ), + } ); + } + + /// Fake that always returns the same client. + private sealed class FakeHttpClientFactory( HttpClient client ) : IHttpClientFactory { + public HttpClient CreateClient( string name ) => client; + } +} diff --git a/src/Test/Werkr.Tests/Integration/WorkflowIntegrationTests.cs b/src/Test/Werkr.Tests/Integration/WorkflowIntegrationTests.cs index a699268..2d7ec6f 100644 --- a/src/Test/Werkr.Tests/Integration/WorkflowIntegrationTests.cs +++ b/src/Test/Werkr.Tests/Integration/WorkflowIntegrationTests.cs @@ -172,7 +172,7 @@ public async Task WorkflowSteps_AddAndLinkWithDependency( ) { var step1Request = new { taskId = task1Id, order = 1, - controlStatement = "Sequential", + controlStatement = "Default", dependencyMode = "All" }; @@ -188,7 +188,7 @@ public async Task WorkflowSteps_AddAndLinkWithDependency( ) { var step2Request = new { taskId = task2Id, order = 2, - controlStatement = "Sequential", + controlStatement = "Default", dependencyMode = "All" }; @@ -290,6 +290,152 @@ public async Task TaskCrud_CreateWithTagsUpdateAndDelete( ) { #endregion Task CRUD with Tags + #region Workflow TargetTags + + /// + /// Verifies that a workflow can be created with TargetTags, updated to change its tags, + /// and that the tags persist correctly through the full CRUD cycle. + /// + [TestMethod] + [Timeout( 60_000, CooperativeCancellation = true )] + public async Task WorkflowTargetTags_CreateUpdateReadDelete( ) { + CancellationToken ct = TestContext.CancellationToken; + + // Create workflow with tags + var createRequest = new { + name = "IntTest_WF_Tags", + description = "Workflow with target tags", + enabled = true, + targetTags = new[] { "linux", "docker" }, + }; + + HttpResponseMessage createResponse = await Api.PostAsJsonAsync( + "/api/workflows", createRequest, JsonOptions, ct ); + Assert.AreEqual( HttpStatusCode.Created, createResponse.StatusCode, + $"Workflow creation failed: {await createResponse.Content.ReadAsStringAsync( ct )}" ); + + JsonElement created = await createResponse.Content.ReadFromJsonAsync( JsonOptions, ct ); + long workflowId = created.GetProperty( "id" ).GetInt64( ); + + // Verify tags returned on create response + JsonElement createdTags = created.GetProperty( "targetTags" ); + Assert.AreEqual( 2, createdTags.GetArrayLength( ), "Should return 2 target tags." ); + + // Read and verify tags persisted + HttpResponseMessage getResponse = await Api.GetAsync( $"/api/workflows/{workflowId}", ct ); + Assert.AreEqual( HttpStatusCode.OK, getResponse.StatusCode ); + + JsonElement retrieved = await getResponse.Content.ReadFromJsonAsync( JsonOptions, ct ); + JsonElement tags = retrieved.GetProperty( "targetTags" ); + Assert.AreEqual( 2, tags.GetArrayLength( ), "Read-back should return 2 target tags." ); + + HashSet tagValues = [.. Enumerable.Range( 0, tags.GetArrayLength( ) ).Select( i => tags[i].GetString( )! )]; + Assert.Contains( "linux", tagValues, "Should contain 'linux' tag." ); + Assert.Contains( "docker", tagValues, "Should contain 'docker' tag." ); + + // Update to a single tag + var updateRequest = new { + name = "IntTest_WF_Tags", + description = "Updated", + enabled = true, + targetTags = new[] { "windows" }, + }; + + HttpResponseMessage putResponse = await Api.PutAsJsonAsync( + $"/api/workflows/{workflowId}", updateRequest, JsonOptions, ct ); + Assert.AreEqual( HttpStatusCode.OK, putResponse.StatusCode ); + + JsonElement updated = await putResponse.Content.ReadFromJsonAsync( JsonOptions, ct ); + JsonElement updatedTags = updated.GetProperty( "targetTags" ); + Assert.AreEqual( 1, updatedTags.GetArrayLength( ), "Updated workflow should have 1 tag." ); + Assert.AreEqual( "windows", updatedTags[0].GetString( ) ); + + // Update to null/empty tags + var clearRequest = new { + name = "IntTest_WF_Tags", + description = "Cleared tags", + enabled = true, + targetTags = Array.Empty( ), + }; + + HttpResponseMessage clearResponse = await Api.PutAsJsonAsync( + $"/api/workflows/{workflowId}", clearRequest, JsonOptions, ct ); + Assert.AreEqual( HttpStatusCode.OK, clearResponse.StatusCode ); + + JsonElement cleared = await clearResponse.Content.ReadFromJsonAsync( JsonOptions, ct ); + JsonElement clearedTags = cleared.GetProperty( "targetTags" ); + Assert.AreEqual( 0, clearedTags.GetArrayLength( ), "Cleared workflow should have 0 tags." ); + + // Cleanup + _ = await Api.DeleteAsync( $"/api/workflows/{workflowId}", ct ); + } + + /// + /// Verifies that a workflow can be associated with a schedule and disassociated. + /// + [TestMethod] + [Timeout( 60_000, CooperativeCancellation = true )] + public async Task WorkflowScheduleAssociation_AssociateAndDisassociate( ) { + CancellationToken ct = TestContext.CancellationToken; + + // Create a workflow + var wfRequest = new { + name = "IntTest_WF_Sched", + description = "Workflow for schedule test", + enabled = true, + }; + + HttpResponseMessage wfResponse = await Api.PostAsJsonAsync( + "/api/workflows", wfRequest, JsonOptions, ct ); + Assert.AreEqual( HttpStatusCode.Created, wfResponse.StatusCode ); + JsonElement wf = await wfResponse.Content.ReadFromJsonAsync( JsonOptions, ct ); + long workflowId = wf.GetProperty( "id" ).GetInt64( ); + + // Create a schedule + var schedRequest = new { + name = "IntTest_Schedule_Assoc", + stopTaskAfterMinutes = 30L, + startDateTime = new { date = "2026-06-15", time = "09:00:00", timeZoneId = "UTC" }, + }; + + HttpResponseMessage schedResponse = await Api.PostAsJsonAsync( + "/api/schedules", schedRequest, JsonOptions, ct ); + Assert.AreEqual( HttpStatusCode.Created, schedResponse.StatusCode ); + JsonElement sched = await schedResponse.Content.ReadFromJsonAsync( JsonOptions, ct ); + string scheduleId = sched.GetProperty( "id" ).GetString( )!; + + // Associate schedule with workflow + var assocRequest = new { scheduleId }; + HttpResponseMessage assocResponse = await Api.PostAsJsonAsync( + $"/api/workflows/{workflowId}/schedules", assocRequest, JsonOptions, ct ); + Assert.AreEqual( HttpStatusCode.Created, assocResponse.StatusCode, + $"Association failed: {await assocResponse.Content.ReadAsStringAsync( ct )}" ); + + // Verify schedule is listed + HttpResponseMessage listResponse = await Api.GetAsync( $"/api/workflows/{workflowId}/schedules", ct ); + Assert.AreEqual( HttpStatusCode.OK, listResponse.StatusCode ); + JsonElement schedules = await listResponse.Content.ReadFromJsonAsync( JsonOptions, ct ); + Assert.IsGreaterThanOrEqualTo( 1, schedules.GetArrayLength( ), + "Should have at least 1 associated schedule." ); + + // Disassociate + HttpResponseMessage disassocResponse = await Api.DeleteAsync( + $"/api/workflows/{workflowId}/schedules/{scheduleId}", ct ); + Assert.AreEqual( HttpStatusCode.NoContent, disassocResponse.StatusCode ); + + // Verify empty + HttpResponseMessage emptyResponse = await Api.GetAsync( $"/api/workflows/{workflowId}/schedules", ct ); + Assert.AreEqual( HttpStatusCode.OK, emptyResponse.StatusCode ); + JsonElement emptyList = await emptyResponse.Content.ReadFromJsonAsync( JsonOptions, ct ); + Assert.AreEqual( 0, emptyList.GetArrayLength( ), "Should have 0 associated schedules." ); + + // Cleanup + _ = await Api.DeleteAsync( $"/api/workflows/{workflowId}", ct ); + _ = await Api.DeleteAsync( $"/api/schedules/{scheduleId}", ct ); + } + + #endregion Workflow TargetTags + #region Job Query Endpoints /// diff --git a/src/Werkr.Agent/Scheduling/WorkflowExecutionService.cs b/src/Werkr.Agent/Scheduling/WorkflowExecutionService.cs index 30df07b..c0ae0ea 100644 --- a/src/Werkr.Agent/Scheduling/WorkflowExecutionService.cs +++ b/src/Werkr.Agent/Scheduling/WorkflowExecutionService.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Text.Json; using Werkr.Agent.Communication; using Werkr.Agent.Operators; @@ -110,7 +111,7 @@ CancellationToken ct } // Seed variable cache from workflow definition defaults and trigger variables - Dictionary variableCache = new(StringComparer.OrdinalIgnoreCase); + ConcurrentDictionary variableCache = new(StringComparer.OrdinalIgnoreCase); foreach (WorkflowVariableDef varDef in workflow.Variables) { if (!string.IsNullOrWhiteSpace( varDef.DefaultValue )) { variableCache[varDef.Name] = varDef.DefaultValue; @@ -124,8 +125,8 @@ CancellationToken ct IReadOnlyList> levels = BuildTopologicalLevels( workflow.Steps ); - // Step result map: stepId → completed job result - Dictionary stepResults = []; + // Step result map: stepId → completed job result (concurrent for parallel steps) + ConcurrentDictionary stepResults = new(); // If/Else/ElseIf chain tracking: stepId → whether that branch was taken Dictionary branchTaken = []; @@ -149,24 +150,23 @@ CancellationToken ct } } - // Execute parallelizable steps (sequentially — no DB context concerns on agent - // but keeps behavior consistent with the original executor) - foreach (ScheduledWorkflowStepDef step in parallelizable) { - ct.ThrowIfCancellationRequested( ); - - StepExecutionResult result = await ExecuteStepAsync( - step, workflowRunId, stepResults, branchTaken, variableCache, scheduleId, ct); - - if (result.Job is not null) { - stepResults[result.StepId] = result.Job; - } - - if (result.Failed) { - logger.LogWarning( - "Workflow run {RunId} failed at step {StepId}: {Error}.", - workflowRunId, result.StepId, result.ErrorMessage ); - workflowFailed = true; - break; + // Execute parallelizable steps concurrently via Task.WhenAll + if (parallelizable.Count > 0) { + StepExecutionResult[] parallelResults = await Task.WhenAll( + parallelizable.Select( step => ExecuteStepAsync( + step, workflowRunId, stepResults, branchTaken, variableCache, scheduleId, ct ) ) ); + + foreach (StepExecutionResult result in parallelResults) { + if (result.Job is not null) { + stepResults[result.StepId] = result.Job; + } + + if (result.Failed) { + logger.LogWarning( + "Workflow run {RunId} failed at step {StepId}: {Error}.", + workflowRunId, result.StepId, result.ErrorMessage ); + workflowFailed = true; + } } } @@ -217,9 +217,9 @@ CancellationToken ct private async Task ExecuteStepAsync( ScheduledWorkflowStepDef step, Guid workflowRunId, - Dictionary stepResults, + ConcurrentDictionary stepResults, Dictionary branchTaken, - Dictionary variableCache, + ConcurrentDictionary variableCache, Guid? scheduleId, CancellationToken ct ) { @@ -298,7 +298,7 @@ private async Task ExecuteLoopStepAsync( ScheduledTaskDefinition taskDef, Guid workflowRunId, List predecessorJobs, - Dictionary variableCache, + ConcurrentDictionary variableCache, Guid? scheduleId, CancellationToken ct ) { @@ -611,7 +611,7 @@ IReadOnlyCollection steps /// Checks whether dependencies are satisfied based on DependencyMode. private static bool CheckDependencies( ScheduledWorkflowStepDef step, - Dictionary stepResults + ConcurrentDictionary stepResults ) { if (step.DependsOnStepIds.Count == 0) { return true; // Root step — no dependencies @@ -637,7 +637,7 @@ Dictionary branchTaken ControlStatement cs = (ControlStatement) step.ControlStatement; switch (cs) { - case ControlStatement.Sequential: + case ControlStatement.Default: return true; case ControlStatement.If: @@ -682,7 +682,7 @@ Dictionary branchTaken /// private static List BuildPredecessorJobs( ScheduledWorkflowStepDef step, - Dictionary stepResults + ConcurrentDictionary stepResults ) { List jobs = []; foreach (long depId in step.DependsOnStepIds) { diff --git a/src/Werkr.Api/Endpoints/WorkflowEndpoints.cs b/src/Werkr.Api/Endpoints/WorkflowEndpoints.cs index aecb2ee..3a08260 100644 --- a/src/Werkr.Api/Endpoints/WorkflowEndpoints.cs +++ b/src/Werkr.Api/Endpoints/WorkflowEndpoints.cs @@ -9,6 +9,7 @@ using Werkr.Core.Scheduling; using Werkr.Core.Workflows; using Werkr.Data; +using Werkr.Data.Calendar.Models; using Werkr.Data.Entities.Workflows; namespace Werkr.Api.Endpoints; @@ -23,6 +24,7 @@ public static WebApplication MapWorkflowEndpoints( this WebApplication app ) { MapWorkflowCrud( app ); MapWorkflowSteps( app ); MapStepDependencies( app ); + MapWorkflowSchedules( app ); MapWorkflowExecution( app ); return app; } @@ -253,6 +255,91 @@ CancellationToken ct .RequireAuthorization( Policies.CanDelete ); } + // ── Workflow Schedules ── + + /// + /// Registers endpoints for associating schedules with a workflow. + /// + private static void MapWorkflowSchedules( WebApplication app ) { + + _ = app.MapGet( "/api/workflows/{workflowId}/schedules", async ( + long workflowId, + WerkrDbContext dbContext, + ScheduleService scheduleService, + CancellationToken ct + ) => { + List links = await dbContext.WorkflowSchedules.AsNoTracking( ) + .Where( ws => ws.WorkflowId == workflowId ) + .ToListAsync( ct ); + + List dtos = []; + foreach (WorkflowSchedule ws in links) { + Schedule? schedule = await scheduleService.GetByIdAsync( ws.ScheduleId, ct ); + if (schedule is not null) { + dtos.Add( ScheduleMapper.ToDto( schedule ) ); + } + } + return Results.Ok( dtos ); + } ) + .WithName( "GetWorkflowSchedules" ) + .RequireAuthorization( Policies.CanRead ); + + _ = app.MapPost( "/api/workflows/{workflowId}/schedules", async ( + long workflowId, + WorkflowScheduleAssociateRequest request, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + bool workflowExists = await dbContext.Workflows.AnyAsync( + w => w.Id == workflowId, ct ); + if (!workflowExists) { + return Results.NotFound( new { message = "Workflow not found." } ); + } + + bool scheduleExists = await dbContext.Schedules.AnyAsync( + s => s.Id == request.ScheduleId, ct ); + if (!scheduleExists) { + return Results.NotFound( new { message = "Schedule not found." } ); + } + + bool alreadyLinked = await dbContext.WorkflowSchedules.AnyAsync( + ws => ws.WorkflowId == workflowId && ws.ScheduleId == request.ScheduleId, ct ); + if (alreadyLinked) { + return Results.Conflict( new { message = "Schedule is already associated with this workflow." } ); + } + + WorkflowSchedule link = new( ) { + WorkflowId = workflowId, + ScheduleId = request.ScheduleId, + }; + _ = dbContext.WorkflowSchedules.Add( link ); + _ = await dbContext.SaveChangesAsync( ct ); + return Results.Created( $"/api/workflows/{workflowId}/schedules", null ); + } ) + .WithName( "AssociateWorkflowSchedule" ) + .RequireAuthorization( Policies.CanCreate ); + + _ = app.MapDelete( "/api/workflows/{workflowId}/schedules/{scheduleId}", async ( + long workflowId, + Guid scheduleId, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + WorkflowSchedule? link = await dbContext.WorkflowSchedules + .FirstOrDefaultAsync( + ws => ws.WorkflowId == workflowId && ws.ScheduleId == scheduleId, ct ); + if (link is null) { + return Results.NotFound( ); + } + + _ = dbContext.WorkflowSchedules.Remove( link ); + _ = await dbContext.SaveChangesAsync( ct ); + return Results.NoContent( ); + } ) + .WithName( "DisassociateWorkflowSchedule" ) + .RequireAuthorization( Policies.CanDelete ); + } + // ── Workflow Execution & Runs ── /// diff --git a/src/Werkr.Api/Models/WorkflowMapper.cs b/src/Werkr.Api/Models/WorkflowMapper.cs index a4a6167..d1696a6 100644 --- a/src/Werkr.Api/Models/WorkflowMapper.cs +++ b/src/Werkr.Api/Models/WorkflowMapper.cs @@ -14,6 +14,7 @@ public static Workflow ToEntity( WorkflowCreateRequest request ) => Name = request.Name, Description = request.Description ?? string.Empty, Enabled = request.Enabled, + TargetTags = request.TargetTags, }; /// Maps a to a entity with a given ID. @@ -23,6 +24,7 @@ public static Workflow ToEntity( long id, WorkflowUpdateRequest request ) => Name = request.Name, Description = request.Description ?? string.Empty, Enabled = request.Enabled, + TargetTags = request.TargetTags, }; /// Maps a entity to a . @@ -32,7 +34,8 @@ public static WorkflowDto ToDto( Workflow workflow ) => Name: workflow.Name, Description: workflow.Description, Enabled: workflow.Enabled, - Steps: [.. workflow.Steps.Select( ToStepDto )] ); + Steps: [.. workflow.Steps.Select( ToStepDto )], + TargetTags: workflow.TargetTags ); /// Maps a entity to a . public static WorkflowStepDto ToStepDto( WorkflowStep step ) => diff --git a/src/Werkr.Api/Services/ScheduleSyncGrpcService.cs b/src/Werkr.Api/Services/ScheduleSyncGrpcService.cs index 0f4d979..dfc3779 100644 --- a/src/Werkr.Api/Services/ScheduleSyncGrpcService.cs +++ b/src/Werkr.Api/Services/ScheduleSyncGrpcService.cs @@ -103,10 +103,15 @@ ServerCallContext context foreach (WorkflowSchedule ws in workflowSchedules) { Workflow workflow = ws.Workflow!; - // Check if any task in the workflow matches the agent's tags - bool anyMatch = workflow.Steps.Any( step => - step.Task is not null && - step.Task.TargetTags.Any( tag => agentTags.Contains( tag.Trim( ) ) ) ); + // If workflow has TargetTags, use those for agent matching; otherwise fall back to per-task tags + bool anyMatch; + if (workflow.TargetTags is { Length: > 0 }) { + anyMatch = workflow.TargetTags.Any( tag => agentTags.Contains( tag.Trim( ) ) ); + } else { + anyMatch = workflow.Steps.Any( step => + step.Task is not null && + step.Task.TargetTags.Any( tag => agentTags.Contains( tag.Trim( ) ) ) ); + } if (!anyMatch) { continue; } diff --git a/src/Werkr.Common/Models/WorkflowCreateRequest.cs b/src/Werkr.Common/Models/WorkflowCreateRequest.cs index 0700e93..01743d5 100644 --- a/src/Werkr.Common/Models/WorkflowCreateRequest.cs +++ b/src/Werkr.Common/Models/WorkflowCreateRequest.cs @@ -4,5 +4,6 @@ namespace Werkr.Common.Models; public sealed record WorkflowCreateRequest( string Name, string? Description = null, - bool Enabled = true + bool Enabled = true, + string[]? TargetTags = null ); diff --git a/src/Werkr.Common/Models/WorkflowDto.cs b/src/Werkr.Common/Models/WorkflowDto.cs index aa86238..94dbee3 100644 --- a/src/Werkr.Common/Models/WorkflowDto.cs +++ b/src/Werkr.Common/Models/WorkflowDto.cs @@ -6,5 +6,6 @@ public sealed record WorkflowDto( string Name, string Description, bool Enabled, - IReadOnlyList Steps + IReadOnlyList Steps, + string[]? TargetTags = null ); diff --git a/src/Werkr.Common/Models/WorkflowScheduleAssociateRequest.cs b/src/Werkr.Common/Models/WorkflowScheduleAssociateRequest.cs new file mode 100644 index 0000000..361f79a --- /dev/null +++ b/src/Werkr.Common/Models/WorkflowScheduleAssociateRequest.cs @@ -0,0 +1,6 @@ +namespace Werkr.Common.Models; + +/// Request DTO for associating a schedule with a workflow. +public sealed record WorkflowScheduleAssociateRequest( + Guid ScheduleId +); diff --git a/src/Werkr.Common/Models/WorkflowStepCreateRequest.cs b/src/Werkr.Common/Models/WorkflowStepCreateRequest.cs index 5714aa1..e9454e0 100644 --- a/src/Werkr.Common/Models/WorkflowStepCreateRequest.cs +++ b/src/Werkr.Common/Models/WorkflowStepCreateRequest.cs @@ -4,7 +4,7 @@ namespace Werkr.Common.Models; public sealed record WorkflowStepCreateRequest( long TaskId, int Order = 0, - string ControlStatement = "Sequential", + string ControlStatement = "Default", string? ConditionExpression = null, int MaxIterations = 100, Guid? AgentConnectionIdOverride = null, diff --git a/src/Werkr.Common/Models/WorkflowStepUpdateRequest.cs b/src/Werkr.Common/Models/WorkflowStepUpdateRequest.cs index 67a7ad9..1636305 100644 --- a/src/Werkr.Common/Models/WorkflowStepUpdateRequest.cs +++ b/src/Werkr.Common/Models/WorkflowStepUpdateRequest.cs @@ -3,7 +3,7 @@ namespace Werkr.Common.Models; /// Request DTO for updating a workflow step. public sealed record WorkflowStepUpdateRequest( int Order = 0, - string ControlStatement = "Sequential", + string ControlStatement = "Default", string? ConditionExpression = null, int MaxIterations = 100, Guid? AgentConnectionIdOverride = null, diff --git a/src/Werkr.Common/Models/WorkflowUpdateRequest.cs b/src/Werkr.Common/Models/WorkflowUpdateRequest.cs index a90ce59..ec87f76 100644 --- a/src/Werkr.Common/Models/WorkflowUpdateRequest.cs +++ b/src/Werkr.Common/Models/WorkflowUpdateRequest.cs @@ -4,5 +4,6 @@ namespace Werkr.Common.Models; public sealed record WorkflowUpdateRequest( string Name, string? Description = null, - bool Enabled = true + bool Enabled = true, + string[]? TargetTags = null ); diff --git a/src/Werkr.Core/Workflows/WorkflowService.cs b/src/Werkr.Core/Workflows/WorkflowService.cs index a83866b..1bfc795 100644 --- a/src/Werkr.Core/Workflows/WorkflowService.cs +++ b/src/Werkr.Core/Workflows/WorkflowService.cs @@ -75,6 +75,7 @@ public async Task UpdateAsync( existing.Name = workflow.Name; existing.Description = workflow.Description; existing.Enabled = workflow.Enabled; + existing.TargetTags = workflow.TargetTags; _ = await dbContext.SaveChangesAsync( ct ); diff --git a/src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.cs b/src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.cs deleted file mode 100644 index 27be5aa..0000000 --- a/src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.cs +++ /dev/null @@ -1,341 +0,0 @@ -#nullable disable - -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Werkr.Data.Identity.Migrations.Postgres; - -/// -public partial class InitialCreate : Migration { - /// - protected override void Up( MigrationBuilder migrationBuilder ) { - _ = migrationBuilder.EnsureSchema( - name: "werkr_identity" ); - - _ = migrationBuilder.CreateTable( - name: "config_settings", - schema: "werkr_identity", - columns: table => new { - id = table.Column( type: "uuid", nullable: false ), - default_key_size = table.Column( type: "integer", nullable: false ), - server_name = table.Column( type: "character varying(200)", maxLength: 200, nullable: false ), - allow_registration = table.Column( type: "boolean", nullable: false ), - polling_interval_seconds = table.Column( type: "integer", nullable: false ), - run_detail_polling_interval_seconds = table.Column( type: "integer", nullable: false ), - created = table.Column( type: "timestamp with time zone", nullable: false ), - last_updated = table.Column( type: "timestamp with time zone", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_config_settings", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "roles", - schema: "werkr_identity", - columns: table => new { - id = table.Column( type: "text", nullable: false ), - name = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), - normalized_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), - concurrency_stamp = table.Column( type: "text", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_roles", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "users", - schema: "werkr_identity", - columns: table => new { - id = table.Column( type: "text", nullable: false ), - name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), - enabled = table.Column( type: "boolean", nullable: false ), - change_password = table.Column( type: "boolean", nullable: false ), - requires2fa = table.Column( type: "boolean", nullable: false ), - last_login_utc = table.Column( type: "timestamp with time zone", nullable: true ), - user_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), - normalized_user_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), - email = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), - normalized_email = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), - email_confirmed = table.Column( type: "boolean", nullable: false ), - password_hash = table.Column( type: "text", nullable: true ), - security_stamp = table.Column( type: "text", nullable: true ), - concurrency_stamp = table.Column( type: "text", nullable: true ), - phone_number = table.Column( type: "text", nullable: true ), - phone_number_confirmed = table.Column( type: "boolean", nullable: false ), - two_factor_enabled = table.Column( type: "boolean", nullable: false ), - lockout_end = table.Column( type: "timestamp with time zone", nullable: true ), - lockout_enabled = table.Column( type: "boolean", nullable: false ), - access_failed_count = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_users", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "role_claims", - schema: "werkr_identity", - columns: table => new { - id = table.Column( type: "integer", nullable: false ) - .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), - role_id = table.Column( type: "text", nullable: false ), - claim_type = table.Column( type: "text", nullable: true ), - claim_value = table.Column( type: "text", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_role_claims", x => x.id ); - _ = table.ForeignKey( - name: "fk_role_claims_roles_role_id", - column: x => x.role_id, - principalSchema: "werkr_identity", - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "role_permissions", - schema: "werkr_identity", - columns: table => new { - id = table.Column( type: "bigint", nullable: false ) - .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), - role_id = table.Column( type: "text", nullable: false ), - permission = table.Column( type: "character varying(64)", maxLength: 64, nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_role_permissions", x => x.id ); - _ = table.ForeignKey( - name: "fk_role_permissions_roles_role_id", - column: x => x.role_id, - principalSchema: "werkr_identity", - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "api_keys", - schema: "werkr_identity", - columns: table => new { - id = table.Column( type: "uuid", nullable: false ), - key_hash = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), - key_prefix = table.Column( type: "character varying(16)", maxLength: 16, nullable: false ), - name = table.Column( type: "character varying(200)", maxLength: 200, nullable: false ), - role = table.Column( type: "character varying(64)", maxLength: 64, nullable: false ), - created_by_user_id = table.Column( type: "text", nullable: false ), - created_utc = table.Column( type: "timestamp with time zone", nullable: false ), - expires_utc = table.Column( type: "timestamp with time zone", nullable: true ), - is_revoked = table.Column( type: "boolean", nullable: false ), - last_used_utc = table.Column( type: "timestamp with time zone", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_api_keys", x => x.id ); - _ = table.ForeignKey( - name: "fk_api_keys_users_created_by_user_id", - column: x => x.created_by_user_id, - principalSchema: "werkr_identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "user_claims", - schema: "werkr_identity", - columns: table => new { - id = table.Column( type: "integer", nullable: false ) - .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), - user_id = table.Column( type: "text", nullable: false ), - claim_type = table.Column( type: "text", nullable: true ), - claim_value = table.Column( type: "text", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_user_claims", x => x.id ); - _ = table.ForeignKey( - name: "fk_user_claims_users_user_id", - column: x => x.user_id, - principalSchema: "werkr_identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "user_logins", - schema: "werkr_identity", - columns: table => new { - login_provider = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), - provider_key = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), - provider_display_name = table.Column( type: "text", nullable: true ), - user_id = table.Column( type: "text", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_user_logins", x => new { x.login_provider, x.provider_key } ); - _ = table.ForeignKey( - name: "fk_user_logins_users_user_id", - column: x => x.user_id, - principalSchema: "werkr_identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "user_roles", - schema: "werkr_identity", - columns: table => new { - user_id = table.Column( type: "text", nullable: false ), - role_id = table.Column( type: "text", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_user_roles", x => new { x.user_id, x.role_id } ); - _ = table.ForeignKey( - name: "fk_user_roles_roles_role_id", - column: x => x.role_id, - principalSchema: "werkr_identity", - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_user_roles_users_user_id", - column: x => x.user_id, - principalSchema: "werkr_identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "user_tokens", - schema: "werkr_identity", - columns: table => new { - user_id = table.Column( type: "text", nullable: false ), - login_provider = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), - name = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), - value = table.Column( type: "text", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_user_tokens", x => new { x.user_id, x.login_provider, x.name } ); - _ = table.ForeignKey( - name: "fk_user_tokens_users_user_id", - column: x => x.user_id, - principalSchema: "werkr_identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateIndex( - name: "ix_api_keys_created_by_user_id", - schema: "werkr_identity", - table: "api_keys", - column: "created_by_user_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_api_keys_key_hash", - schema: "werkr_identity", - table: "api_keys", - column: "key_hash", - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_api_keys_key_prefix", - schema: "werkr_identity", - table: "api_keys", - column: "key_prefix" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_role_claims_role_id", - schema: "werkr_identity", - table: "role_claims", - column: "role_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_role_permissions_role_id_permission", - schema: "werkr_identity", - table: "role_permissions", - columns: ["role_id", "permission"], - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "RoleNameIndex", - schema: "werkr_identity", - table: "roles", - column: "normalized_name", - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_user_claims_user_id", - schema: "werkr_identity", - table: "user_claims", - column: "user_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_user_logins_user_id", - schema: "werkr_identity", - table: "user_logins", - column: "user_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_user_roles_role_id", - schema: "werkr_identity", - table: "user_roles", - column: "role_id" ); - - _ = migrationBuilder.CreateIndex( - name: "EmailIndex", - schema: "werkr_identity", - table: "users", - column: "normalized_email" ); - - _ = migrationBuilder.CreateIndex( - name: "UserNameIndex", - schema: "werkr_identity", - table: "users", - column: "normalized_user_name", - unique: true ); - } - - /// - protected override void Down( MigrationBuilder migrationBuilder ) { - _ = migrationBuilder.DropTable( - name: "api_keys", - schema: "werkr_identity" ); - - _ = migrationBuilder.DropTable( - name: "config_settings", - schema: "werkr_identity" ); - - _ = migrationBuilder.DropTable( - name: "role_claims", - schema: "werkr_identity" ); - - _ = migrationBuilder.DropTable( - name: "role_permissions", - schema: "werkr_identity" ); - - _ = migrationBuilder.DropTable( - name: "user_claims", - schema: "werkr_identity" ); - - _ = migrationBuilder.DropTable( - name: "user_logins", - schema: "werkr_identity" ); - - _ = migrationBuilder.DropTable( - name: "user_roles", - schema: "werkr_identity" ); - - _ = migrationBuilder.DropTable( - name: "user_tokens", - schema: "werkr_identity" ); - - _ = migrationBuilder.DropTable( - name: "roles", - schema: "werkr_identity" ); - - _ = migrationBuilder.DropTable( - name: "users", - schema: "werkr_identity" ); - } -} diff --git a/src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.Designer.cs b/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.Designer.cs similarity index 99% rename from src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.Designer.cs rename to src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.Designer.cs index feb3534..1e6edb2 100644 --- a/src/Werkr.Data.Identity/Migrations/Postgres/20260310023641_InitialCreate.Designer.cs +++ b/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.Designer.cs @@ -12,7 +12,7 @@ namespace Werkr.Data.Identity.Migrations.Postgres { [DbContext(typeof(PostgresWerkrIdentityDbContext))] - [Migration("20260310023641_InitialCreate")] + [Migration("20260312050754_InitialCreate")] partial class InitialCreate { /// @@ -21,7 +21,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("werkr_identity") - .HasAnnotation("ProductVersion", "10.0.3") + .HasAnnotation("ProductVersion", "10.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); diff --git a/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.cs b/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.cs new file mode 100644 index 0000000..4000d48 --- /dev/null +++ b/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.cs @@ -0,0 +1,366 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Werkr.Data.Identity.Migrations.Postgres +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "werkr_identity"); + + migrationBuilder.CreateTable( + name: "config_settings", + schema: "werkr_identity", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + default_key_size = table.Column(type: "integer", nullable: false), + server_name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + allow_registration = table.Column(type: "boolean", nullable: false), + polling_interval_seconds = table.Column(type: "integer", nullable: false), + run_detail_polling_interval_seconds = table.Column(type: "integer", nullable: false), + created = table.Column(type: "timestamp with time zone", nullable: false), + last_updated = table.Column(type: "timestamp with time zone", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_config_settings", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "roles", + schema: "werkr_identity", + columns: table => new + { + id = table.Column(type: "text", nullable: false), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + normalized_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + concurrency_stamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_roles", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "users", + schema: "werkr_identity", + columns: table => new + { + id = table.Column(type: "text", nullable: false), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + enabled = table.Column(type: "boolean", nullable: false), + change_password = table.Column(type: "boolean", nullable: false), + requires2fa = table.Column(type: "boolean", nullable: false), + last_login_utc = table.Column(type: "timestamp with time zone", nullable: true), + user_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + normalized_user_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + normalized_email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + email_confirmed = table.Column(type: "boolean", nullable: false), + password_hash = table.Column(type: "text", nullable: true), + security_stamp = table.Column(type: "text", nullable: true), + concurrency_stamp = table.Column(type: "text", nullable: true), + phone_number = table.Column(type: "text", nullable: true), + phone_number_confirmed = table.Column(type: "boolean", nullable: false), + two_factor_enabled = table.Column(type: "boolean", nullable: false), + lockout_end = table.Column(type: "timestamp with time zone", nullable: true), + lockout_enabled = table.Column(type: "boolean", nullable: false), + access_failed_count = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "role_claims", + schema: "werkr_identity", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + role_id = table.Column(type: "text", nullable: false), + claim_type = table.Column(type: "text", nullable: true), + claim_value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_role_claims", x => x.id); + table.ForeignKey( + name: "fk_role_claims_roles_role_id", + column: x => x.role_id, + principalSchema: "werkr_identity", + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "role_permissions", + schema: "werkr_identity", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + role_id = table.Column(type: "text", nullable: false), + permission = table.Column(type: "character varying(64)", maxLength: 64, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_role_permissions", x => x.id); + table.ForeignKey( + name: "fk_role_permissions_roles_role_id", + column: x => x.role_id, + principalSchema: "werkr_identity", + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "api_keys", + schema: "werkr_identity", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + key_hash = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + key_prefix = table.Column(type: "character varying(16)", maxLength: 16, nullable: false), + name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + role = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + created_by_user_id = table.Column(type: "text", nullable: false), + created_utc = table.Column(type: "timestamp with time zone", nullable: false), + expires_utc = table.Column(type: "timestamp with time zone", nullable: true), + is_revoked = table.Column(type: "boolean", nullable: false), + last_used_utc = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_api_keys", x => x.id); + table.ForeignKey( + name: "fk_api_keys_users_created_by_user_id", + column: x => x.created_by_user_id, + principalSchema: "werkr_identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_claims", + schema: "werkr_identity", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + user_id = table.Column(type: "text", nullable: false), + claim_type = table.Column(type: "text", nullable: true), + claim_value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_user_claims", x => x.id); + table.ForeignKey( + name: "fk_user_claims_users_user_id", + column: x => x.user_id, + principalSchema: "werkr_identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_logins", + schema: "werkr_identity", + columns: table => new + { + login_provider = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + provider_key = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + provider_display_name = table.Column(type: "text", nullable: true), + user_id = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_user_logins", x => new { x.login_provider, x.provider_key }); + table.ForeignKey( + name: "fk_user_logins_users_user_id", + column: x => x.user_id, + principalSchema: "werkr_identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_roles", + schema: "werkr_identity", + columns: table => new + { + user_id = table.Column(type: "text", nullable: false), + role_id = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_user_roles", x => new { x.user_id, x.role_id }); + table.ForeignKey( + name: "fk_user_roles_roles_role_id", + column: x => x.role_id, + principalSchema: "werkr_identity", + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_user_roles_users_user_id", + column: x => x.user_id, + principalSchema: "werkr_identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_tokens", + schema: "werkr_identity", + columns: table => new + { + user_id = table.Column(type: "text", nullable: false), + login_provider = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_user_tokens", x => new { x.user_id, x.login_provider, x.name }); + table.ForeignKey( + name: "fk_user_tokens_users_user_id", + column: x => x.user_id, + principalSchema: "werkr_identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_api_keys_created_by_user_id", + schema: "werkr_identity", + table: "api_keys", + column: "created_by_user_id"); + + migrationBuilder.CreateIndex( + name: "ix_api_keys_key_hash", + schema: "werkr_identity", + table: "api_keys", + column: "key_hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_api_keys_key_prefix", + schema: "werkr_identity", + table: "api_keys", + column: "key_prefix"); + + migrationBuilder.CreateIndex( + name: "ix_role_claims_role_id", + schema: "werkr_identity", + table: "role_claims", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "ix_role_permissions_role_id_permission", + schema: "werkr_identity", + table: "role_permissions", + columns: new[] { "role_id", "permission" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + schema: "werkr_identity", + table: "roles", + column: "normalized_name", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_user_claims_user_id", + schema: "werkr_identity", + table: "user_claims", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_user_logins_user_id", + schema: "werkr_identity", + table: "user_logins", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_user_roles_role_id", + schema: "werkr_identity", + table: "user_roles", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + schema: "werkr_identity", + table: "users", + column: "normalized_email"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + schema: "werkr_identity", + table: "users", + column: "normalized_user_name", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api_keys", + schema: "werkr_identity"); + + migrationBuilder.DropTable( + name: "config_settings", + schema: "werkr_identity"); + + migrationBuilder.DropTable( + name: "role_claims", + schema: "werkr_identity"); + + migrationBuilder.DropTable( + name: "role_permissions", + schema: "werkr_identity"); + + migrationBuilder.DropTable( + name: "user_claims", + schema: "werkr_identity"); + + migrationBuilder.DropTable( + name: "user_logins", + schema: "werkr_identity"); + + migrationBuilder.DropTable( + name: "user_roles", + schema: "werkr_identity"); + + migrationBuilder.DropTable( + name: "user_tokens", + schema: "werkr_identity"); + + migrationBuilder.DropTable( + name: "roles", + schema: "werkr_identity"); + + migrationBuilder.DropTable( + name: "users", + schema: "werkr_identity"); + } + } +} diff --git a/src/Werkr.Data.Identity/Migrations/Postgres/PostgresWerkrIdentityDbContextModelSnapshot.cs b/src/Werkr.Data.Identity/Migrations/Postgres/PostgresWerkrIdentityDbContextModelSnapshot.cs index 584ddeb..0301448 100644 --- a/src/Werkr.Data.Identity/Migrations/Postgres/PostgresWerkrIdentityDbContextModelSnapshot.cs +++ b/src/Werkr.Data.Identity/Migrations/Postgres/PostgresWerkrIdentityDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("werkr_identity") - .HasAnnotation("ProductVersion", "10.0.3") + .HasAnnotation("ProductVersion", "10.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); diff --git a/src/Werkr.Data.Identity/Migrations/Sqlite/20260310023738_InitialCreate.cs b/src/Werkr.Data.Identity/Migrations/Sqlite/20260310023738_InitialCreate.cs deleted file mode 100644 index b190475..0000000 --- a/src/Werkr.Data.Identity/Migrations/Sqlite/20260310023738_InitialCreate.cs +++ /dev/null @@ -1,298 +0,0 @@ -#nullable disable - -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Werkr.Data.Identity.Migrations.Sqlite; - -/// -public partial class InitialCreate : Migration { - /// - protected override void Up( MigrationBuilder migrationBuilder ) { - _ = migrationBuilder.CreateTable( - name: "config_settings", - columns: table => new { - id = table.Column( type: "TEXT", nullable: false ), - default_key_size = table.Column( type: "INTEGER", nullable: false ), - server_name = table.Column( type: "TEXT", maxLength: 200, nullable: false ), - allow_registration = table.Column( type: "INTEGER", nullable: false ), - polling_interval_seconds = table.Column( type: "INTEGER", nullable: false ), - run_detail_polling_interval_seconds = table.Column( type: "INTEGER", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_config_settings", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "roles", - columns: table => new { - id = table.Column( type: "TEXT", nullable: false ), - name = table.Column( type: "TEXT", maxLength: 256, nullable: true ), - normalized_name = table.Column( type: "TEXT", maxLength: 256, nullable: true ), - concurrency_stamp = table.Column( type: "TEXT", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_roles", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "users", - columns: table => new { - id = table.Column( type: "TEXT", nullable: false ), - name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), - enabled = table.Column( type: "INTEGER", nullable: false ), - change_password = table.Column( type: "INTEGER", nullable: false ), - requires2fa = table.Column( type: "INTEGER", nullable: false ), - last_login_utc = table.Column( type: "TEXT", nullable: true ), - user_name = table.Column( type: "TEXT", maxLength: 256, nullable: true ), - normalized_user_name = table.Column( type: "TEXT", maxLength: 256, nullable: true ), - email = table.Column( type: "TEXT", maxLength: 256, nullable: true ), - normalized_email = table.Column( type: "TEXT", maxLength: 256, nullable: true ), - email_confirmed = table.Column( type: "INTEGER", nullable: false ), - password_hash = table.Column( type: "TEXT", nullable: true ), - security_stamp = table.Column( type: "TEXT", nullable: true ), - concurrency_stamp = table.Column( type: "TEXT", nullable: true ), - phone_number = table.Column( type: "TEXT", nullable: true ), - phone_number_confirmed = table.Column( type: "INTEGER", nullable: false ), - two_factor_enabled = table.Column( type: "INTEGER", nullable: false ), - lockout_end = table.Column( type: "TEXT", nullable: true ), - lockout_enabled = table.Column( type: "INTEGER", nullable: false ), - access_failed_count = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_users", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "role_claims", - columns: table => new { - id = table.Column( type: "INTEGER", nullable: false ) - .Annotation( "Sqlite:Autoincrement", true ), - role_id = table.Column( type: "TEXT", nullable: false ), - claim_type = table.Column( type: "TEXT", nullable: true ), - claim_value = table.Column( type: "TEXT", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_role_claims", x => x.id ); - _ = table.ForeignKey( - name: "fk_role_claims_roles_role_id", - column: x => x.role_id, - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "role_permissions", - columns: table => new { - id = table.Column( type: "INTEGER", nullable: false ) - .Annotation( "Sqlite:Autoincrement", true ), - role_id = table.Column( type: "TEXT", nullable: false ), - permission = table.Column( type: "TEXT", maxLength: 64, nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_role_permissions", x => x.id ); - _ = table.ForeignKey( - name: "fk_role_permissions_roles_role_id", - column: x => x.role_id, - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "api_keys", - columns: table => new { - id = table.Column( type: "TEXT", nullable: false ), - key_hash = table.Column( type: "TEXT", maxLength: 128, nullable: false ), - key_prefix = table.Column( type: "TEXT", maxLength: 16, nullable: false ), - name = table.Column( type: "TEXT", maxLength: 200, nullable: false ), - role = table.Column( type: "TEXT", maxLength: 64, nullable: false ), - created_by_user_id = table.Column( type: "TEXT", nullable: false ), - created_utc = table.Column( type: "TEXT", nullable: false ), - expires_utc = table.Column( type: "TEXT", nullable: true ), - is_revoked = table.Column( type: "INTEGER", nullable: false ), - last_used_utc = table.Column( type: "TEXT", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_api_keys", x => x.id ); - _ = table.ForeignKey( - name: "fk_api_keys_users_created_by_user_id", - column: x => x.created_by_user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "user_claims", - columns: table => new { - id = table.Column( type: "INTEGER", nullable: false ) - .Annotation( "Sqlite:Autoincrement", true ), - user_id = table.Column( type: "TEXT", nullable: false ), - claim_type = table.Column( type: "TEXT", nullable: true ), - claim_value = table.Column( type: "TEXT", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_user_claims", x => x.id ); - _ = table.ForeignKey( - name: "fk_user_claims_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "user_logins", - columns: table => new { - login_provider = table.Column( type: "TEXT", maxLength: 128, nullable: false ), - provider_key = table.Column( type: "TEXT", maxLength: 128, nullable: false ), - provider_display_name = table.Column( type: "TEXT", nullable: true ), - user_id = table.Column( type: "TEXT", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_user_logins", x => new { x.login_provider, x.provider_key } ); - _ = table.ForeignKey( - name: "fk_user_logins_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "user_roles", - columns: table => new { - user_id = table.Column( type: "TEXT", nullable: false ), - role_id = table.Column( type: "TEXT", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_user_roles", x => new { x.user_id, x.role_id } ); - _ = table.ForeignKey( - name: "fk_user_roles_roles_role_id", - column: x => x.role_id, - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_user_roles_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "user_tokens", - columns: table => new { - user_id = table.Column( type: "TEXT", nullable: false ), - login_provider = table.Column( type: "TEXT", maxLength: 128, nullable: false ), - name = table.Column( type: "TEXT", maxLength: 128, nullable: false ), - value = table.Column( type: "TEXT", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_user_tokens", x => new { x.user_id, x.login_provider, x.name } ); - _ = table.ForeignKey( - name: "fk_user_tokens_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateIndex( - name: "ix_api_keys_created_by_user_id", - table: "api_keys", - column: "created_by_user_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_api_keys_key_hash", - table: "api_keys", - column: "key_hash", - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_api_keys_key_prefix", - table: "api_keys", - column: "key_prefix" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_role_claims_role_id", - table: "role_claims", - column: "role_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_role_permissions_role_id_permission", - table: "role_permissions", - columns: ["role_id", "permission"], - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "RoleNameIndex", - table: "roles", - column: "normalized_name", - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_user_claims_user_id", - table: "user_claims", - column: "user_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_user_logins_user_id", - table: "user_logins", - column: "user_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_user_roles_role_id", - table: "user_roles", - column: "role_id" ); - - _ = migrationBuilder.CreateIndex( - name: "EmailIndex", - table: "users", - column: "normalized_email" ); - - _ = migrationBuilder.CreateIndex( - name: "UserNameIndex", - table: "users", - column: "normalized_user_name", - unique: true ); - } - - /// - protected override void Down( MigrationBuilder migrationBuilder ) { - _ = migrationBuilder.DropTable( - name: "api_keys" ); - - _ = migrationBuilder.DropTable( - name: "config_settings" ); - - _ = migrationBuilder.DropTable( - name: "role_claims" ); - - _ = migrationBuilder.DropTable( - name: "role_permissions" ); - - _ = migrationBuilder.DropTable( - name: "user_claims" ); - - _ = migrationBuilder.DropTable( - name: "user_logins" ); - - _ = migrationBuilder.DropTable( - name: "user_roles" ); - - _ = migrationBuilder.DropTable( - name: "user_tokens" ); - - _ = migrationBuilder.DropTable( - name: "roles" ); - - _ = migrationBuilder.DropTable( - name: "users" ); - } -} diff --git a/src/Werkr.Data.Identity/Migrations/Sqlite/20260310023738_InitialCreate.Designer.cs b/src/Werkr.Data.Identity/Migrations/Sqlite/20260312050800_InitialCreate.Designer.cs similarity index 99% rename from src/Werkr.Data.Identity/Migrations/Sqlite/20260310023738_InitialCreate.Designer.cs rename to src/Werkr.Data.Identity/Migrations/Sqlite/20260312050800_InitialCreate.Designer.cs index 426f3f4..9eb5f70 100644 --- a/src/Werkr.Data.Identity/Migrations/Sqlite/20260310023738_InitialCreate.Designer.cs +++ b/src/Werkr.Data.Identity/Migrations/Sqlite/20260312050800_InitialCreate.Designer.cs @@ -11,14 +11,14 @@ namespace Werkr.Data.Identity.Migrations.Sqlite { [DbContext(typeof(SqliteWerkrIdentityDbContext))] - [Migration("20260310023738_InitialCreate")] + [Migration("20260312050800_InitialCreate")] partial class InitialCreate { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.3"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.4"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { diff --git a/src/Werkr.Data.Identity/Migrations/Sqlite/20260312050800_InitialCreate.cs b/src/Werkr.Data.Identity/Migrations/Sqlite/20260312050800_InitialCreate.cs new file mode 100644 index 0000000..76bd699 --- /dev/null +++ b/src/Werkr.Data.Identity/Migrations/Sqlite/20260312050800_InitialCreate.cs @@ -0,0 +1,323 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Werkr.Data.Identity.Migrations.Sqlite +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "config_settings", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + default_key_size = table.Column(type: "INTEGER", nullable: false), + server_name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + allow_registration = table.Column(type: "INTEGER", nullable: false), + polling_interval_seconds = table.Column(type: "INTEGER", nullable: false), + run_detail_polling_interval_seconds = table.Column(type: "INTEGER", nullable: false), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_config_settings", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "roles", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + normalized_name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + concurrency_stamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_roles", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + enabled = table.Column(type: "INTEGER", nullable: false), + change_password = table.Column(type: "INTEGER", nullable: false), + requires2fa = table.Column(type: "INTEGER", nullable: false), + last_login_utc = table.Column(type: "TEXT", nullable: true), + user_name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + normalized_user_name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + normalized_email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + email_confirmed = table.Column(type: "INTEGER", nullable: false), + password_hash = table.Column(type: "TEXT", nullable: true), + security_stamp = table.Column(type: "TEXT", nullable: true), + concurrency_stamp = table.Column(type: "TEXT", nullable: true), + phone_number = table.Column(type: "TEXT", nullable: true), + phone_number_confirmed = table.Column(type: "INTEGER", nullable: false), + two_factor_enabled = table.Column(type: "INTEGER", nullable: false), + lockout_end = table.Column(type: "TEXT", nullable: true), + lockout_enabled = table.Column(type: "INTEGER", nullable: false), + access_failed_count = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "role_claims", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + role_id = table.Column(type: "TEXT", nullable: false), + claim_type = table.Column(type: "TEXT", nullable: true), + claim_value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_role_claims", x => x.id); + table.ForeignKey( + name: "fk_role_claims_roles_role_id", + column: x => x.role_id, + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "role_permissions", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + role_id = table.Column(type: "TEXT", nullable: false), + permission = table.Column(type: "TEXT", maxLength: 64, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_role_permissions", x => x.id); + table.ForeignKey( + name: "fk_role_permissions_roles_role_id", + column: x => x.role_id, + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "api_keys", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + key_hash = table.Column(type: "TEXT", maxLength: 128, nullable: false), + key_prefix = table.Column(type: "TEXT", maxLength: 16, nullable: false), + name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + role = table.Column(type: "TEXT", maxLength: 64, nullable: false), + created_by_user_id = table.Column(type: "TEXT", nullable: false), + created_utc = table.Column(type: "TEXT", nullable: false), + expires_utc = table.Column(type: "TEXT", nullable: true), + is_revoked = table.Column(type: "INTEGER", nullable: false), + last_used_utc = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_api_keys", x => x.id); + table.ForeignKey( + name: "fk_api_keys_users_created_by_user_id", + column: x => x.created_by_user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_claims", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + user_id = table.Column(type: "TEXT", nullable: false), + claim_type = table.Column(type: "TEXT", nullable: true), + claim_value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_user_claims", x => x.id); + table.ForeignKey( + name: "fk_user_claims_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_logins", + columns: table => new + { + login_provider = table.Column(type: "TEXT", maxLength: 128, nullable: false), + provider_key = table.Column(type: "TEXT", maxLength: 128, nullable: false), + provider_display_name = table.Column(type: "TEXT", nullable: true), + user_id = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_user_logins", x => new { x.login_provider, x.provider_key }); + table.ForeignKey( + name: "fk_user_logins_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_roles", + columns: table => new + { + user_id = table.Column(type: "TEXT", nullable: false), + role_id = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_user_roles", x => new { x.user_id, x.role_id }); + table.ForeignKey( + name: "fk_user_roles_roles_role_id", + column: x => x.role_id, + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_user_roles_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_tokens", + columns: table => new + { + user_id = table.Column(type: "TEXT", nullable: false), + login_provider = table.Column(type: "TEXT", maxLength: 128, nullable: false), + name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_user_tokens", x => new { x.user_id, x.login_provider, x.name }); + table.ForeignKey( + name: "fk_user_tokens_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_api_keys_created_by_user_id", + table: "api_keys", + column: "created_by_user_id"); + + migrationBuilder.CreateIndex( + name: "ix_api_keys_key_hash", + table: "api_keys", + column: "key_hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_api_keys_key_prefix", + table: "api_keys", + column: "key_prefix"); + + migrationBuilder.CreateIndex( + name: "ix_role_claims_role_id", + table: "role_claims", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "ix_role_permissions_role_id_permission", + table: "role_permissions", + columns: new[] { "role_id", "permission" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "roles", + column: "normalized_name", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_user_claims_user_id", + table: "user_claims", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_user_logins_user_id", + table: "user_logins", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_user_roles_role_id", + table: "user_roles", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "users", + column: "normalized_email"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "users", + column: "normalized_user_name", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "api_keys"); + + migrationBuilder.DropTable( + name: "config_settings"); + + migrationBuilder.DropTable( + name: "role_claims"); + + migrationBuilder.DropTable( + name: "role_permissions"); + + migrationBuilder.DropTable( + name: "user_claims"); + + migrationBuilder.DropTable( + name: "user_logins"); + + migrationBuilder.DropTable( + name: "user_roles"); + + migrationBuilder.DropTable( + name: "user_tokens"); + + migrationBuilder.DropTable( + name: "roles"); + + migrationBuilder.DropTable( + name: "users"); + } + } +} diff --git a/src/Werkr.Data.Identity/Migrations/Sqlite/SqliteWerkrIdentityDbContextModelSnapshot.cs b/src/Werkr.Data.Identity/Migrations/Sqlite/SqliteWerkrIdentityDbContextModelSnapshot.cs index fb4db83..6cf7059 100644 --- a/src/Werkr.Data.Identity/Migrations/Sqlite/SqliteWerkrIdentityDbContextModelSnapshot.cs +++ b/src/Werkr.Data.Identity/Migrations/Sqlite/SqliteWerkrIdentityDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class SqliteWerkrIdentityDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.3"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.4"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { diff --git a/src/Werkr.Data/Entities/Workflows/ControlStatement.cs b/src/Werkr.Data/Entities/Workflows/ControlStatement.cs index 31819e8..1b81306 100644 --- a/src/Werkr.Data/Entities/Workflows/ControlStatement.cs +++ b/src/Werkr.Data/Entities/Workflows/ControlStatement.cs @@ -5,8 +5,10 @@ namespace Werkr.Data.Entities.Workflows; /// public enum ControlStatement { - /// Execute sequentially after dependencies complete. - Sequential = 0, + /// Default control flow. Step runs after dependencies complete. + /// Parallelism is implicit: steps at the same topological level with no + /// mutual dependencies execute concurrently. + Default = 0, /// Execute only if condition is true. If = 1, diff --git a/src/Werkr.Data/Entities/Workflows/Workflow.cs b/src/Werkr.Data/Entities/Workflows/Workflow.cs index 792ce89..daa9f16 100644 --- a/src/Werkr.Data/Entities/Workflows/Workflow.cs +++ b/src/Werkr.Data/Entities/Workflows/Workflow.cs @@ -28,6 +28,9 @@ public class Workflow : ConcurrencyBase, IKey { /// Whether the workflow is enabled. public bool Enabled { get; set; } = true; + /// Agent targeting tags for workflow-level override. + public string[]? TargetTags { get; set; } + /// Navigation property for workflow steps. public ICollection Steps { get; set; } = []; diff --git a/src/Werkr.Data/Entities/Workflows/WorkflowStep.cs b/src/Werkr.Data/Entities/Workflows/WorkflowStep.cs index c461dc1..39aaa0c 100644 --- a/src/Werkr.Data/Entities/Workflows/WorkflowStep.cs +++ b/src/Werkr.Data/Entities/Workflows/WorkflowStep.cs @@ -28,13 +28,13 @@ public class WorkflowStep : ConcurrencyBase, IKey { public int Order { get; set; } /// Control flow statement type for this step. - public ControlStatement ControlStatement { get; set; } = ControlStatement.Sequential; + public ControlStatement ControlStatement { get; set; } = ControlStatement.Default; /// /// Condition expression evaluated against prior step results. /// Supports: $exitCode == 0, $exitCode != 0, $exitCode > N, /// $? -eq $true, $? -eq $false, and custom expressions. - /// Null/empty = always true (for Sequential steps). + /// Null/empty = always true (for Default steps). /// [MaxLength( 2000 )] public string? ConditionExpression { get; set; } diff --git a/src/Werkr.Data/Migrations/Postgres/20260310023423_InitialCreate.cs b/src/Werkr.Data/Migrations/Postgres/20260310023423_InitialCreate.cs deleted file mode 100644 index 110ce76..0000000 --- a/src/Werkr.Data/Migrations/Postgres/20260310023423_InitialCreate.cs +++ /dev/null @@ -1,923 +0,0 @@ -#nullable disable - -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -namespace Werkr.Data.Migrations.Postgres; - -/// -public partial class InitialCreate : Migration { - /// - protected override void Up( MigrationBuilder migrationBuilder ) { - _ = migrationBuilder.EnsureSchema( - name: "werkr" ); - - _ = migrationBuilder.CreateTable( - name: "holiday_calendars", - schema: "werkr", - columns: table => new { - id = table.Column( type: "uuid", nullable: false ), - name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), - description = table.Column( type: "character varying(1024)", maxLength: 1024, nullable: false ), - is_system_calendar = table.Column( type: "boolean", nullable: false ), - created_utc = table.Column( type: "text", nullable: false ), - updated_utc = table.Column( type: "text", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_holiday_calendars", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "registered_connections", - schema: "werkr", - columns: table => new { - id = table.Column( type: "uuid", nullable: false ), - connection_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), - remote_url = table.Column( type: "character varying(2048)", maxLength: 2048, nullable: false ), - local_public_key = table.Column( type: "text", nullable: false ), - local_private_key = table.Column( type: "text", nullable: false ), - remote_public_key = table.Column( type: "text", nullable: false ), - outbound_api_key = table.Column( type: "character varying(512)", maxLength: 512, nullable: false ), - inbound_api_key_hash = table.Column( type: "character varying(512)", maxLength: 512, nullable: false ), - shared_key = table.Column( type: "text", nullable: false ), - previous_shared_key = table.Column( type: "text", nullable: true ), - active_key_id = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ), - previous_key_id = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ), - is_server = table.Column( type: "boolean", nullable: false ), - status = table.Column( type: "text", nullable: false ), - last_seen = table.Column( type: "text", nullable: true ), - tags = table.Column( type: "text", nullable: false ), - allowed_paths = table.Column( type: "text", nullable: false ), - enforce_allowlist = table.Column( type: "boolean", nullable: false ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_registered_connections", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "registration_bundles", - schema: "werkr", - columns: table => new { - id = table.Column( type: "uuid", nullable: false ), - connection_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), - server_public_key = table.Column( type: "text", nullable: false ), - server_private_key = table.Column( type: "text", nullable: false ), - bundle_id = table.Column( type: "text", nullable: false ), - status = table.Column( type: "text", nullable: false ), - expires_at = table.Column( type: "text", nullable: false ), - key_size = table.Column( type: "integer", nullable: false ), - tags = table.Column( type: "text[]", nullable: false ), - allowed_paths = table.Column( type: "text", nullable: false ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_registration_bundles", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedules", - schema: "werkr", - columns: table => new { - id = table.Column( type: "uuid", nullable: false ), - name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), - stop_task_after_minutes = table.Column( type: "bigint", nullable: false ), - catch_up_enabled = table.Column( type: "boolean", nullable: false ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedules", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflows", - schema: "werkr", - columns: table => new { - id = table.Column( type: "bigint", nullable: false ) - .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), - name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), - description = table.Column( type: "character varying(2000)", maxLength: 2000, nullable: false ), - enabled = table.Column( type: "boolean", nullable: false ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflows", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "holiday_rules", - schema: "werkr", - columns: table => new { - id = table.Column( type: "bigint", nullable: false ) - .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn ), - holiday_calendar_id = table.Column( type: "uuid", nullable: false ), - name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), - rule_type = table.Column( type: "text", nullable: false ), - month = table.Column( type: "integer", nullable: true ), - day = table.Column( type: "integer", nullable: true ), - day_of_week = table.Column( type: "integer", nullable: true ), - week_number = table.Column( type: "integer", nullable: true ), - window_start = table.Column( type: "time without time zone", nullable: true ), - window_end = table.Column( type: "time without time zone", nullable: true ), - window_time_zone_id = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ), - observance_rule = table.Column( type: "text", nullable: false ), - year_start = table.Column( type: "integer", nullable: true ), - year_end = table.Column( type: "integer", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_holiday_rules", x => x.id ); - _ = table.ForeignKey( - name: "fk_holiday_rules_holiday_calendars_holiday_calendar_id", - column: x => x.holiday_calendar_id, - principalSchema: "werkr", - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "daily_recurrence", - schema: "werkr", - columns: table => new { - schedule_id = table.Column( type: "uuid", nullable: false ), - day_interval = table.Column( type: "integer", nullable: false ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_daily_recurrence", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_daily_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "monthly_recurrence", - schema: "werkr", - columns: table => new { - schedule_id = table.Column( type: "uuid", nullable: false ), - day_numbers = table.Column( type: "text", nullable: true ), - months_of_year = table.Column( type: "integer", nullable: false ), - week_number = table.Column( type: "integer", nullable: true ), - days_of_week = table.Column( type: "integer", nullable: true ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_monthly_recurrence", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_monthly_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedule_audit_log", - schema: "werkr", - columns: table => new { - id = table.Column( type: "bigint", nullable: false ) - .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn ), - schedule_id = table.Column( type: "uuid", nullable: false ), - occurrence_utc_time = table.Column( type: "text", nullable: false ), - calendar_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), - holiday_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), - mode = table.Column( type: "text", nullable: false ), - created_utc = table.Column( type: "text", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedule_audit_log", x => x.id ); - _ = table.ForeignKey( - name: "fk_schedule_audit_log_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedule_expiration", - schema: "werkr", - columns: table => new { - schedule_id = table.Column( type: "uuid", nullable: false ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ), - date = table.Column( type: "date", nullable: false ), - time = table.Column( type: "time without time zone", nullable: false ), - time_zone = table.Column( type: "text", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedule_expiration", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_schedule_expiration_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedule_holiday_calendars", - schema: "werkr", - columns: table => new { - schedule_id = table.Column( type: "uuid", nullable: false ), - holiday_calendar_id = table.Column( type: "uuid", nullable: false ), - mode = table.Column( type: "text", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedule_holiday_calendars", x => new { x.schedule_id, x.holiday_calendar_id } ); - _ = table.ForeignKey( - name: "fk_schedule_holiday_calendars_holiday_calendars_holiday_calend", - column: x => x.holiday_calendar_id, - principalSchema: "werkr", - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_schedule_holiday_calendars_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedule_repeat_options", - schema: "werkr", - columns: table => new { - schedule_id = table.Column( type: "uuid", nullable: false ), - repeat_interval_minutes = table.Column( type: "integer", nullable: false ), - repeat_duration_minutes = table.Column( type: "integer", nullable: false ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedule_repeat_options", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_schedule_repeat_options_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedule_start_datetimeinfo", - schema: "werkr", - columns: table => new { - schedule_id = table.Column( type: "uuid", nullable: false ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ), - date = table.Column( type: "date", nullable: false ), - time = table.Column( type: "time without time zone", nullable: false ), - time_zone = table.Column( type: "text", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedule_start_datetimeinfo", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_schedule_start_datetimeinfo_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "weekly_recurrence", - schema: "werkr", - columns: table => new { - schedule_id = table.Column( type: "uuid", nullable: false ), - week_interval = table.Column( type: "integer", nullable: false ), - days_of_week = table.Column( type: "integer", nullable: false ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_weekly_recurrence", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_weekly_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "tasks", - schema: "werkr", - columns: table => new { - id = table.Column( type: "bigint", nullable: false ) - .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), - name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), - description = table.Column( type: "character varying(2000)", maxLength: 2000, nullable: false ), - action_type = table.Column( type: "text", nullable: false ), - workflow_id = table.Column( type: "bigint", nullable: true ), - content = table.Column( type: "character varying(8000)", maxLength: 8000, nullable: false ), - arguments = table.Column( type: "text", nullable: true ), - target_tags = table.Column( type: "text", nullable: false ), - enabled = table.Column( type: "boolean", nullable: false ), - is_ephemeral = table.Column( type: "boolean", nullable: false ), - timeout_minutes = table.Column( type: "bigint", nullable: true ), - sync_interval_minutes = table.Column( type: "integer", nullable: false ), - success_criteria = table.Column( type: "character varying(500)", maxLength: 500, nullable: true ), - action_sub_type = table.Column( type: "character varying(30)", maxLength: 30, nullable: true ), - action_parameters = table.Column( type: "text", nullable: true ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_tasks", x => x.id ); - _ = table.ForeignKey( - name: "fk_tasks_workflows_workflow_id", - column: x => x.workflow_id, - principalSchema: "werkr", - principalTable: "workflows", - principalColumn: "id" ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_runs", - schema: "werkr", - columns: table => new { - id = table.Column( type: "uuid", nullable: false ), - workflow_id = table.Column( type: "bigint", nullable: false ), - start_time = table.Column( type: "text", nullable: false ), - end_time = table.Column( type: "text", nullable: true ), - status = table.Column( type: "text", nullable: false ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_runs", x => x.id ); - _ = table.ForeignKey( - name: "fk_workflow_runs_workflows_workflow_id", - column: x => x.workflow_id, - principalSchema: "werkr", - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_schedules", - schema: "werkr", - columns: table => new { - workflow_id = table.Column( type: "bigint", nullable: false ), - schedule_id = table.Column( type: "uuid", nullable: false ), - created_at_utc = table.Column( type: "text", nullable: false ), - is_one_time = table.Column( type: "boolean", nullable: false ), - workflow_run_id = table.Column( type: "uuid", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_schedules", x => new { x.workflow_id, x.schedule_id } ); - _ = table.ForeignKey( - name: "fk_workflow_schedules_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_workflow_schedules_workflows_workflow_id", - column: x => x.workflow_id, - principalSchema: "werkr", - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_variables", - schema: "werkr", - columns: table => new { - id = table.Column( type: "bigint", nullable: false ) - .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), - workflow_id = table.Column( type: "bigint", nullable: false ), - name = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), - description = table.Column( type: "character varying(500)", maxLength: 500, nullable: true ), - default_value = table.Column( type: "text", nullable: true ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_variables", x => x.id ); - _ = table.ForeignKey( - name: "fk_workflow_variables_workflows_workflow_id", - column: x => x.workflow_id, - principalSchema: "werkr", - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "holiday_dates", - schema: "werkr", - columns: table => new { - id = table.Column( type: "bigint", nullable: false ) - .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn ), - holiday_calendar_id = table.Column( type: "uuid", nullable: false ), - holiday_rule_id = table.Column( type: "bigint", nullable: true ), - date = table.Column( type: "date", nullable: false ), - name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), - year = table.Column( type: "integer", nullable: false ), - window_start = table.Column( type: "time without time zone", nullable: true ), - window_end = table.Column( type: "time without time zone", nullable: true ), - window_time_zone_id = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_holiday_dates", x => x.id ); - _ = table.ForeignKey( - name: "fk_holiday_dates_holiday_calendars_holiday_calendar_id", - column: x => x.holiday_calendar_id, - principalSchema: "werkr", - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_holiday_dates_holiday_rules_holiday_rule_id", - column: x => x.holiday_rule_id, - principalSchema: "werkr", - principalTable: "holiday_rules", - principalColumn: "id", - onDelete: ReferentialAction.SetNull ); - } ); - - _ = migrationBuilder.CreateTable( - name: "task_schedules", - schema: "werkr", - columns: table => new { - task_id = table.Column( type: "bigint", nullable: false ), - schedule_id = table.Column( type: "uuid", nullable: false ), - created_at_utc = table.Column( type: "text", nullable: false ), - is_one_time = table.Column( type: "boolean", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_task_schedules", x => new { x.task_id, x.schedule_id } ); - _ = table.ForeignKey( - name: "fk_task_schedules_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_task_schedules_tasks_task_id", - column: x => x.task_id, - principalSchema: "werkr", - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_steps", - schema: "werkr", - columns: table => new { - id = table.Column( type: "bigint", nullable: false ) - .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), - workflow_id = table.Column( type: "bigint", nullable: false ), - task_id = table.Column( type: "bigint", nullable: false ), - order = table.Column( type: "integer", nullable: false ), - control_statement = table.Column( type: "text", nullable: false ), - condition_expression = table.Column( type: "character varying(2000)", maxLength: 2000, nullable: true ), - max_iterations = table.Column( type: "integer", nullable: false ), - agent_connection_id_override = table.Column( type: "uuid", nullable: true ), - dependency_mode = table.Column( type: "text", nullable: false ), - input_variable_name = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ), - output_variable_name = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_steps", x => x.id ); - _ = table.ForeignKey( - name: "fk_workflow_steps_registered_connections_agent_connection_id_o", - column: x => x.agent_connection_id_override, - principalSchema: "werkr", - principalTable: "registered_connections", - principalColumn: "id" ); - _ = table.ForeignKey( - name: "fk_workflow_steps_tasks_task_id", - column: x => x.task_id, - principalSchema: "werkr", - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_workflow_steps_workflows_workflow_id", - column: x => x.workflow_id, - principalSchema: "werkr", - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "jobs", - schema: "werkr", - columns: table => new { - id = table.Column( type: "uuid", nullable: false ), - task_id = table.Column( type: "bigint", nullable: false ), - task_snapshot = table.Column( type: "character varying(8000)", maxLength: 8000, nullable: false ), - runtime_seconds = table.Column( type: "double precision", nullable: false ), - start_time = table.Column( type: "text", nullable: false ), - end_time = table.Column( type: "text", nullable: true ), - success = table.Column( type: "boolean", nullable: false ), - agent_connection_id = table.Column( type: "uuid", nullable: true ), - exit_code = table.Column( type: "integer", nullable: true ), - error_category = table.Column( type: "text", nullable: false ), - output = table.Column( type: "character varying(2000)", maxLength: 2000, nullable: true ), - output_path = table.Column( type: "character varying(512)", maxLength: 512, nullable: true ), - workflow_run_id = table.Column( type: "uuid", nullable: true ), - schedule_id = table.Column( type: "uuid", nullable: true ), - created = table.Column( type: "text", nullable: false ), - last_updated = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_jobs", x => x.id ); - _ = table.ForeignKey( - name: "fk_jobs_registered_connections_agent_connection_id", - column: x => x.agent_connection_id, - principalSchema: "werkr", - principalTable: "registered_connections", - principalColumn: "id" ); - _ = table.ForeignKey( - name: "fk_jobs_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id" ); - _ = table.ForeignKey( - name: "fk_jobs_tasks_task_id", - column: x => x.task_id, - principalSchema: "werkr", - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_jobs_workflow_runs_workflow_run_id", - column: x => x.workflow_run_id, - principalSchema: "werkr", - principalTable: "workflow_runs", - principalColumn: "id" ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_step_dependencies", - schema: "werkr", - columns: table => new { - step_id = table.Column( type: "bigint", nullable: false ), - depends_on_step_id = table.Column( type: "bigint", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_step_dependencies", x => new { x.step_id, x.depends_on_step_id } ); - _ = table.ForeignKey( - name: "fk_workflow_step_dependencies_workflow_steps_depends_on_step_id", - column: x => x.depends_on_step_id, - principalSchema: "werkr", - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.Restrict ); - _ = table.ForeignKey( - name: "fk_workflow_step_dependencies_workflow_steps_step_id", - column: x => x.step_id, - principalSchema: "werkr", - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_run_variables", - schema: "werkr", - columns: table => new { - id = table.Column( type: "bigint", nullable: false ) - .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), - workflow_run_id = table.Column( type: "uuid", nullable: false ), - variable_name = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), - value = table.Column( type: "text", nullable: false ), - version = table.Column( type: "integer", nullable: false ), - produced_by_step_id = table.Column( type: "bigint", nullable: true ), - produced_by_job_id = table.Column( type: "uuid", nullable: true ), - source = table.Column( type: "text", nullable: false ), - created = table.Column( type: "text", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_run_variables", x => x.id ); - _ = table.ForeignKey( - name: "fk_workflow_run_variables_jobs_produced_by_job_id", - column: x => x.produced_by_job_id, - principalSchema: "werkr", - principalTable: "jobs", - principalColumn: "id", - onDelete: ReferentialAction.SetNull ); - _ = table.ForeignKey( - name: "fk_workflow_run_variables_workflow_runs_workflow_run_id", - column: x => x.workflow_run_id, - principalSchema: "werkr", - principalTable: "workflow_runs", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_workflow_run_variables_workflow_steps_produced_by_step_id", - column: x => x.produced_by_step_id, - principalSchema: "werkr", - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.SetNull ); - } ); - - _ = migrationBuilder.CreateIndex( - name: "ix_holiday_calendars_name", - schema: "werkr", - table: "holiday_calendars", - column: "name", - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_holiday_dates_holiday_calendar_id_date", - schema: "werkr", - table: "holiday_dates", - columns: ["holiday_calendar_id", "date"], - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_holiday_dates_holiday_rule_id", - schema: "werkr", - table: "holiday_dates", - column: "holiday_rule_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_holiday_rules_holiday_calendar_id", - schema: "werkr", - table: "holiday_rules", - column: "holiday_calendar_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_jobs_agent_connection_id", - schema: "werkr", - table: "jobs", - column: "agent_connection_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_jobs_schedule_id", - schema: "werkr", - table: "jobs", - column: "schedule_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_jobs_task_id", - schema: "werkr", - table: "jobs", - column: "task_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_jobs_workflow_run_id", - schema: "werkr", - table: "jobs", - column: "workflow_run_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_registered_connections_connection_name", - schema: "werkr", - table: "registered_connections", - column: "connection_name" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_registered_connections_remote_url", - schema: "werkr", - table: "registered_connections", - column: "remote_url" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_registration_bundles_bundle_id", - schema: "werkr", - table: "registration_bundles", - column: "bundle_id", - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_schedule_audit_log_schedule_id_occurrence_utc_time", - schema: "werkr", - table: "schedule_audit_log", - columns: ["schedule_id", "occurrence_utc_time"] ); - - _ = migrationBuilder.CreateIndex( - name: "ix_schedule_holiday_calendars_holiday_calendar_id", - schema: "werkr", - table: "schedule_holiday_calendars", - column: "holiday_calendar_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_schedule_holiday_calendars_schedule_id", - schema: "werkr", - table: "schedule_holiday_calendars", - column: "schedule_id", - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_task_schedules_schedule_id", - schema: "werkr", - table: "task_schedules", - column: "schedule_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_tasks_workflow_id", - schema: "werkr", - table: "tasks", - column: "workflow_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_produced_by_job_id", - schema: "werkr", - table: "workflow_run_variables", - column: "produced_by_job_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_produced_by_step_id", - schema: "werkr", - table: "workflow_run_variables", - column: "produced_by_step_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_workflow_run_id_variable_name_version", - schema: "werkr", - table: "workflow_run_variables", - columns: ["workflow_run_id", "variable_name", "version"], - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_runs_workflow_id", - schema: "werkr", - table: "workflow_runs", - column: "workflow_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_schedules_schedule_id", - schema: "werkr", - table: "workflow_schedules", - column: "schedule_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_step_dependencies_depends_on_step_id", - schema: "werkr", - table: "workflow_step_dependencies", - column: "depends_on_step_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_steps_agent_connection_id_override", - schema: "werkr", - table: "workflow_steps", - column: "agent_connection_id_override" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_steps_task_id", - schema: "werkr", - table: "workflow_steps", - column: "task_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_steps_workflow_id", - schema: "werkr", - table: "workflow_steps", - column: "workflow_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_variables_workflow_id_name", - schema: "werkr", - table: "workflow_variables", - columns: ["workflow_id", "name"], - unique: true ); - } - - /// - protected override void Down( MigrationBuilder migrationBuilder ) { - _ = migrationBuilder.DropTable( - name: "daily_recurrence", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "holiday_dates", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "monthly_recurrence", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "registration_bundles", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "schedule_audit_log", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "schedule_expiration", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "schedule_holiday_calendars", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "schedule_repeat_options", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "schedule_start_datetimeinfo", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "task_schedules", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "weekly_recurrence", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "workflow_run_variables", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "workflow_schedules", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "workflow_step_dependencies", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "workflow_variables", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "holiday_rules", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "jobs", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "workflow_steps", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "holiday_calendars", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "schedules", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "workflow_runs", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "registered_connections", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "tasks", - schema: "werkr" ); - - _ = migrationBuilder.DropTable( - name: "workflows", - schema: "werkr" ); - } -} diff --git a/src/Werkr.Data/Migrations/Postgres/20260310023423_InitialCreate.Designer.cs b/src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.Designer.cs similarity index 99% rename from src/Werkr.Data/Migrations/Postgres/20260310023423_InitialCreate.Designer.cs rename to src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.Designer.cs index a65ba4f..ff8e77f 100644 --- a/src/Werkr.Data/Migrations/Postgres/20260310023423_InitialCreate.Designer.cs +++ b/src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.Designer.cs @@ -12,7 +12,7 @@ namespace Werkr.Data.Migrations.Postgres { [DbContext(typeof(PostgresWerkrDbContext))] - [Migration("20260310023423_InitialCreate")] + [Migration("20260312050739_InitialCreate")] partial class InitialCreate { /// @@ -21,7 +21,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("werkr") - .HasAnnotation("ProductVersion", "10.0.3") + .HasAnnotation("ProductVersion", "10.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -1000,6 +1000,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("character varying(256)") .HasColumnName("name"); + b.Property("TargetTags") + .HasColumnType("text") + .HasColumnName("target_tags"); + b.Property("Version") .IsConcurrencyToken() .HasColumnType("integer") diff --git a/src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.cs b/src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.cs new file mode 100644 index 0000000..e2e9e2b --- /dev/null +++ b/src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.cs @@ -0,0 +1,977 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Werkr.Data.Migrations.Postgres +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "werkr"); + + migrationBuilder.CreateTable( + name: "holiday_calendars", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), + is_system_calendar = table.Column(type: "boolean", nullable: false), + created_utc = table.Column(type: "text", nullable: false), + updated_utc = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_holiday_calendars", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "registered_connections", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + connection_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + remote_url = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), + local_public_key = table.Column(type: "text", nullable: false), + local_private_key = table.Column(type: "text", nullable: false), + remote_public_key = table.Column(type: "text", nullable: false), + outbound_api_key = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + inbound_api_key_hash = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + shared_key = table.Column(type: "text", nullable: false), + previous_shared_key = table.Column(type: "text", nullable: true), + active_key_id = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + previous_key_id = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + is_server = table.Column(type: "boolean", nullable: false), + status = table.Column(type: "text", nullable: false), + last_seen = table.Column(type: "text", nullable: true), + tags = table.Column(type: "text", nullable: false), + allowed_paths = table.Column(type: "text", nullable: false), + enforce_allowlist = table.Column(type: "boolean", nullable: false), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_registered_connections", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "registration_bundles", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + connection_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + server_public_key = table.Column(type: "text", nullable: false), + server_private_key = table.Column(type: "text", nullable: false), + bundle_id = table.Column(type: "text", nullable: false), + status = table.Column(type: "text", nullable: false), + expires_at = table.Column(type: "text", nullable: false), + key_size = table.Column(type: "integer", nullable: false), + tags = table.Column(type: "text[]", nullable: false), + allowed_paths = table.Column(type: "text", nullable: false), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_registration_bundles", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "schedules", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + stop_task_after_minutes = table.Column(type: "bigint", nullable: false), + catch_up_enabled = table.Column(type: "boolean", nullable: false), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedules", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "workflows", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), + enabled = table.Column(type: "boolean", nullable: false), + target_tags = table.Column(type: "text", nullable: true), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflows", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "holiday_rules", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn), + holiday_calendar_id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + rule_type = table.Column(type: "text", nullable: false), + month = table.Column(type: "integer", nullable: true), + day = table.Column(type: "integer", nullable: true), + day_of_week = table.Column(type: "integer", nullable: true), + week_number = table.Column(type: "integer", nullable: true), + window_start = table.Column(type: "time without time zone", nullable: true), + window_end = table.Column(type: "time without time zone", nullable: true), + window_time_zone_id = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + observance_rule = table.Column(type: "text", nullable: false), + year_start = table.Column(type: "integer", nullable: true), + year_end = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_holiday_rules", x => x.id); + table.ForeignKey( + name: "fk_holiday_rules_holiday_calendars_holiday_calendar_id", + column: x => x.holiday_calendar_id, + principalSchema: "werkr", + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "daily_recurrence", + schema: "werkr", + columns: table => new + { + schedule_id = table.Column(type: "uuid", nullable: false), + day_interval = table.Column(type: "integer", nullable: false), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_daily_recurrence", x => x.schedule_id); + table.ForeignKey( + name: "fk_daily_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "monthly_recurrence", + schema: "werkr", + columns: table => new + { + schedule_id = table.Column(type: "uuid", nullable: false), + day_numbers = table.Column(type: "text", nullable: true), + months_of_year = table.Column(type: "integer", nullable: false), + week_number = table.Column(type: "integer", nullable: true), + days_of_week = table.Column(type: "integer", nullable: true), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_monthly_recurrence", x => x.schedule_id); + table.ForeignKey( + name: "fk_monthly_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "schedule_audit_log", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn), + schedule_id = table.Column(type: "uuid", nullable: false), + occurrence_utc_time = table.Column(type: "text", nullable: false), + calendar_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + holiday_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + mode = table.Column(type: "text", nullable: false), + created_utc = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedule_audit_log", x => x.id); + table.ForeignKey( + name: "fk_schedule_audit_log_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "schedule_expiration", + schema: "werkr", + columns: table => new + { + schedule_id = table.Column(type: "uuid", nullable: false), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false), + date = table.Column(type: "date", nullable: false), + time = table.Column(type: "time without time zone", nullable: false), + time_zone = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedule_expiration", x => x.schedule_id); + table.ForeignKey( + name: "fk_schedule_expiration_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "schedule_holiday_calendars", + schema: "werkr", + columns: table => new + { + schedule_id = table.Column(type: "uuid", nullable: false), + holiday_calendar_id = table.Column(type: "uuid", nullable: false), + mode = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedule_holiday_calendars", x => new { x.schedule_id, x.holiday_calendar_id }); + table.ForeignKey( + name: "fk_schedule_holiday_calendars_holiday_calendars_holiday_calend", + column: x => x.holiday_calendar_id, + principalSchema: "werkr", + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_schedule_holiday_calendars_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "schedule_repeat_options", + schema: "werkr", + columns: table => new + { + schedule_id = table.Column(type: "uuid", nullable: false), + repeat_interval_minutes = table.Column(type: "integer", nullable: false), + repeat_duration_minutes = table.Column(type: "integer", nullable: false), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedule_repeat_options", x => x.schedule_id); + table.ForeignKey( + name: "fk_schedule_repeat_options_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "schedule_start_datetimeinfo", + schema: "werkr", + columns: table => new + { + schedule_id = table.Column(type: "uuid", nullable: false), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false), + date = table.Column(type: "date", nullable: false), + time = table.Column(type: "time without time zone", nullable: false), + time_zone = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedule_start_datetimeinfo", x => x.schedule_id); + table.ForeignKey( + name: "fk_schedule_start_datetimeinfo_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "weekly_recurrence", + schema: "werkr", + columns: table => new + { + schedule_id = table.Column(type: "uuid", nullable: false), + week_interval = table.Column(type: "integer", nullable: false), + days_of_week = table.Column(type: "integer", nullable: false), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_weekly_recurrence", x => x.schedule_id); + table.ForeignKey( + name: "fk_weekly_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tasks", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), + action_type = table.Column(type: "text", nullable: false), + workflow_id = table.Column(type: "bigint", nullable: true), + content = table.Column(type: "character varying(8000)", maxLength: 8000, nullable: false), + arguments = table.Column(type: "text", nullable: true), + target_tags = table.Column(type: "text", nullable: false), + enabled = table.Column(type: "boolean", nullable: false), + is_ephemeral = table.Column(type: "boolean", nullable: false), + timeout_minutes = table.Column(type: "bigint", nullable: true), + sync_interval_minutes = table.Column(type: "integer", nullable: false), + success_criteria = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + action_sub_type = table.Column(type: "character varying(30)", maxLength: 30, nullable: true), + action_parameters = table.Column(type: "text", nullable: true), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tasks", x => x.id); + table.ForeignKey( + name: "fk_tasks_workflows_workflow_id", + column: x => x.workflow_id, + principalSchema: "werkr", + principalTable: "workflows", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "workflow_runs", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + workflow_id = table.Column(type: "bigint", nullable: false), + start_time = table.Column(type: "text", nullable: false), + end_time = table.Column(type: "text", nullable: true), + status = table.Column(type: "text", nullable: false), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_runs", x => x.id); + table.ForeignKey( + name: "fk_workflow_runs_workflows_workflow_id", + column: x => x.workflow_id, + principalSchema: "werkr", + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "workflow_schedules", + schema: "werkr", + columns: table => new + { + workflow_id = table.Column(type: "bigint", nullable: false), + schedule_id = table.Column(type: "uuid", nullable: false), + created_at_utc = table.Column(type: "text", nullable: false), + is_one_time = table.Column(type: "boolean", nullable: false), + workflow_run_id = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_schedules", x => new { x.workflow_id, x.schedule_id }); + table.ForeignKey( + name: "fk_workflow_schedules_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_workflow_schedules_workflows_workflow_id", + column: x => x.workflow_id, + principalSchema: "werkr", + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "workflow_variables", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + workflow_id = table.Column(type: "bigint", nullable: false), + name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + default_value = table.Column(type: "text", nullable: true), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_variables", x => x.id); + table.ForeignKey( + name: "fk_workflow_variables_workflows_workflow_id", + column: x => x.workflow_id, + principalSchema: "werkr", + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "holiday_dates", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn), + holiday_calendar_id = table.Column(type: "uuid", nullable: false), + holiday_rule_id = table.Column(type: "bigint", nullable: true), + date = table.Column(type: "date", nullable: false), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + year = table.Column(type: "integer", nullable: false), + window_start = table.Column(type: "time without time zone", nullable: true), + window_end = table.Column(type: "time without time zone", nullable: true), + window_time_zone_id = table.Column(type: "character varying(128)", maxLength: 128, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_holiday_dates", x => x.id); + table.ForeignKey( + name: "fk_holiday_dates_holiday_calendars_holiday_calendar_id", + column: x => x.holiday_calendar_id, + principalSchema: "werkr", + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_holiday_dates_holiday_rules_holiday_rule_id", + column: x => x.holiday_rule_id, + principalSchema: "werkr", + principalTable: "holiday_rules", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "task_schedules", + schema: "werkr", + columns: table => new + { + task_id = table.Column(type: "bigint", nullable: false), + schedule_id = table.Column(type: "uuid", nullable: false), + created_at_utc = table.Column(type: "text", nullable: false), + is_one_time = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_task_schedules", x => new { x.task_id, x.schedule_id }); + table.ForeignKey( + name: "fk_task_schedules_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_task_schedules_tasks_task_id", + column: x => x.task_id, + principalSchema: "werkr", + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "workflow_steps", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + workflow_id = table.Column(type: "bigint", nullable: false), + task_id = table.Column(type: "bigint", nullable: false), + order = table.Column(type: "integer", nullable: false), + control_statement = table.Column(type: "text", nullable: false), + condition_expression = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + max_iterations = table.Column(type: "integer", nullable: false), + agent_connection_id_override = table.Column(type: "uuid", nullable: true), + dependency_mode = table.Column(type: "text", nullable: false), + input_variable_name = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + output_variable_name = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_steps", x => x.id); + table.ForeignKey( + name: "fk_workflow_steps_registered_connections_agent_connection_id_o", + column: x => x.agent_connection_id_override, + principalSchema: "werkr", + principalTable: "registered_connections", + principalColumn: "id"); + table.ForeignKey( + name: "fk_workflow_steps_tasks_task_id", + column: x => x.task_id, + principalSchema: "werkr", + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_workflow_steps_workflows_workflow_id", + column: x => x.workflow_id, + principalSchema: "werkr", + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "jobs", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + task_id = table.Column(type: "bigint", nullable: false), + task_snapshot = table.Column(type: "character varying(8000)", maxLength: 8000, nullable: false), + runtime_seconds = table.Column(type: "double precision", nullable: false), + start_time = table.Column(type: "text", nullable: false), + end_time = table.Column(type: "text", nullable: true), + success = table.Column(type: "boolean", nullable: false), + agent_connection_id = table.Column(type: "uuid", nullable: true), + exit_code = table.Column(type: "integer", nullable: true), + error_category = table.Column(type: "text", nullable: false), + output = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), + output_path = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), + workflow_run_id = table.Column(type: "uuid", nullable: true), + schedule_id = table.Column(type: "uuid", nullable: true), + created = table.Column(type: "text", nullable: false), + last_updated = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_jobs", x => x.id); + table.ForeignKey( + name: "fk_jobs_registered_connections_agent_connection_id", + column: x => x.agent_connection_id, + principalSchema: "werkr", + principalTable: "registered_connections", + principalColumn: "id"); + table.ForeignKey( + name: "fk_jobs_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id"); + table.ForeignKey( + name: "fk_jobs_tasks_task_id", + column: x => x.task_id, + principalSchema: "werkr", + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_jobs_workflow_runs_workflow_run_id", + column: x => x.workflow_run_id, + principalSchema: "werkr", + principalTable: "workflow_runs", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "workflow_step_dependencies", + schema: "werkr", + columns: table => new + { + step_id = table.Column(type: "bigint", nullable: false), + depends_on_step_id = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_step_dependencies", x => new { x.step_id, x.depends_on_step_id }); + table.ForeignKey( + name: "fk_workflow_step_dependencies_workflow_steps_depends_on_step_id", + column: x => x.depends_on_step_id, + principalSchema: "werkr", + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_workflow_step_dependencies_workflow_steps_step_id", + column: x => x.step_id, + principalSchema: "werkr", + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "workflow_run_variables", + schema: "werkr", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + workflow_run_id = table.Column(type: "uuid", nullable: false), + variable_name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), + value = table.Column(type: "text", nullable: false), + version = table.Column(type: "integer", nullable: false), + produced_by_step_id = table.Column(type: "bigint", nullable: true), + produced_by_job_id = table.Column(type: "uuid", nullable: true), + source = table.Column(type: "text", nullable: false), + created = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_run_variables", x => x.id); + table.ForeignKey( + name: "fk_workflow_run_variables_jobs_produced_by_job_id", + column: x => x.produced_by_job_id, + principalSchema: "werkr", + principalTable: "jobs", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "fk_workflow_run_variables_workflow_runs_workflow_run_id", + column: x => x.workflow_run_id, + principalSchema: "werkr", + principalTable: "workflow_runs", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_workflow_run_variables_workflow_steps_produced_by_step_id", + column: x => x.produced_by_step_id, + principalSchema: "werkr", + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "ix_holiday_calendars_name", + schema: "werkr", + table: "holiday_calendars", + column: "name", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_holiday_dates_holiday_calendar_id_date", + schema: "werkr", + table: "holiday_dates", + columns: new[] { "holiday_calendar_id", "date" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_holiday_dates_holiday_rule_id", + schema: "werkr", + table: "holiday_dates", + column: "holiday_rule_id"); + + migrationBuilder.CreateIndex( + name: "ix_holiday_rules_holiday_calendar_id", + schema: "werkr", + table: "holiday_rules", + column: "holiday_calendar_id"); + + migrationBuilder.CreateIndex( + name: "ix_jobs_agent_connection_id", + schema: "werkr", + table: "jobs", + column: "agent_connection_id"); + + migrationBuilder.CreateIndex( + name: "ix_jobs_schedule_id", + schema: "werkr", + table: "jobs", + column: "schedule_id"); + + migrationBuilder.CreateIndex( + name: "ix_jobs_task_id", + schema: "werkr", + table: "jobs", + column: "task_id"); + + migrationBuilder.CreateIndex( + name: "ix_jobs_workflow_run_id", + schema: "werkr", + table: "jobs", + column: "workflow_run_id"); + + migrationBuilder.CreateIndex( + name: "ix_registered_connections_connection_name", + schema: "werkr", + table: "registered_connections", + column: "connection_name"); + + migrationBuilder.CreateIndex( + name: "ix_registered_connections_remote_url", + schema: "werkr", + table: "registered_connections", + column: "remote_url"); + + migrationBuilder.CreateIndex( + name: "ix_registration_bundles_bundle_id", + schema: "werkr", + table: "registration_bundles", + column: "bundle_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_schedule_audit_log_schedule_id_occurrence_utc_time", + schema: "werkr", + table: "schedule_audit_log", + columns: new[] { "schedule_id", "occurrence_utc_time" }); + + migrationBuilder.CreateIndex( + name: "ix_schedule_holiday_calendars_holiday_calendar_id", + schema: "werkr", + table: "schedule_holiday_calendars", + column: "holiday_calendar_id"); + + migrationBuilder.CreateIndex( + name: "ix_schedule_holiday_calendars_schedule_id", + schema: "werkr", + table: "schedule_holiday_calendars", + column: "schedule_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_task_schedules_schedule_id", + schema: "werkr", + table: "task_schedules", + column: "schedule_id"); + + migrationBuilder.CreateIndex( + name: "ix_tasks_workflow_id", + schema: "werkr", + table: "tasks", + column: "workflow_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_produced_by_job_id", + schema: "werkr", + table: "workflow_run_variables", + column: "produced_by_job_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_produced_by_step_id", + schema: "werkr", + table: "workflow_run_variables", + column: "produced_by_step_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_workflow_run_id_variable_name_version", + schema: "werkr", + table: "workflow_run_variables", + columns: new[] { "workflow_run_id", "variable_name", "version" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_workflow_runs_workflow_id", + schema: "werkr", + table: "workflow_runs", + column: "workflow_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_schedules_schedule_id", + schema: "werkr", + table: "workflow_schedules", + column: "schedule_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_step_dependencies_depends_on_step_id", + schema: "werkr", + table: "workflow_step_dependencies", + column: "depends_on_step_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_steps_agent_connection_id_override", + schema: "werkr", + table: "workflow_steps", + column: "agent_connection_id_override"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_steps_task_id", + schema: "werkr", + table: "workflow_steps", + column: "task_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_steps_workflow_id", + schema: "werkr", + table: "workflow_steps", + column: "workflow_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_variables_workflow_id_name", + schema: "werkr", + table: "workflow_variables", + columns: new[] { "workflow_id", "name" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "daily_recurrence", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "holiday_dates", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "monthly_recurrence", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "registration_bundles", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "schedule_audit_log", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "schedule_expiration", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "schedule_holiday_calendars", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "schedule_repeat_options", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "schedule_start_datetimeinfo", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "task_schedules", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "weekly_recurrence", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "workflow_run_variables", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "workflow_schedules", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "workflow_step_dependencies", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "workflow_variables", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "holiday_rules", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "jobs", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "workflow_steps", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "holiday_calendars", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "schedules", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "workflow_runs", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "registered_connections", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "tasks", + schema: "werkr"); + + migrationBuilder.DropTable( + name: "workflows", + schema: "werkr"); + } + } +} diff --git a/src/Werkr.Data/Migrations/Postgres/PostgresWerkrDbContextModelSnapshot.cs b/src/Werkr.Data/Migrations/Postgres/PostgresWerkrDbContextModelSnapshot.cs index b54f4ea..2604fe7 100644 --- a/src/Werkr.Data/Migrations/Postgres/PostgresWerkrDbContextModelSnapshot.cs +++ b/src/Werkr.Data/Migrations/Postgres/PostgresWerkrDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("werkr") - .HasAnnotation("ProductVersion", "10.0.3") + .HasAnnotation("ProductVersion", "10.0.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -997,6 +997,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(256)") .HasColumnName("name"); + b.Property("TargetTags") + .HasColumnType("text") + .HasColumnName("target_tags"); + b.Property("Version") .IsConcurrencyToken() .HasColumnType("integer") diff --git a/src/Werkr.Data/Migrations/Sqlite/20260310023534_InitialCreate.cs b/src/Werkr.Data/Migrations/Sqlite/20260310023534_InitialCreate.cs deleted file mode 100644 index f420b27..0000000 --- a/src/Werkr.Data/Migrations/Sqlite/20260310023534_InitialCreate.cs +++ /dev/null @@ -1,814 +0,0 @@ -#nullable disable - -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Werkr.Data.Migrations.Sqlite; - -/// -public partial class InitialCreate : Migration { - /// - protected override void Up( MigrationBuilder migrationBuilder ) { - _ = migrationBuilder.CreateTable( - name: "holiday_calendars", - columns: table => new { - id = table.Column( type: "TEXT", nullable: false ), - name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), - description = table.Column( type: "TEXT", maxLength: 1024, nullable: false ), - is_system_calendar = table.Column( type: "INTEGER", nullable: false ), - created_utc = table.Column( type: "TEXT", nullable: false ), - updated_utc = table.Column( type: "TEXT", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_holiday_calendars", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "registered_connections", - columns: table => new { - id = table.Column( type: "TEXT", nullable: false ), - connection_name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), - remote_url = table.Column( type: "TEXT", maxLength: 2048, nullable: false ), - local_public_key = table.Column( type: "TEXT", nullable: false ), - local_private_key = table.Column( type: "TEXT", nullable: false ), - remote_public_key = table.Column( type: "TEXT", nullable: false ), - outbound_api_key = table.Column( type: "TEXT", maxLength: 512, nullable: false ), - inbound_api_key_hash = table.Column( type: "TEXT", maxLength: 512, nullable: false ), - shared_key = table.Column( type: "TEXT", nullable: false ), - previous_shared_key = table.Column( type: "TEXT", nullable: true ), - active_key_id = table.Column( type: "TEXT", maxLength: 128, nullable: true ), - previous_key_id = table.Column( type: "TEXT", maxLength: 128, nullable: true ), - is_server = table.Column( type: "INTEGER", nullable: false ), - status = table.Column( type: "TEXT", nullable: false ), - last_seen = table.Column( type: "TEXT", nullable: true ), - tags = table.Column( type: "TEXT", nullable: false ), - allowed_paths = table.Column( type: "TEXT", nullable: false ), - enforce_allowlist = table.Column( type: "INTEGER", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_registered_connections", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "registration_bundles", - columns: table => new { - id = table.Column( type: "TEXT", nullable: false ), - connection_name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), - server_public_key = table.Column( type: "TEXT", nullable: false ), - server_private_key = table.Column( type: "TEXT", nullable: false ), - bundle_id = table.Column( type: "TEXT", nullable: false ), - status = table.Column( type: "TEXT", nullable: false ), - expires_at = table.Column( type: "TEXT", nullable: false ), - key_size = table.Column( type: "INTEGER", nullable: false ), - tags = table.Column( type: "TEXT", nullable: false ), - allowed_paths = table.Column( type: "TEXT", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_registration_bundles", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedules", - columns: table => new { - id = table.Column( type: "TEXT", nullable: false ), - name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), - stop_task_after_minutes = table.Column( type: "INTEGER", nullable: false ), - catch_up_enabled = table.Column( type: "INTEGER", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedules", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflows", - columns: table => new { - id = table.Column( type: "INTEGER", nullable: false ) - .Annotation( "Sqlite:Autoincrement", true ), - name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), - description = table.Column( type: "TEXT", maxLength: 2000, nullable: false ), - enabled = table.Column( type: "INTEGER", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflows", x => x.id ); - } ); - - _ = migrationBuilder.CreateTable( - name: "holiday_rules", - columns: table => new { - id = table.Column( type: "INTEGER", nullable: false ) - .Annotation( "Sqlite:Autoincrement", true ), - holiday_calendar_id = table.Column( type: "TEXT", nullable: false ), - name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), - rule_type = table.Column( type: "TEXT", nullable: false ), - month = table.Column( type: "INTEGER", nullable: true ), - day = table.Column( type: "INTEGER", nullable: true ), - day_of_week = table.Column( type: "INTEGER", nullable: true ), - week_number = table.Column( type: "INTEGER", nullable: true ), - window_start = table.Column( type: "TEXT", nullable: true ), - window_end = table.Column( type: "TEXT", nullable: true ), - window_time_zone_id = table.Column( type: "TEXT", maxLength: 128, nullable: true ), - observance_rule = table.Column( type: "TEXT", nullable: false ), - year_start = table.Column( type: "INTEGER", nullable: true ), - year_end = table.Column( type: "INTEGER", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_holiday_rules", x => x.id ); - _ = table.ForeignKey( - name: "fk_holiday_rules_holiday_calendars_holiday_calendar_id", - column: x => x.holiday_calendar_id, - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "daily_recurrence", - columns: table => new { - schedule_id = table.Column( type: "TEXT", nullable: false ), - day_interval = table.Column( type: "INTEGER", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_daily_recurrence", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_daily_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "monthly_recurrence", - columns: table => new { - schedule_id = table.Column( type: "TEXT", nullable: false ), - day_numbers = table.Column( type: "TEXT", nullable: true ), - months_of_year = table.Column( type: "INTEGER", nullable: false ), - week_number = table.Column( type: "INTEGER", nullable: true ), - days_of_week = table.Column( type: "INTEGER", nullable: true ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_monthly_recurrence", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_monthly_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedule_audit_log", - columns: table => new { - id = table.Column( type: "INTEGER", nullable: false ) - .Annotation( "Sqlite:Autoincrement", true ), - schedule_id = table.Column( type: "TEXT", nullable: false ), - occurrence_utc_time = table.Column( type: "TEXT", nullable: false ), - calendar_name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), - holiday_name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), - mode = table.Column( type: "TEXT", nullable: false ), - created_utc = table.Column( type: "TEXT", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedule_audit_log", x => x.id ); - _ = table.ForeignKey( - name: "fk_schedule_audit_log_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedule_expiration", - columns: table => new { - schedule_id = table.Column( type: "TEXT", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ), - date = table.Column( type: "TEXT", nullable: false ), - time = table.Column( type: "TEXT", nullable: false ), - time_zone = table.Column( type: "TEXT", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedule_expiration", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_schedule_expiration_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedule_holiday_calendars", - columns: table => new { - schedule_id = table.Column( type: "TEXT", nullable: false ), - holiday_calendar_id = table.Column( type: "TEXT", nullable: false ), - mode = table.Column( type: "TEXT", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedule_holiday_calendars", x => new { x.schedule_id, x.holiday_calendar_id } ); - _ = table.ForeignKey( - name: "fk_schedule_holiday_calendars_holiday_calendars_holiday_calendar_id", - column: x => x.holiday_calendar_id, - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_schedule_holiday_calendars_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedule_repeat_options", - columns: table => new { - schedule_id = table.Column( type: "TEXT", nullable: false ), - repeat_interval_minutes = table.Column( type: "INTEGER", nullable: false ), - repeat_duration_minutes = table.Column( type: "INTEGER", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedule_repeat_options", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_schedule_repeat_options_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "schedule_start_datetimeinfo", - columns: table => new { - schedule_id = table.Column( type: "TEXT", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ), - date = table.Column( type: "TEXT", nullable: false ), - time = table.Column( type: "TEXT", nullable: false ), - time_zone = table.Column( type: "TEXT", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_schedule_start_datetimeinfo", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_schedule_start_datetimeinfo_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "weekly_recurrence", - columns: table => new { - schedule_id = table.Column( type: "TEXT", nullable: false ), - week_interval = table.Column( type: "INTEGER", nullable: false ), - days_of_week = table.Column( type: "INTEGER", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_weekly_recurrence", x => x.schedule_id ); - _ = table.ForeignKey( - name: "fk_weekly_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "tasks", - columns: table => new { - id = table.Column( type: "INTEGER", nullable: false ) - .Annotation( "Sqlite:Autoincrement", true ), - name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), - description = table.Column( type: "TEXT", maxLength: 2000, nullable: false ), - action_type = table.Column( type: "TEXT", nullable: false ), - workflow_id = table.Column( type: "INTEGER", nullable: true ), - content = table.Column( type: "TEXT", maxLength: 8000, nullable: false ), - arguments = table.Column( type: "TEXT", nullable: true ), - target_tags = table.Column( type: "TEXT", nullable: false ), - enabled = table.Column( type: "INTEGER", nullable: false ), - is_ephemeral = table.Column( type: "INTEGER", nullable: false ), - timeout_minutes = table.Column( type: "INTEGER", nullable: true ), - sync_interval_minutes = table.Column( type: "INTEGER", nullable: false ), - success_criteria = table.Column( type: "TEXT", maxLength: 500, nullable: true ), - action_sub_type = table.Column( type: "TEXT", maxLength: 30, nullable: true ), - action_parameters = table.Column( type: "TEXT", nullable: true ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_tasks", x => x.id ); - _ = table.ForeignKey( - name: "fk_tasks_workflows_workflow_id", - column: x => x.workflow_id, - principalTable: "workflows", - principalColumn: "id" ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_runs", - columns: table => new { - id = table.Column( type: "TEXT", nullable: false ), - workflow_id = table.Column( type: "INTEGER", nullable: false ), - start_time = table.Column( type: "TEXT", nullable: false ), - end_time = table.Column( type: "TEXT", nullable: true ), - status = table.Column( type: "TEXT", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_runs", x => x.id ); - _ = table.ForeignKey( - name: "fk_workflow_runs_workflows_workflow_id", - column: x => x.workflow_id, - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_schedules", - columns: table => new { - workflow_id = table.Column( type: "INTEGER", nullable: false ), - schedule_id = table.Column( type: "TEXT", nullable: false ), - created_at_utc = table.Column( type: "TEXT", nullable: false ), - is_one_time = table.Column( type: "INTEGER", nullable: false ), - workflow_run_id = table.Column( type: "TEXT", nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_schedules", x => new { x.workflow_id, x.schedule_id } ); - _ = table.ForeignKey( - name: "fk_workflow_schedules_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_workflow_schedules_workflows_workflow_id", - column: x => x.workflow_id, - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_variables", - columns: table => new { - id = table.Column( type: "INTEGER", nullable: false ) - .Annotation( "Sqlite:Autoincrement", true ), - workflow_id = table.Column( type: "INTEGER", nullable: false ), - name = table.Column( type: "TEXT", maxLength: 128, nullable: false ), - description = table.Column( type: "TEXT", maxLength: 500, nullable: true ), - default_value = table.Column( type: "TEXT", nullable: true ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_variables", x => x.id ); - _ = table.ForeignKey( - name: "fk_workflow_variables_workflows_workflow_id", - column: x => x.workflow_id, - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "holiday_dates", - columns: table => new { - id = table.Column( type: "INTEGER", nullable: false ) - .Annotation( "Sqlite:Autoincrement", true ), - holiday_calendar_id = table.Column( type: "TEXT", nullable: false ), - holiday_rule_id = table.Column( type: "INTEGER", nullable: true ), - date = table.Column( type: "TEXT", nullable: false ), - name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), - year = table.Column( type: "INTEGER", nullable: false ), - window_start = table.Column( type: "TEXT", nullable: true ), - window_end = table.Column( type: "TEXT", nullable: true ), - window_time_zone_id = table.Column( type: "TEXT", maxLength: 128, nullable: true ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_holiday_dates", x => x.id ); - _ = table.ForeignKey( - name: "fk_holiday_dates_holiday_calendars_holiday_calendar_id", - column: x => x.holiday_calendar_id, - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_holiday_dates_holiday_rules_holiday_rule_id", - column: x => x.holiday_rule_id, - principalTable: "holiday_rules", - principalColumn: "id", - onDelete: ReferentialAction.SetNull ); - } ); - - _ = migrationBuilder.CreateTable( - name: "task_schedules", - columns: table => new { - task_id = table.Column( type: "INTEGER", nullable: false ), - schedule_id = table.Column( type: "TEXT", nullable: false ), - created_at_utc = table.Column( type: "TEXT", nullable: false ), - is_one_time = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_task_schedules", x => new { x.task_id, x.schedule_id } ); - _ = table.ForeignKey( - name: "fk_task_schedules_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_task_schedules_tasks_task_id", - column: x => x.task_id, - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_steps", - columns: table => new { - id = table.Column( type: "INTEGER", nullable: false ) - .Annotation( "Sqlite:Autoincrement", true ), - workflow_id = table.Column( type: "INTEGER", nullable: false ), - task_id = table.Column( type: "INTEGER", nullable: false ), - order = table.Column( type: "INTEGER", nullable: false ), - control_statement = table.Column( type: "TEXT", nullable: false ), - condition_expression = table.Column( type: "TEXT", maxLength: 2000, nullable: true ), - max_iterations = table.Column( type: "INTEGER", nullable: false ), - agent_connection_id_override = table.Column( type: "TEXT", nullable: true ), - dependency_mode = table.Column( type: "TEXT", nullable: false ), - input_variable_name = table.Column( type: "TEXT", maxLength: 128, nullable: true ), - output_variable_name = table.Column( type: "TEXT", maxLength: 128, nullable: true ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_steps", x => x.id ); - _ = table.ForeignKey( - name: "fk_workflow_steps_registered_connections_agent_connection_id_override", - column: x => x.agent_connection_id_override, - principalTable: "registered_connections", - principalColumn: "id" ); - _ = table.ForeignKey( - name: "fk_workflow_steps_tasks_task_id", - column: x => x.task_id, - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_workflow_steps_workflows_workflow_id", - column: x => x.workflow_id, - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "jobs", - columns: table => new { - id = table.Column( type: "TEXT", nullable: false ), - task_id = table.Column( type: "INTEGER", nullable: false ), - task_snapshot = table.Column( type: "TEXT", maxLength: 8000, nullable: false ), - runtime_seconds = table.Column( type: "REAL", nullable: false ), - start_time = table.Column( type: "TEXT", nullable: false ), - end_time = table.Column( type: "TEXT", nullable: true ), - success = table.Column( type: "INTEGER", nullable: false ), - agent_connection_id = table.Column( type: "TEXT", nullable: true ), - exit_code = table.Column( type: "INTEGER", nullable: true ), - error_category = table.Column( type: "TEXT", nullable: false ), - output = table.Column( type: "TEXT", maxLength: 2000, nullable: true ), - output_path = table.Column( type: "TEXT", maxLength: 512, nullable: true ), - workflow_run_id = table.Column( type: "TEXT", nullable: true ), - schedule_id = table.Column( type: "TEXT", nullable: true ), - created = table.Column( type: "TEXT", nullable: false ), - last_updated = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_jobs", x => x.id ); - _ = table.ForeignKey( - name: "fk_jobs_registered_connections_agent_connection_id", - column: x => x.agent_connection_id, - principalTable: "registered_connections", - principalColumn: "id" ); - _ = table.ForeignKey( - name: "fk_jobs_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id" ); - _ = table.ForeignKey( - name: "fk_jobs_tasks_task_id", - column: x => x.task_id, - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_jobs_workflow_runs_workflow_run_id", - column: x => x.workflow_run_id, - principalTable: "workflow_runs", - principalColumn: "id" ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_step_dependencies", - columns: table => new { - step_id = table.Column( type: "INTEGER", nullable: false ), - depends_on_step_id = table.Column( type: "INTEGER", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_step_dependencies", x => new { x.step_id, x.depends_on_step_id } ); - _ = table.ForeignKey( - name: "fk_workflow_step_dependencies_workflow_steps_depends_on_step_id", - column: x => x.depends_on_step_id, - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.Restrict ); - _ = table.ForeignKey( - name: "fk_workflow_step_dependencies_workflow_steps_step_id", - column: x => x.step_id, - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - } ); - - _ = migrationBuilder.CreateTable( - name: "workflow_run_variables", - columns: table => new { - id = table.Column( type: "INTEGER", nullable: false ) - .Annotation( "Sqlite:Autoincrement", true ), - workflow_run_id = table.Column( type: "TEXT", nullable: false ), - variable_name = table.Column( type: "TEXT", maxLength: 128, nullable: false ), - value = table.Column( type: "TEXT", nullable: false ), - version = table.Column( type: "INTEGER", nullable: false ), - produced_by_step_id = table.Column( type: "INTEGER", nullable: true ), - produced_by_job_id = table.Column( type: "TEXT", nullable: true ), - source = table.Column( type: "TEXT", nullable: false ), - created = table.Column( type: "TEXT", nullable: false ) - }, - constraints: table => { - _ = table.PrimaryKey( "pk_workflow_run_variables", x => x.id ); - _ = table.ForeignKey( - name: "fk_workflow_run_variables_jobs_produced_by_job_id", - column: x => x.produced_by_job_id, - principalTable: "jobs", - principalColumn: "id", - onDelete: ReferentialAction.SetNull ); - _ = table.ForeignKey( - name: "fk_workflow_run_variables_workflow_runs_workflow_run_id", - column: x => x.workflow_run_id, - principalTable: "workflow_runs", - principalColumn: "id", - onDelete: ReferentialAction.Cascade ); - _ = table.ForeignKey( - name: "fk_workflow_run_variables_workflow_steps_produced_by_step_id", - column: x => x.produced_by_step_id, - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.SetNull ); - } ); - - _ = migrationBuilder.CreateIndex( - name: "ix_holiday_calendars_name", - table: "holiday_calendars", - column: "name", - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_holiday_dates_holiday_calendar_id_date", - table: "holiday_dates", - columns: ["holiday_calendar_id", "date"], - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_holiday_dates_holiday_rule_id", - table: "holiday_dates", - column: "holiday_rule_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_holiday_rules_holiday_calendar_id", - table: "holiday_rules", - column: "holiday_calendar_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_jobs_agent_connection_id", - table: "jobs", - column: "agent_connection_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_jobs_schedule_id", - table: "jobs", - column: "schedule_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_jobs_task_id", - table: "jobs", - column: "task_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_jobs_workflow_run_id", - table: "jobs", - column: "workflow_run_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_registered_connections_connection_name", - table: "registered_connections", - column: "connection_name" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_registered_connections_remote_url", - table: "registered_connections", - column: "remote_url" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_registration_bundles_bundle_id", - table: "registration_bundles", - column: "bundle_id", - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_schedule_audit_log_schedule_id_occurrence_utc_time", - table: "schedule_audit_log", - columns: ["schedule_id", "occurrence_utc_time"] ); - - _ = migrationBuilder.CreateIndex( - name: "ix_schedule_holiday_calendars_holiday_calendar_id", - table: "schedule_holiday_calendars", - column: "holiday_calendar_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_schedule_holiday_calendars_schedule_id", - table: "schedule_holiday_calendars", - column: "schedule_id", - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_task_schedules_schedule_id", - table: "task_schedules", - column: "schedule_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_tasks_workflow_id", - table: "tasks", - column: "workflow_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_produced_by_job_id", - table: "workflow_run_variables", - column: "produced_by_job_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_produced_by_step_id", - table: "workflow_run_variables", - column: "produced_by_step_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_workflow_run_id_variable_name_version", - table: "workflow_run_variables", - columns: ["workflow_run_id", "variable_name", "version"], - unique: true ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_runs_workflow_id", - table: "workflow_runs", - column: "workflow_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_schedules_schedule_id", - table: "workflow_schedules", - column: "schedule_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_step_dependencies_depends_on_step_id", - table: "workflow_step_dependencies", - column: "depends_on_step_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_steps_agent_connection_id_override", - table: "workflow_steps", - column: "agent_connection_id_override" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_steps_task_id", - table: "workflow_steps", - column: "task_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_steps_workflow_id", - table: "workflow_steps", - column: "workflow_id" ); - - _ = migrationBuilder.CreateIndex( - name: "ix_workflow_variables_workflow_id_name", - table: "workflow_variables", - columns: ["workflow_id", "name"], - unique: true ); - } - - /// - protected override void Down( MigrationBuilder migrationBuilder ) { - _ = migrationBuilder.DropTable( - name: "daily_recurrence" ); - - _ = migrationBuilder.DropTable( - name: "holiday_dates" ); - - _ = migrationBuilder.DropTable( - name: "monthly_recurrence" ); - - _ = migrationBuilder.DropTable( - name: "registration_bundles" ); - - _ = migrationBuilder.DropTable( - name: "schedule_audit_log" ); - - _ = migrationBuilder.DropTable( - name: "schedule_expiration" ); - - _ = migrationBuilder.DropTable( - name: "schedule_holiday_calendars" ); - - _ = migrationBuilder.DropTable( - name: "schedule_repeat_options" ); - - _ = migrationBuilder.DropTable( - name: "schedule_start_datetimeinfo" ); - - _ = migrationBuilder.DropTable( - name: "task_schedules" ); - - _ = migrationBuilder.DropTable( - name: "weekly_recurrence" ); - - _ = migrationBuilder.DropTable( - name: "workflow_run_variables" ); - - _ = migrationBuilder.DropTable( - name: "workflow_schedules" ); - - _ = migrationBuilder.DropTable( - name: "workflow_step_dependencies" ); - - _ = migrationBuilder.DropTable( - name: "workflow_variables" ); - - _ = migrationBuilder.DropTable( - name: "holiday_rules" ); - - _ = migrationBuilder.DropTable( - name: "jobs" ); - - _ = migrationBuilder.DropTable( - name: "workflow_steps" ); - - _ = migrationBuilder.DropTable( - name: "holiday_calendars" ); - - _ = migrationBuilder.DropTable( - name: "schedules" ); - - _ = migrationBuilder.DropTable( - name: "workflow_runs" ); - - _ = migrationBuilder.DropTable( - name: "registered_connections" ); - - _ = migrationBuilder.DropTable( - name: "tasks" ); - - _ = migrationBuilder.DropTable( - name: "workflows" ); - } -} diff --git a/src/Werkr.Data/Migrations/Sqlite/20260310023534_InitialCreate.Designer.cs b/src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.Designer.cs similarity index 99% rename from src/Werkr.Data/Migrations/Sqlite/20260310023534_InitialCreate.Designer.cs rename to src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.Designer.cs index ff2265c..c0268ff 100644 --- a/src/Werkr.Data/Migrations/Sqlite/20260310023534_InitialCreate.Designer.cs +++ b/src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.Designer.cs @@ -12,14 +12,14 @@ namespace Werkr.Data.Migrations.Sqlite { [DbContext(typeof(SqliteWerkrDbContext))] - [Migration("20260310023534_InitialCreate")] + [Migration("20260312050748_InitialCreate")] partial class InitialCreate { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.3"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.4"); modelBuilder.Entity("Werkr.Data.Entities.Registration.RegisteredConnection", b => { @@ -988,6 +988,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasColumnName("name"); + b.Property("TargetTags") + .HasColumnType("TEXT") + .HasColumnName("target_tags"); + b.Property("Version") .IsConcurrencyToken() .HasColumnType("INTEGER") diff --git a/src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.cs b/src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.cs new file mode 100644 index 0000000..8424198 --- /dev/null +++ b/src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.cs @@ -0,0 +1,868 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Werkr.Data.Migrations.Sqlite +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "holiday_calendars", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + description = table.Column(type: "TEXT", maxLength: 1024, nullable: false), + is_system_calendar = table.Column(type: "INTEGER", nullable: false), + created_utc = table.Column(type: "TEXT", nullable: false), + updated_utc = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_holiday_calendars", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "registered_connections", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + connection_name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + remote_url = table.Column(type: "TEXT", maxLength: 2048, nullable: false), + local_public_key = table.Column(type: "TEXT", nullable: false), + local_private_key = table.Column(type: "TEXT", nullable: false), + remote_public_key = table.Column(type: "TEXT", nullable: false), + outbound_api_key = table.Column(type: "TEXT", maxLength: 512, nullable: false), + inbound_api_key_hash = table.Column(type: "TEXT", maxLength: 512, nullable: false), + shared_key = table.Column(type: "TEXT", nullable: false), + previous_shared_key = table.Column(type: "TEXT", nullable: true), + active_key_id = table.Column(type: "TEXT", maxLength: 128, nullable: true), + previous_key_id = table.Column(type: "TEXT", maxLength: 128, nullable: true), + is_server = table.Column(type: "INTEGER", nullable: false), + status = table.Column(type: "TEXT", nullable: false), + last_seen = table.Column(type: "TEXT", nullable: true), + tags = table.Column(type: "TEXT", nullable: false), + allowed_paths = table.Column(type: "TEXT", nullable: false), + enforce_allowlist = table.Column(type: "INTEGER", nullable: false), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_registered_connections", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "registration_bundles", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + connection_name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + server_public_key = table.Column(type: "TEXT", nullable: false), + server_private_key = table.Column(type: "TEXT", nullable: false), + bundle_id = table.Column(type: "TEXT", nullable: false), + status = table.Column(type: "TEXT", nullable: false), + expires_at = table.Column(type: "TEXT", nullable: false), + key_size = table.Column(type: "INTEGER", nullable: false), + tags = table.Column(type: "TEXT", nullable: false), + allowed_paths = table.Column(type: "TEXT", nullable: false), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_registration_bundles", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "schedules", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + stop_task_after_minutes = table.Column(type: "INTEGER", nullable: false), + catch_up_enabled = table.Column(type: "INTEGER", nullable: false), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedules", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "workflows", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + description = table.Column(type: "TEXT", maxLength: 2000, nullable: false), + enabled = table.Column(type: "INTEGER", nullable: false), + target_tags = table.Column(type: "TEXT", nullable: true), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflows", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "holiday_rules", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + holiday_calendar_id = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + rule_type = table.Column(type: "TEXT", nullable: false), + month = table.Column(type: "INTEGER", nullable: true), + day = table.Column(type: "INTEGER", nullable: true), + day_of_week = table.Column(type: "INTEGER", nullable: true), + week_number = table.Column(type: "INTEGER", nullable: true), + window_start = table.Column(type: "TEXT", nullable: true), + window_end = table.Column(type: "TEXT", nullable: true), + window_time_zone_id = table.Column(type: "TEXT", maxLength: 128, nullable: true), + observance_rule = table.Column(type: "TEXT", nullable: false), + year_start = table.Column(type: "INTEGER", nullable: true), + year_end = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_holiday_rules", x => x.id); + table.ForeignKey( + name: "fk_holiday_rules_holiday_calendars_holiday_calendar_id", + column: x => x.holiday_calendar_id, + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "daily_recurrence", + columns: table => new + { + schedule_id = table.Column(type: "TEXT", nullable: false), + day_interval = table.Column(type: "INTEGER", nullable: false), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_daily_recurrence", x => x.schedule_id); + table.ForeignKey( + name: "fk_daily_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "monthly_recurrence", + columns: table => new + { + schedule_id = table.Column(type: "TEXT", nullable: false), + day_numbers = table.Column(type: "TEXT", nullable: true), + months_of_year = table.Column(type: "INTEGER", nullable: false), + week_number = table.Column(type: "INTEGER", nullable: true), + days_of_week = table.Column(type: "INTEGER", nullable: true), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_monthly_recurrence", x => x.schedule_id); + table.ForeignKey( + name: "fk_monthly_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "schedule_audit_log", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + schedule_id = table.Column(type: "TEXT", nullable: false), + occurrence_utc_time = table.Column(type: "TEXT", nullable: false), + calendar_name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + holiday_name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + mode = table.Column(type: "TEXT", nullable: false), + created_utc = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedule_audit_log", x => x.id); + table.ForeignKey( + name: "fk_schedule_audit_log_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "schedule_expiration", + columns: table => new + { + schedule_id = table.Column(type: "TEXT", nullable: false), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false), + date = table.Column(type: "TEXT", nullable: false), + time = table.Column(type: "TEXT", nullable: false), + time_zone = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedule_expiration", x => x.schedule_id); + table.ForeignKey( + name: "fk_schedule_expiration_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "schedule_holiday_calendars", + columns: table => new + { + schedule_id = table.Column(type: "TEXT", nullable: false), + holiday_calendar_id = table.Column(type: "TEXT", nullable: false), + mode = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedule_holiday_calendars", x => new { x.schedule_id, x.holiday_calendar_id }); + table.ForeignKey( + name: "fk_schedule_holiday_calendars_holiday_calendars_holiday_calendar_id", + column: x => x.holiday_calendar_id, + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_schedule_holiday_calendars_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "schedule_repeat_options", + columns: table => new + { + schedule_id = table.Column(type: "TEXT", nullable: false), + repeat_interval_minutes = table.Column(type: "INTEGER", nullable: false), + repeat_duration_minutes = table.Column(type: "INTEGER", nullable: false), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedule_repeat_options", x => x.schedule_id); + table.ForeignKey( + name: "fk_schedule_repeat_options_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "schedule_start_datetimeinfo", + columns: table => new + { + schedule_id = table.Column(type: "TEXT", nullable: false), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false), + date = table.Column(type: "TEXT", nullable: false), + time = table.Column(type: "TEXT", nullable: false), + time_zone = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_schedule_start_datetimeinfo", x => x.schedule_id); + table.ForeignKey( + name: "fk_schedule_start_datetimeinfo_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "weekly_recurrence", + columns: table => new + { + schedule_id = table.Column(type: "TEXT", nullable: false), + week_interval = table.Column(type: "INTEGER", nullable: false), + days_of_week = table.Column(type: "INTEGER", nullable: false), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_weekly_recurrence", x => x.schedule_id); + table.ForeignKey( + name: "fk_weekly_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "tasks", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + description = table.Column(type: "TEXT", maxLength: 2000, nullable: false), + action_type = table.Column(type: "TEXT", nullable: false), + workflow_id = table.Column(type: "INTEGER", nullable: true), + content = table.Column(type: "TEXT", maxLength: 8000, nullable: false), + arguments = table.Column(type: "TEXT", nullable: true), + target_tags = table.Column(type: "TEXT", nullable: false), + enabled = table.Column(type: "INTEGER", nullable: false), + is_ephemeral = table.Column(type: "INTEGER", nullable: false), + timeout_minutes = table.Column(type: "INTEGER", nullable: true), + sync_interval_minutes = table.Column(type: "INTEGER", nullable: false), + success_criteria = table.Column(type: "TEXT", maxLength: 500, nullable: true), + action_sub_type = table.Column(type: "TEXT", maxLength: 30, nullable: true), + action_parameters = table.Column(type: "TEXT", nullable: true), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tasks", x => x.id); + table.ForeignKey( + name: "fk_tasks_workflows_workflow_id", + column: x => x.workflow_id, + principalTable: "workflows", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "workflow_runs", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + workflow_id = table.Column(type: "INTEGER", nullable: false), + start_time = table.Column(type: "TEXT", nullable: false), + end_time = table.Column(type: "TEXT", nullable: true), + status = table.Column(type: "TEXT", nullable: false), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_runs", x => x.id); + table.ForeignKey( + name: "fk_workflow_runs_workflows_workflow_id", + column: x => x.workflow_id, + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "workflow_schedules", + columns: table => new + { + workflow_id = table.Column(type: "INTEGER", nullable: false), + schedule_id = table.Column(type: "TEXT", nullable: false), + created_at_utc = table.Column(type: "TEXT", nullable: false), + is_one_time = table.Column(type: "INTEGER", nullable: false), + workflow_run_id = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_schedules", x => new { x.workflow_id, x.schedule_id }); + table.ForeignKey( + name: "fk_workflow_schedules_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_workflow_schedules_workflows_workflow_id", + column: x => x.workflow_id, + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "workflow_variables", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + workflow_id = table.Column(type: "INTEGER", nullable: false), + name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + default_value = table.Column(type: "TEXT", nullable: true), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_variables", x => x.id); + table.ForeignKey( + name: "fk_workflow_variables_workflows_workflow_id", + column: x => x.workflow_id, + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "holiday_dates", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + holiday_calendar_id = table.Column(type: "TEXT", nullable: false), + holiday_rule_id = table.Column(type: "INTEGER", nullable: true), + date = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "TEXT", maxLength: 256, nullable: false), + year = table.Column(type: "INTEGER", nullable: false), + window_start = table.Column(type: "TEXT", nullable: true), + window_end = table.Column(type: "TEXT", nullable: true), + window_time_zone_id = table.Column(type: "TEXT", maxLength: 128, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_holiday_dates", x => x.id); + table.ForeignKey( + name: "fk_holiday_dates_holiday_calendars_holiday_calendar_id", + column: x => x.holiday_calendar_id, + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_holiday_dates_holiday_rules_holiday_rule_id", + column: x => x.holiday_rule_id, + principalTable: "holiday_rules", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "task_schedules", + columns: table => new + { + task_id = table.Column(type: "INTEGER", nullable: false), + schedule_id = table.Column(type: "TEXT", nullable: false), + created_at_utc = table.Column(type: "TEXT", nullable: false), + is_one_time = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_task_schedules", x => new { x.task_id, x.schedule_id }); + table.ForeignKey( + name: "fk_task_schedules_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_task_schedules_tasks_task_id", + column: x => x.task_id, + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "workflow_steps", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + workflow_id = table.Column(type: "INTEGER", nullable: false), + task_id = table.Column(type: "INTEGER", nullable: false), + order = table.Column(type: "INTEGER", nullable: false), + control_statement = table.Column(type: "TEXT", nullable: false), + condition_expression = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + max_iterations = table.Column(type: "INTEGER", nullable: false), + agent_connection_id_override = table.Column(type: "TEXT", nullable: true), + dependency_mode = table.Column(type: "TEXT", nullable: false), + input_variable_name = table.Column(type: "TEXT", maxLength: 128, nullable: true), + output_variable_name = table.Column(type: "TEXT", maxLength: 128, nullable: true), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_steps", x => x.id); + table.ForeignKey( + name: "fk_workflow_steps_registered_connections_agent_connection_id_override", + column: x => x.agent_connection_id_override, + principalTable: "registered_connections", + principalColumn: "id"); + table.ForeignKey( + name: "fk_workflow_steps_tasks_task_id", + column: x => x.task_id, + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_workflow_steps_workflows_workflow_id", + column: x => x.workflow_id, + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "jobs", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + task_id = table.Column(type: "INTEGER", nullable: false), + task_snapshot = table.Column(type: "TEXT", maxLength: 8000, nullable: false), + runtime_seconds = table.Column(type: "REAL", nullable: false), + start_time = table.Column(type: "TEXT", nullable: false), + end_time = table.Column(type: "TEXT", nullable: true), + success = table.Column(type: "INTEGER", nullable: false), + agent_connection_id = table.Column(type: "TEXT", nullable: true), + exit_code = table.Column(type: "INTEGER", nullable: true), + error_category = table.Column(type: "TEXT", nullable: false), + output = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + output_path = table.Column(type: "TEXT", maxLength: 512, nullable: true), + workflow_run_id = table.Column(type: "TEXT", nullable: true), + schedule_id = table.Column(type: "TEXT", nullable: true), + created = table.Column(type: "TEXT", nullable: false), + last_updated = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_jobs", x => x.id); + table.ForeignKey( + name: "fk_jobs_registered_connections_agent_connection_id", + column: x => x.agent_connection_id, + principalTable: "registered_connections", + principalColumn: "id"); + table.ForeignKey( + name: "fk_jobs_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id"); + table.ForeignKey( + name: "fk_jobs_tasks_task_id", + column: x => x.task_id, + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_jobs_workflow_runs_workflow_run_id", + column: x => x.workflow_run_id, + principalTable: "workflow_runs", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "workflow_step_dependencies", + columns: table => new + { + step_id = table.Column(type: "INTEGER", nullable: false), + depends_on_step_id = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_step_dependencies", x => new { x.step_id, x.depends_on_step_id }); + table.ForeignKey( + name: "fk_workflow_step_dependencies_workflow_steps_depends_on_step_id", + column: x => x.depends_on_step_id, + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_workflow_step_dependencies_workflow_steps_step_id", + column: x => x.step_id, + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "workflow_run_variables", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + workflow_run_id = table.Column(type: "TEXT", nullable: false), + variable_name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + value = table.Column(type: "TEXT", nullable: false), + version = table.Column(type: "INTEGER", nullable: false), + produced_by_step_id = table.Column(type: "INTEGER", nullable: true), + produced_by_job_id = table.Column(type: "TEXT", nullable: true), + source = table.Column(type: "TEXT", nullable: false), + created = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_workflow_run_variables", x => x.id); + table.ForeignKey( + name: "fk_workflow_run_variables_jobs_produced_by_job_id", + column: x => x.produced_by_job_id, + principalTable: "jobs", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + table.ForeignKey( + name: "fk_workflow_run_variables_workflow_runs_workflow_run_id", + column: x => x.workflow_run_id, + principalTable: "workflow_runs", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_workflow_run_variables_workflow_steps_produced_by_step_id", + column: x => x.produced_by_step_id, + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "ix_holiday_calendars_name", + table: "holiday_calendars", + column: "name", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_holiday_dates_holiday_calendar_id_date", + table: "holiday_dates", + columns: new[] { "holiday_calendar_id", "date" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_holiday_dates_holiday_rule_id", + table: "holiday_dates", + column: "holiday_rule_id"); + + migrationBuilder.CreateIndex( + name: "ix_holiday_rules_holiday_calendar_id", + table: "holiday_rules", + column: "holiday_calendar_id"); + + migrationBuilder.CreateIndex( + name: "ix_jobs_agent_connection_id", + table: "jobs", + column: "agent_connection_id"); + + migrationBuilder.CreateIndex( + name: "ix_jobs_schedule_id", + table: "jobs", + column: "schedule_id"); + + migrationBuilder.CreateIndex( + name: "ix_jobs_task_id", + table: "jobs", + column: "task_id"); + + migrationBuilder.CreateIndex( + name: "ix_jobs_workflow_run_id", + table: "jobs", + column: "workflow_run_id"); + + migrationBuilder.CreateIndex( + name: "ix_registered_connections_connection_name", + table: "registered_connections", + column: "connection_name"); + + migrationBuilder.CreateIndex( + name: "ix_registered_connections_remote_url", + table: "registered_connections", + column: "remote_url"); + + migrationBuilder.CreateIndex( + name: "ix_registration_bundles_bundle_id", + table: "registration_bundles", + column: "bundle_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_schedule_audit_log_schedule_id_occurrence_utc_time", + table: "schedule_audit_log", + columns: new[] { "schedule_id", "occurrence_utc_time" }); + + migrationBuilder.CreateIndex( + name: "ix_schedule_holiday_calendars_holiday_calendar_id", + table: "schedule_holiday_calendars", + column: "holiday_calendar_id"); + + migrationBuilder.CreateIndex( + name: "ix_schedule_holiday_calendars_schedule_id", + table: "schedule_holiday_calendars", + column: "schedule_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_task_schedules_schedule_id", + table: "task_schedules", + column: "schedule_id"); + + migrationBuilder.CreateIndex( + name: "ix_tasks_workflow_id", + table: "tasks", + column: "workflow_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_produced_by_job_id", + table: "workflow_run_variables", + column: "produced_by_job_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_produced_by_step_id", + table: "workflow_run_variables", + column: "produced_by_step_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_workflow_run_id_variable_name_version", + table: "workflow_run_variables", + columns: new[] { "workflow_run_id", "variable_name", "version" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_workflow_runs_workflow_id", + table: "workflow_runs", + column: "workflow_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_schedules_schedule_id", + table: "workflow_schedules", + column: "schedule_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_step_dependencies_depends_on_step_id", + table: "workflow_step_dependencies", + column: "depends_on_step_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_steps_agent_connection_id_override", + table: "workflow_steps", + column: "agent_connection_id_override"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_steps_task_id", + table: "workflow_steps", + column: "task_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_steps_workflow_id", + table: "workflow_steps", + column: "workflow_id"); + + migrationBuilder.CreateIndex( + name: "ix_workflow_variables_workflow_id_name", + table: "workflow_variables", + columns: new[] { "workflow_id", "name" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "daily_recurrence"); + + migrationBuilder.DropTable( + name: "holiday_dates"); + + migrationBuilder.DropTable( + name: "monthly_recurrence"); + + migrationBuilder.DropTable( + name: "registration_bundles"); + + migrationBuilder.DropTable( + name: "schedule_audit_log"); + + migrationBuilder.DropTable( + name: "schedule_expiration"); + + migrationBuilder.DropTable( + name: "schedule_holiday_calendars"); + + migrationBuilder.DropTable( + name: "schedule_repeat_options"); + + migrationBuilder.DropTable( + name: "schedule_start_datetimeinfo"); + + migrationBuilder.DropTable( + name: "task_schedules"); + + migrationBuilder.DropTable( + name: "weekly_recurrence"); + + migrationBuilder.DropTable( + name: "workflow_run_variables"); + + migrationBuilder.DropTable( + name: "workflow_schedules"); + + migrationBuilder.DropTable( + name: "workflow_step_dependencies"); + + migrationBuilder.DropTable( + name: "workflow_variables"); + + migrationBuilder.DropTable( + name: "holiday_rules"); + + migrationBuilder.DropTable( + name: "jobs"); + + migrationBuilder.DropTable( + name: "workflow_steps"); + + migrationBuilder.DropTable( + name: "holiday_calendars"); + + migrationBuilder.DropTable( + name: "schedules"); + + migrationBuilder.DropTable( + name: "workflow_runs"); + + migrationBuilder.DropTable( + name: "registered_connections"); + + migrationBuilder.DropTable( + name: "tasks"); + + migrationBuilder.DropTable( + name: "workflows"); + } + } +} diff --git a/src/Werkr.Data/Migrations/Sqlite/SqliteWerkrDbContextModelSnapshot.cs b/src/Werkr.Data/Migrations/Sqlite/SqliteWerkrDbContextModelSnapshot.cs index 15a771b..3bc1c7e 100644 --- a/src/Werkr.Data/Migrations/Sqlite/SqliteWerkrDbContextModelSnapshot.cs +++ b/src/Werkr.Data/Migrations/Sqlite/SqliteWerkrDbContextModelSnapshot.cs @@ -16,7 +16,7 @@ partial class SqliteWerkrDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "10.0.3"); + modelBuilder.HasAnnotation("ProductVersion", "10.0.4"); modelBuilder.Entity("Werkr.Data.Entities.Registration.RegisteredConnection", b => { @@ -985,6 +985,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasColumnName("name"); + b.Property("TargetTags") + .HasColumnType("TEXT") + .HasColumnName("target_tags"); + b.Property("Version") .IsConcurrencyToken() .HasColumnType("INTEGER") diff --git a/src/Werkr.Data/WerkrDbContext.cs b/src/Werkr.Data/WerkrDbContext.cs index 2c8dca5..56cfd2d 100644 --- a/src/Werkr.Data/WerkrDbContext.cs +++ b/src/Werkr.Data/WerkrDbContext.cs @@ -236,6 +236,22 @@ protected override void OnModelCreating( ModelBuilder modelBuilder ) { ); } ); + // Workflow.TargetTags stored as JSON + _ = modelBuilder.Entity( entity => { + PropertyBuilder targetTagsProp = entity.Property( e => e.TargetTags ) + .HasConversion( + v => v == null ? null : JsonSerializer.Serialize( v, (JsonSerializerOptions?)null ), + v => v == null ? null : JsonSerializer.Deserialize(v, (JsonSerializerOptions?)null) + ); + targetTagsProp.Metadata.SetValueComparer( + new ValueComparer( + ( a, b ) => (a == null && b == null) || (a != null && b != null && a.SequenceEqual( b )), + v => v == null ? 0 : v.Aggregate( 0, ( hash, item ) => HashCode.Combine( hash, item.GetHashCode( StringComparison.OrdinalIgnoreCase ) ) ), + v => v == null ? null : v.ToArray( ) + ) + ); + } ); + // WorkflowStepDependency composite key and relationships _ = modelBuilder.Entity( entity => { _ = entity.HasKey( e => new { e.StepId, e.DependsOnStepId } ); @@ -539,8 +555,8 @@ private sealed class ErrorCategoryStringConverter( ) private sealed class ControlStatementStringConverter( ) : ValueConverter( - v => v.ToString( ), - v => Enum.Parse( v ) ); + v => v == ControlStatement.Default ? "Default" : v.ToString( ), + v => v == "Sequential" ? ControlStatement.Default : Enum.Parse( v ) ); private sealed class DependencyModeStringConverter( ) : ValueConverter( diff --git a/src/Werkr.Server/Components/Pages/Workflows/Edit.razor b/src/Werkr.Server/Components/Pages/Workflows/Edit.razor index 9b98460..0eac2f2 100644 --- a/src/Werkr.Server/Components/Pages/Workflows/Edit.razor +++ b/src/Werkr.Server/Components/Pages/Workflows/Edit.razor @@ -40,6 +40,11 @@ +
+ + +
+ + + + + + @* ── Steps ── *@
@@ -144,25 +203,30 @@
- + + @if ( _tasks is not null ) { + @foreach ( TaskDto t in _tasks ) { + + } } - } - + + +
@@ -178,9 +242,9 @@
@if ( IsConditionalControl( _newStep.ControlStatement ) ) {
-
+
- +
@@ -243,6 +307,8 @@ }
+ + } @code { @@ -252,6 +318,8 @@ private WorkflowFormModel? _model; private List? _runs; private List? _tasks; + private List? _schedules; + private List? _availableSchedules; private DagValidationResult? _dagResult; private string? _errorMessage; @@ -259,8 +327,10 @@ private bool _isLoading; private bool _isSaving; private bool _isValidating; + private string _selectedScheduleId = ""; private readonly NewStepModel _newStep = new( ); + private TaskCreateModal? _taskCreateModal; private long _depStepId; private long _depDependsOnStepId; @@ -289,9 +359,16 @@ Name = _workflow.Name, Description = _workflow.Description, Enabled = _workflow.Enabled, + TargetTags = _workflow.TargetTags ?? [], }; _runs = await client.GetFromJsonAsync>( $"/api/workflows/{Id}/runs" ); _tasks = await client.GetFromJsonAsync>( "/api/tasks" ); + _schedules = await client.GetFromJsonAsync>( $"/api/workflows/{Id}/schedules" ); + List? allSchedules = await client.GetFromJsonAsync>( "/api/schedules" ); + HashSet linkedIds = _schedules is not null + ? [.. _schedules.Select( s => s.Id )] + : []; + _availableSchedules = allSchedules?.Where( s => !linkedIds.Contains( s.Id ) ).ToList( ) ?? []; } catch ( Exception ex ) { _errorMessage = $"Failed to load workflow: {ex.Message}"; } finally { @@ -308,7 +385,8 @@ WorkflowUpdateRequest request = new( Name: _model.Name, Description: _model.Description, - Enabled: _model.Enabled ); + Enabled: _model.Enabled, + TargetTags: _model.TargetTags.Length > 0 ? _model.TargetTags : null ); HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); HttpResponseMessage response = await client.PutAsJsonAsync( $"/api/workflows/{Id}", request ); @@ -417,17 +495,58 @@ } private static bool IsConditionalControl( string control ) => - control is "ConditionalIf" or "ConditionalElseIf" or "ConditionalWhile" or "ConditionalDo"; + control is "If" or "ElseIf" or "While" or "Do"; + + private async Task AssociateScheduleAsync( ) { + if ( string.IsNullOrEmpty( _selectedScheduleId ) || !Guid.TryParse( _selectedScheduleId, out Guid schedId ) ) return; + _errorMessage = null; + try { + HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); + HttpResponseMessage response = await client.PostAsJsonAsync( + $"/api/workflows/{Id}/schedules", new WorkflowScheduleAssociateRequest( schedId ) ); + if ( response.IsSuccessStatusCode ) { + _selectedScheduleId = ""; + await LoadAsync( ); + } else { + string body = await response.Content.ReadAsStringAsync( ); + _errorMessage = $"Failed to associate schedule: {body}"; + } + } catch ( Exception ex ) { + _errorMessage = $"Failed to associate schedule: {ex.Message}"; + } + } + + private async Task DisassociateScheduleAsync( Guid scheduleId ) { + _errorMessage = null; + try { + HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); + HttpResponseMessage response = await client.DeleteAsync( + $"/api/workflows/{Id}/schedules/{scheduleId}" ); + if ( response.IsSuccessStatusCode ) { + await LoadAsync( ); + } else { + _errorMessage = $"Failed to remove schedule: HTTP {(int) response.StatusCode}"; + } + } catch ( Exception ex ) { + _errorMessage = $"Failed to remove schedule: {ex.Message}"; + } + } + + private async Task OnTaskCreatedAsync( TaskDto task ) { + _newStep.TaskId = task.Id; + await LoadAsync( ); + } private sealed class WorkflowFormModel { public string Name { get; set; } = ""; public string? Description { get; set; } public bool Enabled { get; set; } = true; + public string[] TargetTags { get; set; } = []; } private sealed class NewStepModel { public long TaskId { get; set; } - public string ControlStatement { get; set; } = "Sequential"; + public string ControlStatement { get; set; } = "Default"; public string DependencyMode { get; set; } = "All"; public string? ConditionExpression { get; set; } public int MaxIterations { get; set; } = 100; diff --git a/src/Werkr.Server/Components/Shared/ConditionBuilder.razor b/src/Werkr.Server/Components/Shared/ConditionBuilder.razor new file mode 100644 index 0000000..ab525d8 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/ConditionBuilder.razor @@ -0,0 +1,170 @@ +@rendermode InteractiveServer + +@if ( _isAdvanced ) { +
+ + + @if ( !string.IsNullOrWhiteSpace( _validationError ) ) { +
@_validationError
+ } +
+ + +} else { +
+
+ + +
+ + @if ( _expressionType == "ExitCode" ) { +
+ + +
+
+ + +
+ } else { +
+ + +
+ } + +
+ +
+
+ + +} + +@if ( !string.IsNullOrWhiteSpace( Expression ) ) { +
+ @Expression +
+} + +
+ Supported expression patterns +
    +
  • $? -eq $true — previous step succeeded
  • +
  • $? -eq $false — previous step failed
  • +
  • $exitCode == 0 — exit code equals value
  • +
  • $exitCode != 0 — exit code not equal
  • +
  • $exitCode > N, < N, >= N, <= N — exit code comparisons
  • +
+
+ +@code { + [Parameter] + public string? Expression { get; set; } + + [Parameter] + public EventCallback ExpressionChanged { get; set; } + + private bool _isAdvanced; + private string _rawExpression = ""; + private string? _validationError; + + private string _expressionType = "ExitCode"; + private string _operator = "=="; + private int _exitCodeValue; + private string _successValue = "true"; + + private static readonly System.Text.RegularExpressions.Regex SuccessPattern = + new( @"^\$\?\s*-eq\s*\$(true|false)$", System.Text.RegularExpressions.RegexOptions.IgnoreCase ); + + private static readonly System.Text.RegularExpressions.Regex ExitCodePattern = + new( @"^\$exitCode\s*(==|!=|>|<|>=|<=)\s*(-?\d+)$", System.Text.RegularExpressions.RegexOptions.IgnoreCase ); + + protected override void OnParametersSet( ) { + if ( !string.IsNullOrWhiteSpace( Expression ) ) { + ParseExistingExpression( Expression ); + } + } + + private void ParseExistingExpression( string expression ) { + System.Text.RegularExpressions.Match successMatch = SuccessPattern.Match( expression.Trim( ) ); + if ( successMatch.Success ) { + _expressionType = "SuccessStatus"; + _successValue = successMatch.Groups[1].Value.ToLowerInvariant( ); + return; + } + + System.Text.RegularExpressions.Match exitMatch = ExitCodePattern.Match( expression.Trim( ) ); + if ( exitMatch.Success ) { + _expressionType = "ExitCode"; + _operator = exitMatch.Groups[1].Value; + if ( int.TryParse( exitMatch.Groups[2].Value, out int val ) ) { + _exitCodeValue = val; + } + } + } + + private async Task ApplyStructuredExpression( ) { + string expr = _expressionType == "ExitCode" + ? $"$exitCode {_operator} {_exitCodeValue}" + : $"$? -eq ${_successValue}"; + + Expression = expr; + await ExpressionChanged.InvokeAsync( expr ); + } + + private async Task ApplyRawExpression( ) { + _validationError = null; + string trimmed = _rawExpression.Trim( ); + + if ( string.IsNullOrWhiteSpace( trimmed ) ) { + Expression = null; + await ExpressionChanged.InvokeAsync( null ); + return; + } + + if ( !SuccessPattern.IsMatch( trimmed ) && !ExitCodePattern.IsMatch( trimmed ) ) { + _validationError = "Expression must match '$exitCode N' or '$? -eq $true/$false'."; + return; + } + + Expression = trimmed; + _rawExpression = trimmed; + await ExpressionChanged.InvokeAsync( trimmed ); + } + + private void SwitchToAdvanced( ) { + _isAdvanced = true; + _rawExpression = Expression ?? ""; + _validationError = null; + } + + private void SwitchToStructured( ) { + _isAdvanced = false; + _validationError = null; + if ( !string.IsNullOrWhiteSpace( _rawExpression ) ) { + ParseExistingExpression( _rawExpression ); + } + } +} diff --git a/src/Werkr.Server/Components/Shared/DagView.razor b/src/Werkr.Server/Components/Shared/DagView.razor index ba3da50..84cf908 100644 --- a/src/Werkr.Server/Components/Shared/DagView.razor +++ b/src/Werkr.Server/Components/Shared/DagView.razor @@ -180,7 +180,7 @@ } private static string GetNodeFill( WorkflowStepDto step ) => step.ControlStatement.ToLowerInvariant( ) switch { - "sequential" => "#0d6efd", // Blue — normal sequential execution + "default" => "#0d6efd", // Blue — default execution "if" or "elseif" => "#6f42c1", // Purple — conditional branch "else" => "#9b59b6", // Light purple — fallback branch "while" or "do" => "#fd7e14", // Orange — loop diff --git a/src/Werkr.Server/Components/Shared/TaskCreateModal.razor b/src/Werkr.Server/Components/Shared/TaskCreateModal.razor new file mode 100644 index 0000000..0100cf1 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/TaskCreateModal.razor @@ -0,0 +1,195 @@ +@rendermode InteractiveServer +@inject IHttpClientFactory HttpClientFactory + +@if ( _isVisible ) { + +} + +@code { + [Parameter] + public long WorkflowId { get; set; } + + [Parameter] + public EventCallback OnTaskCreated { get; set; } + + private bool _isVisible; + private bool _isSaving; + private string? _errorMessage; + private ActionParameterEditor? _actionEditor; + private string[] _targetTags = []; + private TaskFormModel _model = new( ); + + public void Show( ) { + _model = new TaskFormModel( ); + _targetTags = []; + _isVisible = true; + _isSaving = false; + _errorMessage = null; + StateHasChanged( ); + } + + private void Close( ) { + _isVisible = false; + StateHasChanged( ); + } + + private async Task SubmitAsync( ) { + if ( _model.ActionType == "Action" && _actionEditor is not null && !_actionEditor.Validate( ) ) { + return; + } + _isSaving = true; + _errorMessage = null; + try { + TaskCreateRequest request = _model.ToCreateRequest( WorkflowId, _targetTags ); + HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); + HttpResponseMessage response = await client.PostAsJsonAsync( "/api/tasks", request ); + if ( response.IsSuccessStatusCode ) { + TaskDto? created = await response.Content.ReadFromJsonAsync( ); + _isVisible = false; + if ( created is not null ) { + await OnTaskCreated.InvokeAsync( created ); + } + } else { + string body = await response.Content.ReadAsStringAsync( ); + _errorMessage = $"Failed to create task: {body}"; + } + } catch ( Exception ex ) { + _errorMessage = $"Failed to create task: {ex.Message}"; + } finally { + _isSaving = false; + } + } + + private static bool IsScriptType( string actionType ) => + actionType is "PowerShellScript" or "ShellScript"; + + private void OnTargetTagsChanged( string[] tags ) { + _targetTags = tags; + } + + private sealed class TaskFormModel { + public string Name { get; set; } = ""; + public string? Description { get; set; } + public string ActionType { get; set; } = "ShellCommand"; + public string Content { get; set; } = ""; + public string? Arguments { get; set; } + public bool Enabled { get; set; } = true; + public long? TimeoutMinutes { get; set; } + public string? SuccessCriteria { get; set; } + public string ActionSubType { get; set; } = ""; + public string? ActionParameters { get; set; } + + public TaskCreateRequest ToCreateRequest( long workflowId, string[] targetTags ) { + string[]? args = string.IsNullOrWhiteSpace( Arguments ) + ? null + : Arguments.Split( '\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); + + return new TaskCreateRequest( + Name: Name, + Description: Description, + ActionType: ActionType, + Content: Content, + Arguments: args, + TargetTags: targetTags, + Enabled: Enabled, + TimeoutMinutes: TimeoutMinutes, + SuccessCriteria: SuccessCriteria, + WorkflowId: workflowId, + ActionSubType: string.IsNullOrWhiteSpace( ActionSubType ) ? null : ActionSubType, + ActionParameters: string.IsNullOrWhiteSpace( ActionParameters ) ? null : ActionParameters ); + } + } +} From 8ca38aeeb378cc1b718d85a702efa7a0279fd366 Mon Sep 17 00:00:00 2001 From: Taylor Marvin Date: Sat, 14 Mar 2026 15:42:06 -0700 Subject: [PATCH 02/11] Workflow-Centric UI refactor (#10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Workflow UI & Interactive DAG Editor This PR delivers a complete UI refactor: a TypeScript/Node.js build pipeline, real-time run monitoring via SignalR, a vis-timeline Gantt view, and a full AntV X6-based DAG renderer and interactive workflow editor - replacing the previous server-rendered SVG `DagView.razor` entirely. --- ### Interactive Visual Workflow Editor **Why:** The form-based step/dependency management was not intuitive or visually appealing. The best visual workflow editors share a common set of critical UX patterns: composable architecture (separating mapping, operations, layout, and traversal concerns), connection-drop-to-node-creator, undo/redo with grouped history, node dirtiness tracking, fixed connection points with visual feedback state machines (green/blue port indicators), and searchable, categorized shape palettes. The editor uses a **batch-save architecture**: all mutations modify local X6 graph state and an in-memory changeset. An explicit "Save" syncs to the API. Undo/redo operates purely on local state - avoiding the complexity of reversing per-action API calls. - **DagEditor.razor** - Top-level editor page at `/workflows/{Id}/dag-editor`. Three-column layout: `StepPalette` | `DagEditorCanvas` | `NodeConfigPanel`. Manages draft restore, metadata editing, DAG validation. Authorized for Admin/Operator roles. - **DagEditorCanvas.razor** - Interactive X6 canvas with minimap - **DagEditorJsInterop** - Editor interop: `InitEditorAsync`, `LoadGraphAsync`, `AddNodeAsync`, `UpdateNodeDataAsync`, `GetChangesetJsonAsync`, `UndoAsync`/`RedoAsync`, `SetLayoutDirectionAsync`, `ZoomToAsync`/`ZoomToFitAsync`. JS→.NET callbacks: `OnNodeSelected`, `OnNodeDropped`, `OnSaveRequested`, `OnCycleDetected`, `OnGraphDirtyChanged`. - **StepPalette.razor** - Categorized, searchable sidebar with drag-to-canvas via X6 Dnd. Collapsible categories with filtered step creation for rapid discovery. - **NodeConfigPanel.razor** - Right-side property editor for task info, control flow, conditions, variable bindings, dependencies. Enables task-level I/O inspection - the #1 debugging capability for workflow operators. - **EditorToolbar.razor** - Save, undo/redo, validate, zoom, export controls - **Undo/redo** via X6 History plugin - tracks all mutations with grouped operations (e.g., delete node + edges = one undo step) - **Keyboard shortcuts** via X6 Keyboard: Delete, Ctrl+A, Ctrl+C/V, Ctrl+Z/Y, Escape, arrow keys - **Clipboard** via X6 Clipboard: copy/paste subgraphs with preserved internal edges - **changeset.ts** - Tracks add/delete/move mutations locally. Debounced: rapid operations accumulate, Save sends a single batch. - **cycle-detection.ts** - Client-side DFS cycle detection O(V+E) with visual feedback on invalid connections. Most diagramming tools don't perform cycle detection; this is a Werkr addition required for DAG integrity. - **draft-storage.ts** - Auto-save drafts to localStorage. `beforeunload` warning for unsaved changes. - **export-handler.ts** - PNG and SVG export via `graph.toSVG()` / `graph.toPNG()` - **dnd-handler.ts** - Drag-and-drop from palette to canvas - **annotation-node.ts** - Free-form sticky note annotations on canvas for documenting workflow sections --- ### AntV X6 DAG Renderer - Replacing Server-Rendered SVG **Why:** The previous `DagView.razor` was a server-rendered SVG using Kahn's algorithm with fixed-size nodes (160x52) and click-to-select only. SVG + foreignObject rendering with Dagre layout is the proven approach for rich node content (full HTML/CSS) while maintaining SVG coordinates for pan, zoom, and edges. AntV X6 provides all visual interactions client-side at 0ms latency, with built-in undo/redo, clipboard, drag-from-toolbar, minimap, and snapline alignment. - **DagCanvas.razor** - Read-only X6 DAG component with toolbar (LR/TB layout switching) - **DagJsInterop** - Typed C# wrapper inheriting `GraphJsInteropBase`. Methods: `CreateGraphAsync`, `LoadGraphAsync`, `ZoomToFitAsync`, `ApplyStatusOverlayAsync`. No scattered `IJSRuntime.InvokeAsync("eval", ...)` calls. - **TypeScript core:** create-graph.ts, dag-readonly.ts, werkr-node.ts, werkr-edge.ts - **Dagre hierarchical layout** via layout-engine.ts - configurable TB/LR rank direction. The `tight-tree` ranker was chosen over `network-simplex` for dramatically better performance at scale (minutes → seconds for large graphs). - **foreignObject HTML nodes** with Bootstrap styling: cards, badges, action-type indicators - enabling full HTML/CSS inside graph nodes rather than flat SVG text - **parallel-lanes.ts** - Light background lanes behind same-level parallel nodes for visual concurrency indication - **status-overlay.ts** - Color-coded execution status per selected run via `cell.setData()`. Consistent color language: Green=Success, Red=Failed, Amber=Running, Purple=Pending, Gray=Skipped. - **X6 plugins:** Selection, Snapline, Scroller, Transform, MiniMap - all built into X6 3.x core - **Type definitions:** dag-types.ts (node/edge DTOs), dotnet-interop.d.ts (.NET callback interface) --- ### Real-Time Run Monitoring & Execution Views **Why:** The workflow debugging experience is the primary functionality for the product - building workflows happens once; debugging them happens daily. - **Three-view tiered complexity** - different users need different detail levels (Compact for quick glance, Timeline for duration analysis, Detail for debugging) - **Steps × Runs grid view** - the most effective at-a-glance cross-run pattern recognition, with color-coded cells making failures immediately visible - **Gantt charts** - essential for bottleneck identification and understanding parallelism - **Sparkline bar charts** - quick pattern recognition for duration and failure trends without drilling in - **Event grouping** - collapsing related events (Scheduled→Started→Completed) into one logical unit reduces cognitive load - **Real-time liveness** - all views update as events stream, rather than polling on a timer - **Task-level I/O inspection** - see exactly what went in/out at every task boundary - **Saved filter views** - operators managing many workflows need personalized, filtered views by status, agent, tag, and owner **Workflow event types** in `Werkr.Core/Communication/`: - **WorkflowEvent.cs** - Six new event types: `StepStartedEvent`, `StepCompletedEvent`, `StepFailedEvent`, `StepSkippedEvent`, `RunCompletedEvent`, `LogAppendedEvent` - **WorkflowEventBroadcaster** - Singleton fan-out via `System.Threading.Channels`. Published from gRPC service handlers (`JobReportingGrpcService`, `OutputStreamingService`, `VariableService`) when agents report execution progress. - **WorkflowEventSubscription** - Consumer subscription pattern for SSE endpoints **Real-time data pipeline:** ``` Agent → gRPC → API WorkflowEventBroadcaster → SSE stream → JobEventRelayService → SignalR WorkflowRunHub → Browser ``` - **WorkflowRunHub** - SignalR hub at `/hubs/workflow-run`. Authorized clients join groups keyed by run ID to receive `StepStatusChanged`, `RunCompleted`, and `LogAppended` events. - **JobEventRelayService** - `BackgroundService` + `IHealthCheck` that opens a long-lived SSE connection to `GET /api/events/workflow-runs` via the `"ApiServiceSse"` named `HttpClient`, deserializes events, and relays them to the hub via `IHubContext`. Auto-reconnects with exponential backoff (1s → 30s). Tracks active run subscriptions in a `ConcurrentDictionary`. Registered as health check (`"sse-relay"`) for operational visibility into SSE connection status. - **ConnectionStatus.razor** - Visual indicator (Live / Reconnecting / Polling) in the UI - **RunDetail.razor** - Multi-view run detail page (Compact / Timeline / Log tabs) - **GanttTab.razor** + **TimelineJsInterop** - vis-timeline Gantt chart showing horizontal bars per task with duration and status. Live bars grow during active runs via SignalR push. - **RunSparkline.razor** - Server-side SVG mini bar charts (bar height = duration, bar color = status) inline on the workflow list - **RunGridView.razor** - Steps × Runs 2D status matrix for cross-run comparison - **StepDetailPanel.razor** - Step-level log preview and error messages - **TimelineView.razor** - Chronological step execution list with attempt tracking and event grouping - **SavedFilterService** + **FilterBar.razor** - Named saved filter views persisted via localStorage. Backed by a full filter model ecosystem: `FilterField`, `FilterDefinition`, `FilterCriteria`, `FilterFieldType`, `SavedFilterView` in `Werkr.Common/Models/`. - **SignalR DTOs** - `StepStatusDto`, `RunStatusDto`, `LogLineDto` in `WorkflowRunHubDtos.cs` - typed payloads for each hub broadcast method - **Additional DTOs** - `RunSparklineDto`, `StepStatusSummaryDto`, `AnnotationDto`, `DagValidationResult` in `Werkr.Common/Models/` - Dedicated SSE `HttpClient` (`"ApiServiceSse"`) configured with infinite timeout and no resilience handlers in Program.cs --- ### API Endpoints & Batch Operations **Batch save endpoint** - The server-side contract for the editor's batch-save architecture: - `POST /api/workflows/{workflowId}/steps/batch` - Atomic batch operations for editor save. Accepts a `WorkflowStepBatchRequest` containing ordered step operations. - **Sign convention**: negative `StepId` = temp ID (new nodes created in the editor), positive = real ID (existing nodes). The response includes `StepIdMapping[]` for temp→real ID resolution after save. - **WorkflowStepBatchModels.cs** - `WorkflowStepBatchRequest`, `StepBatchOperation`, `DependencyBatchItem`, `WorkflowStepBatchResponse`, `StepIdMapping` **Run management:** - `POST /api/workflows/{id}/runs/{runId}/retry-from/{stepId}` - Retry from a failed step with optional variable overrides via `RetryFromFailedRequest` **SSE event streams:** - `GET /api/events/workflow-runs` - Dedicated SSE stream for workflow run events (consumed by `JobEventRelayService`) - `GET /api/workflows/runs/{runId}/events` - Per-run filtered event stream - `GET /api/events/jobs` - Existing job event stream (backward-compatible) **Saved filter persistence:** - Full CRUD at `/api/filters/{pageKey}` (GET list, POST create, PUT update, DELETE) with page keys for runs, workflows, jobs, agents, schedules, tasks. Admin-only share toggle. --- ### Consistent Color Standard & Theme System **Why:** A consistent color language across all views - where the same semantic colors map to the same states throughout the entire UI - is essential for quick visual parsing. - **CSS custom properties** in theme.css defining a standardized palette with dark/light theme support: - Status: `--werkr-success` (#28a745), `--werkr-failed` (#dc3545), `--werkr-running` (#ffc107), `--werkr-pending` (#6f42c1), `--werkr-skipped` (#6c757d), `--werkr-not-run` - Node types: `--werkr-node-default`, `--werkr-node-conditional`, `--werkr-node-fallback`, `--werkr-node-loop` - Structural: `--werkr-edge-color`, `--werkr-node-stroke`, `--werkr-parallel-group-bg`, `--werkr-snapline` - Applied consistently across DAG nodes, sparklines, grid view cells, Gantt bars, and status badges --- ### JS/TS Build Infrastructure **Why:** The X6 graph engine, vis-timeline, and Dagre layout all require a proper JS/TS build pipeline. Keeping all drag, zoom, pan, and connection drawing in the browser while reserving SignalR round-trips for state commits. - **TypeScript 5.9** project with strict mode under `src/Werkr.Server/graph-ui/` - **esbuild** bundler (~50ms incremental builds) with code-split output to `wwwroot/js/dist/`: `dag-readonly.js`, `dag-editor.js`, `timeline-view.js`, plus shared vendor chunks. Editor bundle lazy-loaded only when entering edit mode. - **MSBuild integration** in Werkr.Server.csproj: `EnsureNode` → `NpmInstall` → `BuildGraphUi` targets fire automatically during `dotnet build`. No separate npm steps needed during development. - **Vitest 3.0** test runner with 7 unit tests covering changeset operations, cycle detection, draft storage, clipboard handling, timeline item mapping, and status styling - **Blazor<->JS interop hierarchy:** `GraphJsInteropBase` manages `IJSObjectReference` + `DotNetObjectReference` lifecycle with `IAsyncDisposable`. Three derived classes: `DagJsInterop` (read-only), `DagEditorJsInterop` (editor), `TimelineJsInterop` (Gantt). - `.nvmrc` pins Node.js 22. `.gitignore` excludes `node_modules/` and `wwwroot/js/dist/`. --- ### CI/CD & Dependency Updates - **CI workflow** (ci.yml): Added Node.js 22 setup → `npm ci` → Vitest → `npm run build:prod` → bundle size check via check-bundle-size.mjs (250 KB gzipped budget) - **DocFX workflow** (DocFX_gh-pages.yml): Simplified from multi-job Windows workflow to single Ubuntu-based job - **NuGet dependency updates** in Directory.Packages.props: Microsoft.Extensions, ASP.NET Core, EF Core, SignalR, and test packages updated to latest patch versions - **Lock file updates** across all projects to match new package versions - **Cross-platform test fix** in UrlValidatorTests.cs: `RelativeUrl_Rejected` test now handles Unix vs. Windows URI parsing differences - **`System.Text.Json` transitive dependency** update in installer project --- ### New Dependencies | Package | Version | Purpose | |---------|---------|---------| | `@antv/x6` | 3.1.6 | Graph visualization & editing engine (MIT) | | `dagre` | 0.8.5 | Hierarchical DAG layout (MIT) | | `vis-timeline` | 7.7.3 | Gantt timeline rendering (Apache-2.0 / MIT) | | `vis-data` | 7.1.9 | Reactive data containers for vis-timeline (Apache-2.0 / MIT) | | `typescript` | 5.9.x | TypeScript compiler | | `esbuild` | 0.27.x | Bundler | | `vitest` | 3.0.x | Test runner | No new NuGet packages - all required .NET packages already existed in Directory.Packages.props. ### New Prerequisite - **Node.js 22+** required on dev machines and CI agents --- .github/workflows/DocFX_gh-pages.yml | 151 +- .github/workflows/ci.yml | 19 + .gitignore | 5 + .vscode/tasks.json | 442 +++ Directory.Packages.props | 32 +- src/Installer/Msi/Agent/packages.lock.json | 44 +- .../Werkr.Installer.Msi.CustomActions.csproj | 2 +- .../Msi/CustomActions/packages.lock.json | 24 +- src/Installer/Msi/Server/packages.lock.json | 44 +- .../Security/UrlValidatorTests.cs | 5 +- src/Test/Werkr.Tests.Agent/packages.lock.json | 3244 ++++++++--------- .../ControlStatementConverterTests.cs | 18 +- src/Test/Werkr.Tests.Data/packages.lock.json | 2028 +++++------ .../Authorization/PageAuthorizationTests.cs | 3 +- .../Components/ConditionBuilderTests.cs | 2 +- .../Components/TaskCreateModalTests.cs | 132 - .../Components/TaskSetupModalTests.cs | 265 ++ .../Pages/PageDiscoveryTests.cs | 2 + .../Werkr.Tests.Server/packages.lock.json | 2387 ++++++------ src/Test/Werkr.Tests/packages.lock.json | 2534 ++++++------- .../Communication/AgentGrpcClientFactory.cs | 27 +- .../Operators/SystemShellOperator.cs | 5 + .../Scheduling/WorkflowExecutionService.cs | 174 +- .../Services/OutputStreamingService.cs | 44 +- src/Werkr.Agent/packages.lock.json | 82 +- src/Werkr.Api/Endpoints/EventEndpoints.cs | 77 +- src/Werkr.Api/Endpoints/FilterEndpoints.cs | 246 ++ src/Werkr.Api/Endpoints/ShellEndpoints.cs | 18 +- src/Werkr.Api/Endpoints/WorkflowEndpoints.cs | 435 ++- src/Werkr.Api/Models/WorkflowMapper.cs | 43 +- src/Werkr.Api/Program.cs | 5 + .../Services/JobReportingGrpcService.cs | 158 +- .../Services/OutputStreamingGrpcService.cs | 78 + .../ScheduleInvalidationDispatcher.cs | 21 +- .../Services/ScheduleSyncGrpcService.cs | 9 +- src/Werkr.Api/Services/VariableGrpcService.cs | 103 + src/Werkr.Api/Werkr.Api.csproj.user | 6 + src/Werkr.Api/packages.lock.json | 104 +- src/Werkr.AppHost/AppHost.cs | 23 +- src/Werkr.AppHost/packages.lock.json | 4 +- .../packages.lock.json | 40 +- .../Models/Actions/ActionRegistry.cs | 44 + .../Actions/PowerShellCommandParameters.cs | 10 + .../Actions/PowerShellScriptParameters.cs | 13 + .../Models/Actions/ShellCommandParameters.cs | 10 + .../Models/Actions/ShellScriptParameters.cs | 13 + src/Werkr.Common/Models/AnnotationDto.cs | 35 + src/Werkr.Common/Models/DagEdgeDto.cs | 7 + src/Werkr.Common/Models/DagNodeDto.cs | 10 + src/Werkr.Common/Models/FilterCriteria.cs | 12 + src/Werkr.Common/Models/FilterDefinition.cs | 6 + src/Werkr.Common/Models/FilterField.cs | 9 + src/Werkr.Common/Models/FilterFieldType.cs | 17 + src/Werkr.Common/Models/GanttItemDto.cs | 11 + .../Models/RetryFromFailedRequest.cs | 6 + src/Werkr.Common/Models/RunSparklineDto.cs | 8 + src/Werkr.Common/Models/SavedFilterView.cs | 10 + src/Werkr.Common/Models/StepExecutionDto.cs | 15 + .../Models/StepExecutionStatus.cs | 23 + .../Models/StepStatusSummaryDto.cs | 10 + .../Models/WorkflowDashboardDto.cs | 15 + .../Models/WorkflowDashboardPageDto.cs | 9 + src/Werkr.Common/Models/WorkflowDto.cs | 3 +- .../Models/WorkflowLatestRunStatusDto.cs | 10 + .../Models/WorkflowRunDetailDto.cs | 5 +- src/Werkr.Common/Models/WorkflowRunDto.cs | 3 +- .../Models/WorkflowRunSummaryDto.cs | 9 + .../Models/WorkflowStepBatchModels.cs | 70 + src/Werkr.Common/Models/WorkflowStepDto.cs | 3 +- .../Models/WorkflowUpdateRequest.cs | 3 +- src/Werkr.Common/Protos/JobReport.proto | 27 + src/Werkr.Common/Protos/OutputStreaming.proto | 3 + src/Werkr.Common/Protos/VariableService.proto | 33 + src/Werkr.Common/packages.lock.json | 388 +- src/Werkr.Core/Communication/WorkflowEvent.cs | 47 + .../Communication/WorkflowEventBroadcaster.cs | 80 + .../WorkflowEventSubscription.cs | 28 + .../Scheduling/RetryFromFailedService.cs | 191 + src/Werkr.Core/Workflows/WorkflowService.cs | 200 + src/Werkr.Core/packages.lock.json | 854 ++--- .../Postgres/20260312050754_InitialCreate.cs | 690 ++-- .../Sqlite/20260312050800_InitialCreate.cs | 606 ++- src/Werkr.Data.Identity/packages.lock.json | 104 +- .../Entities/Settings/SavedFilter.cs | 40 + src/Werkr.Data/Entities/Tasks/WerkrJob.cs | 8 + src/Werkr.Data/Entities/Workflows/Workflow.cs | 3 + .../Entities/Workflows/WorkflowRun.cs | 3 + .../Workflows/WorkflowStepExecution.cs | 61 + .../Postgres/20260312050739_InitialCreate.cs | 977 ----- ... 20260314004316_InitialCreate.Designer.cs} | 202 +- .../Postgres/20260314004316_InitialCreate.cs | 1047 ++++++ .../PostgresWerkrDbContextModelSnapshot.cs | 200 +- .../Sqlite/20260312050748_InitialCreate.cs | 868 ----- ... 20260314004323_InitialCreate.Designer.cs} | 198 +- .../Sqlite/20260314004323_InitialCreate.cs | 923 +++++ .../SqliteWerkrDbContextModelSnapshot.cs | 196 +- src/Werkr.Data/WerkrDbContext.cs | 56 + src/Werkr.Data/packages.lock.json | 290 +- src/Werkr.Server/Components/App.razor | 1 + .../Components/Layout/MainLayout.razor.css | 1 + .../Components/Layout/NavMenu.razor | 41 +- .../Components/Pages/AgentDetail.razor | 8 +- .../Components/Pages/Agents/Console.razor | 43 +- .../Components/Pages/Agents/Index.razor | 34 +- .../Components/Pages/ApiKeys.razor | 11 +- .../Pages/Dashboard/Calendar.razor.css | 1 + .../Components/Pages/Dashboard/TaskList.razor | 5 +- src/Werkr.Server/Components/Pages/Home.razor | 19 +- .../Components/Pages/Jobs/Detail.razor | 2 +- .../Components/Pages/Jobs/Index.razor | 110 +- .../Components/Pages/Schedules/Create.razor | 452 +-- .../Components/Pages/Schedules/Edit.razor | 574 +-- .../Components/Pages/Schedules/Index.razor | 64 +- .../Components/Pages/Settings.razor | 12 +- .../Components/Pages/Tasks/Create.razor | 4 +- .../Components/Pages/Tasks/Edit.razor | 6 +- .../Components/Pages/Tasks/Index.razor | 63 +- .../Components/Pages/Workflows/AllRuns.razor | 150 + .../Components/Pages/Workflows/Create.razor | 87 +- .../Components/Pages/Workflows/Dag.razor | 3 +- .../Pages/Workflows/DagEditor.razor | 690 ++++ .../Pages/Workflows/DagEditor.razor.css | 14 + .../Components/Pages/Workflows/Detail.razor | 215 ++ .../Components/Pages/Workflows/Edit.razor | 554 --- .../Components/Pages/Workflows/Index.razor | 329 +- .../Pages/Workflows/RunDetail.razor | 361 +- .../Components/Pages/Workflows/Runs.razor | 48 +- .../Components/Shared/ConnectionStatus.razor | 29 + .../Components/Shared/DagCanvas.razor | 220 ++ .../Components/Shared/DagCanvas.razor.css | 45 + .../Components/Shared/DagEditorCanvas.razor | 99 + .../Shared/DagEditorCanvas.razor.css | 21 + .../Components/Shared/DagView.razor | 195 - .../Components/Shared/DagView.razor.css | 22 - .../Components/Shared/EditorToolbar.razor | 152 + .../Components/Shared/FilterBar.razor | 213 ++ .../Components/Shared/FilterBar.razor.css | 50 + .../Components/Shared/GanttTab.razor | 166 + .../Components/Shared/GanttTab.razor.css | 43 + .../Components/Shared/LogViewer.razor | 20 + .../Components/Shared/NodeConfigPanel.razor | 217 ++ .../Shared/NodeConfigPanel.razor.css | 14 + .../Components/Shared/RunGridView.razor | 69 + .../Components/Shared/RunGridView.razor.css | 10 + .../Components/Shared/RunSparkline.razor | 41 + .../Components/Shared/RunSparkline.razor.css | 8 + .../Shared/ScheduleSetupModal.razor | 617 ++++ .../Components/Shared/StepDetailPanel.razor | 93 + .../Components/Shared/StepPalette.razor | 70 + .../Components/Shared/StepPalette.razor.css | 21 + ...CreateModal.razor => TaskSetupModal.razor} | 100 +- .../Components/Shared/TimelineView.razor | 48 + .../Shared/WorkflowStepListView.razor | 73 + src/Werkr.Server/Components/_Imports.razor | 1 + .../Helpers/AgentDisplayHelper.cs | 18 +- .../Helpers/DagEditorJsInterop.cs | 258 ++ src/Werkr.Server/Helpers/DagJsInterop.cs | 107 + .../Helpers/GraphJsInteropBase.cs | 64 + .../Helpers/StatusDisplayHelper.cs | 38 + src/Werkr.Server/Helpers/TimelineJsInterop.cs | 90 + src/Werkr.Server/Hubs/WorkflowRunHub.cs | 40 + src/Werkr.Server/Hubs/WorkflowRunHubDtos.cs | 19 + src/Werkr.Server/Program.cs | 37 +- .../Services/JobEventRelayService.cs | 257 ++ .../Services/SavedFilterService.cs | 172 + src/Werkr.Server/Werkr.Server.csproj | 37 + src/Werkr.Server/Werkr.Server.csproj.user | 6 + src/Werkr.Server/graph-ui/.nvmrc | 1 + src/Werkr.Server/graph-ui/esbuild.config.mjs | 59 + src/Werkr.Server/graph-ui/package-lock.json | 1839 ++++++++++ src/Werkr.Server/graph-ui/package.json | 27 + .../graph-ui/scripts/check-bundle-size.mjs | 83 + .../graph-ui/src/dag/annotation-node.ts | 59 + .../graph-ui/src/dag/changeset.ts | 253 ++ .../graph-ui/src/dag/clipboard-handler.ts | 69 + .../graph-ui/src/dag/create-graph.ts | 113 + .../graph-ui/src/dag/cycle-detection.ts | 39 + .../graph-ui/src/dag/dag-editor.ts | 516 +++ .../graph-ui/src/dag/dag-readonly.ts | 225 ++ .../graph-ui/src/dag/dag-types.ts | 56 + src/Werkr.Server/graph-ui/src/dag/dagre.d.ts | 14 + .../graph-ui/src/dag/dnd-handler.ts | 57 + .../graph-ui/src/dag/draft-storage.ts | 59 + .../graph-ui/src/dag/editor-bindings.ts | 258 ++ .../graph-ui/src/dag/export-handler.ts | 63 + .../graph-ui/src/dag/graph-bindings.ts | 45 + .../graph-ui/src/dag/layout-engine.ts | 82 + .../graph-ui/src/dag/parallel-lanes.ts | 95 + .../graph-ui/src/dag/status-overlay.ts | 103 + .../graph-ui/src/dag/werkr-edge.ts | 27 + .../graph-ui/src/dag/werkr-node.ts | 119 + src/Werkr.Server/graph-ui/src/index.ts | 2 + .../graph-ui/src/timeline/gantt-item-dto.ts | 9 + .../graph-ui/src/timeline/timeline-items.ts | 15 + .../graph-ui/src/timeline/timeline-options.ts | 19 + .../graph-ui/src/timeline/timeline-styles.ts | 11 + .../graph-ui/src/timeline/timeline-view.ts | 140 + .../graph-ui/src/types/dotnet-interop.d.ts | 5 + .../graph-ui/test/dag/changeset.test.ts | 278 ++ .../test/dag/clipboard-handler.test.ts | 145 + .../graph-ui/test/dag/cycle-detection.test.ts | 64 + .../graph-ui/test/dag/draft-storage.test.ts | 109 + src/Werkr.Server/graph-ui/test/smoke.test.ts | 8 + .../test/timeline/timeline-items.test.ts | 79 + .../test/timeline/timeline-styles.test.ts | 35 + src/Werkr.Server/graph-ui/tsconfig.json | 21 + src/Werkr.Server/graph-ui/vitest.config.ts | 18 + src/Werkr.Server/packages.lock.json | 128 +- src/Werkr.Server/wwwroot/css/theme.css | 153 +- src/Werkr.Server/wwwroot/js/toggle-theme.js | 43 +- .../bootstrap-icons/bootstrap-icons.min.css | 5 + .../fonts/bootstrap-icons.woff | Bin 0 -> 176032 bytes .../fonts/bootstrap-icons.woff2 | Bin 0 -> 130396 bytes src/Werkr.ServiceDefaults/packages.lock.json | 294 +- 214 files changed, 24035 insertions(+), 11316 deletions(-) create mode 100644 .vscode/tasks.json delete mode 100644 src/Test/Werkr.Tests.Server/Components/TaskCreateModalTests.cs create mode 100644 src/Test/Werkr.Tests.Server/Components/TaskSetupModalTests.cs create mode 100644 src/Werkr.Api/Endpoints/FilterEndpoints.cs create mode 100644 src/Werkr.Api/Werkr.Api.csproj.user create mode 100644 src/Werkr.Common/Models/Actions/PowerShellCommandParameters.cs create mode 100644 src/Werkr.Common/Models/Actions/PowerShellScriptParameters.cs create mode 100644 src/Werkr.Common/Models/Actions/ShellCommandParameters.cs create mode 100644 src/Werkr.Common/Models/Actions/ShellScriptParameters.cs create mode 100644 src/Werkr.Common/Models/AnnotationDto.cs create mode 100644 src/Werkr.Common/Models/DagEdgeDto.cs create mode 100644 src/Werkr.Common/Models/DagNodeDto.cs create mode 100644 src/Werkr.Common/Models/FilterCriteria.cs create mode 100644 src/Werkr.Common/Models/FilterDefinition.cs create mode 100644 src/Werkr.Common/Models/FilterField.cs create mode 100644 src/Werkr.Common/Models/FilterFieldType.cs create mode 100644 src/Werkr.Common/Models/GanttItemDto.cs create mode 100644 src/Werkr.Common/Models/RetryFromFailedRequest.cs create mode 100644 src/Werkr.Common/Models/RunSparklineDto.cs create mode 100644 src/Werkr.Common/Models/SavedFilterView.cs create mode 100644 src/Werkr.Common/Models/StepExecutionDto.cs create mode 100644 src/Werkr.Common/Models/StepExecutionStatus.cs create mode 100644 src/Werkr.Common/Models/StepStatusSummaryDto.cs create mode 100644 src/Werkr.Common/Models/WorkflowDashboardDto.cs create mode 100644 src/Werkr.Common/Models/WorkflowDashboardPageDto.cs create mode 100644 src/Werkr.Common/Models/WorkflowLatestRunStatusDto.cs create mode 100644 src/Werkr.Common/Models/WorkflowRunSummaryDto.cs create mode 100644 src/Werkr.Common/Models/WorkflowStepBatchModels.cs create mode 100644 src/Werkr.Core/Communication/WorkflowEvent.cs create mode 100644 src/Werkr.Core/Communication/WorkflowEventBroadcaster.cs create mode 100644 src/Werkr.Core/Communication/WorkflowEventSubscription.cs create mode 100644 src/Werkr.Core/Scheduling/RetryFromFailedService.cs create mode 100644 src/Werkr.Data/Entities/Settings/SavedFilter.cs create mode 100644 src/Werkr.Data/Entities/Workflows/WorkflowStepExecution.cs delete mode 100644 src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.cs rename src/Werkr.Data/Migrations/Postgres/{20260312050739_InitialCreate.Designer.cs => 20260314004316_InitialCreate.Designer.cs} (89%) create mode 100644 src/Werkr.Data/Migrations/Postgres/20260314004316_InitialCreate.cs delete mode 100644 src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.cs rename src/Werkr.Data/Migrations/Sqlite/{20260312050748_InitialCreate.Designer.cs => 20260314004323_InitialCreate.Designer.cs} (89%) create mode 100644 src/Werkr.Data/Migrations/Sqlite/20260314004323_InitialCreate.cs create mode 100644 src/Werkr.Server/Components/Pages/Workflows/AllRuns.razor create mode 100644 src/Werkr.Server/Components/Pages/Workflows/DagEditor.razor create mode 100644 src/Werkr.Server/Components/Pages/Workflows/DagEditor.razor.css create mode 100644 src/Werkr.Server/Components/Pages/Workflows/Detail.razor delete mode 100644 src/Werkr.Server/Components/Pages/Workflows/Edit.razor create mode 100644 src/Werkr.Server/Components/Shared/ConnectionStatus.razor create mode 100644 src/Werkr.Server/Components/Shared/DagCanvas.razor create mode 100644 src/Werkr.Server/Components/Shared/DagCanvas.razor.css create mode 100644 src/Werkr.Server/Components/Shared/DagEditorCanvas.razor create mode 100644 src/Werkr.Server/Components/Shared/DagEditorCanvas.razor.css delete mode 100644 src/Werkr.Server/Components/Shared/DagView.razor delete mode 100644 src/Werkr.Server/Components/Shared/DagView.razor.css create mode 100644 src/Werkr.Server/Components/Shared/EditorToolbar.razor create mode 100644 src/Werkr.Server/Components/Shared/FilterBar.razor create mode 100644 src/Werkr.Server/Components/Shared/FilterBar.razor.css create mode 100644 src/Werkr.Server/Components/Shared/GanttTab.razor create mode 100644 src/Werkr.Server/Components/Shared/GanttTab.razor.css create mode 100644 src/Werkr.Server/Components/Shared/LogViewer.razor create mode 100644 src/Werkr.Server/Components/Shared/NodeConfigPanel.razor create mode 100644 src/Werkr.Server/Components/Shared/NodeConfigPanel.razor.css create mode 100644 src/Werkr.Server/Components/Shared/RunGridView.razor create mode 100644 src/Werkr.Server/Components/Shared/RunGridView.razor.css create mode 100644 src/Werkr.Server/Components/Shared/RunSparkline.razor create mode 100644 src/Werkr.Server/Components/Shared/RunSparkline.razor.css create mode 100644 src/Werkr.Server/Components/Shared/ScheduleSetupModal.razor create mode 100644 src/Werkr.Server/Components/Shared/StepDetailPanel.razor create mode 100644 src/Werkr.Server/Components/Shared/StepPalette.razor create mode 100644 src/Werkr.Server/Components/Shared/StepPalette.razor.css rename src/Werkr.Server/Components/Shared/{TaskCreateModal.razor => TaskSetupModal.razor} (66%) create mode 100644 src/Werkr.Server/Components/Shared/TimelineView.razor create mode 100644 src/Werkr.Server/Components/Shared/WorkflowStepListView.razor create mode 100644 src/Werkr.Server/Helpers/DagEditorJsInterop.cs create mode 100644 src/Werkr.Server/Helpers/DagJsInterop.cs create mode 100644 src/Werkr.Server/Helpers/GraphJsInteropBase.cs create mode 100644 src/Werkr.Server/Helpers/StatusDisplayHelper.cs create mode 100644 src/Werkr.Server/Helpers/TimelineJsInterop.cs create mode 100644 src/Werkr.Server/Hubs/WorkflowRunHub.cs create mode 100644 src/Werkr.Server/Hubs/WorkflowRunHubDtos.cs create mode 100644 src/Werkr.Server/Services/JobEventRelayService.cs create mode 100644 src/Werkr.Server/Services/SavedFilterService.cs create mode 100644 src/Werkr.Server/Werkr.Server.csproj.user create mode 100644 src/Werkr.Server/graph-ui/.nvmrc create mode 100644 src/Werkr.Server/graph-ui/esbuild.config.mjs create mode 100644 src/Werkr.Server/graph-ui/package-lock.json create mode 100644 src/Werkr.Server/graph-ui/package.json create mode 100644 src/Werkr.Server/graph-ui/scripts/check-bundle-size.mjs create mode 100644 src/Werkr.Server/graph-ui/src/dag/annotation-node.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/changeset.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/clipboard-handler.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/create-graph.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/cycle-detection.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/dag-editor.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/dag-readonly.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/dag-types.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/dagre.d.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/dnd-handler.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/draft-storage.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/editor-bindings.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/export-handler.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/graph-bindings.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/layout-engine.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/parallel-lanes.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/status-overlay.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/werkr-edge.ts create mode 100644 src/Werkr.Server/graph-ui/src/dag/werkr-node.ts create mode 100644 src/Werkr.Server/graph-ui/src/index.ts create mode 100644 src/Werkr.Server/graph-ui/src/timeline/gantt-item-dto.ts create mode 100644 src/Werkr.Server/graph-ui/src/timeline/timeline-items.ts create mode 100644 src/Werkr.Server/graph-ui/src/timeline/timeline-options.ts create mode 100644 src/Werkr.Server/graph-ui/src/timeline/timeline-styles.ts create mode 100644 src/Werkr.Server/graph-ui/src/timeline/timeline-view.ts create mode 100644 src/Werkr.Server/graph-ui/src/types/dotnet-interop.d.ts create mode 100644 src/Werkr.Server/graph-ui/test/dag/changeset.test.ts create mode 100644 src/Werkr.Server/graph-ui/test/dag/clipboard-handler.test.ts create mode 100644 src/Werkr.Server/graph-ui/test/dag/cycle-detection.test.ts create mode 100644 src/Werkr.Server/graph-ui/test/dag/draft-storage.test.ts create mode 100644 src/Werkr.Server/graph-ui/test/smoke.test.ts create mode 100644 src/Werkr.Server/graph-ui/test/timeline/timeline-items.test.ts create mode 100644 src/Werkr.Server/graph-ui/test/timeline/timeline-styles.test.ts create mode 100644 src/Werkr.Server/graph-ui/tsconfig.json create mode 100644 src/Werkr.Server/graph-ui/vitest.config.ts create mode 100644 src/Werkr.Server/wwwroot/lib/bootstrap-icons/bootstrap-icons.min.css create mode 100644 src/Werkr.Server/wwwroot/lib/bootstrap-icons/fonts/bootstrap-icons.woff create mode 100644 src/Werkr.Server/wwwroot/lib/bootstrap-icons/fonts/bootstrap-icons.woff2 diff --git a/.github/workflows/DocFX_gh-pages.yml b/.github/workflows/DocFX_gh-pages.yml index 0c3744a..c61978b 100644 --- a/.github/workflows/DocFX_gh-pages.yml +++ b/.github/workflows/DocFX_gh-pages.yml @@ -3,116 +3,57 @@ on: push: branches: - main + paths: + - 'docs/**' + - 'src/**/*.csproj' + - '.github/workflows/DocFX_gh-pages.yml' + +permissions: + contents: write + jobs: - document: - runs-on: windows-latest - env: - DOTNET_NOLOGO: true - DOCFX_SOURCE_BRANCH_NAME: ${{ github.ref }} - strategy: - matrix: - dotnet-version: [ '7.0.x' ] - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - name: Check out Werkr.Common - uses: actions/checkout@v2 - with: - repository: DarkgreyDevelopment/Werkr.Common - path: src/Werkr.Common - token: ${{ secrets.CI_TOKEN }} - - name: Check out Werkr.Common.Configuration - uses: actions/checkout@v2 - with: - repository: DarkgreyDevelopment/Werkr.Common.Configuration - path: src/Werkr.Common.Configuration - token: ${{ secrets.CI_TOKEN }} - - name: Check out Werkr.Installers - uses: actions/checkout@v2 - with: - repository: DarkgreyDevelopment/Werkr.Installers - path: src/Werkr.Installers - token: ${{ secrets.CI_TOKEN }} - - name: Check out Werkr.Server - uses: actions/checkout@v2 - with: - repository: DarkgreyDevelopment/Werkr.Server - path: src/Werkr.Server - token: ${{ secrets.CI_TOKEN }} - - name: Check out Werkr.Agent - uses: actions/checkout@v2 - with: - repository: DarkgreyDevelopment/Werkr.Agent - path: src/Werkr.Agent - token: ${{ secrets.CI_TOKEN }} - - name: Get DocFX - shell: pwsh - run: | - $IWRParams = @{ - Uri = "https://github.com/dotnet/docfx/releases/download/v2.59.4/docfx.zip" - OutFile = '${{ github.workspace }}/docfx.zip' - Method = 'Get' - } - Invoke-WebRequest @IWRParams - Expand-Archive -Path '${{ github.workspace }}/docfx.zip' -DestinationPath '${{ github.workspace }}/docfx' - - name: Custom File processing. - shell: pwsh - run: | - $DocsPath = '${{ github.workspace }}/docs' - $CopyParams = @{ - Verbose = $true - Force = $true - } - copy-item -Path '${{ github.workspace }}/LICENSE' -Destination "$DocsPath/LICENSE.md" @CopyParams - copy-item -Path '${{ github.workspace }}/README.md' -Destination "$DocsPath/index.md" @CopyParams - copy-Item -Path '${{ github.workspace }}/docs/docfx/*' -Destination $DocsPath -Exclude README.md -Verbose -Recurse - - name: Generate Documentation and build site. - shell: pwsh - run: | - Write-Host "`nGenerating API documentation:" - & '${{ github.workspace }}/docfx/docfx.exe' metadata '${{ github.workspace }}/docs/docfx.json' - Write-Host "`nCreating docfx site:" - & '${{ github.workspace }}/docfx/docfx.exe' '${{ github.workspace }}/docs/docfx.json' - - name: Compress Site for upload as Artifact. - shell: pwsh - run: | - $CopyToSiteParams = @{ - Destination = '${{ github.workspace }}/docs/_site' - Verbose = $true - } - copy-item -Path '${{ github.workspace }}/docs/CNAME' @CopyToSiteParams - copy-item -Path '${{ github.workspace }}/docs/_config.yml' @CopyToSiteParams - Write-Host "`nCompressing Site for Artifact Upload" - Compress-Archive -Path '${{ github.workspace }}/docs/_site' -DestinationPath '${{ github.workspace }}/docs/_site.zip' - - name: Upload Artifacts - uses: actions/upload-artifact@v1 - with: - name: site - path: ${{ github.workspace }}/docs/_site.zip - publish: - needs: document + build-and-publish: runs-on: ubuntu-latest env: DOTNET_NOLOGO: true - DOCFX_SOURCE_BRANCH_NAME: ${{ github.ref }} - strategy: - matrix: - dotnet-version: [ '7.0.x' ] + DOTNET_CLI_TELEMETRY_OPTOUT: true steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Download Artifacts - uses: actions/download-artifact@v1 - with: - name: site - path: ${{ github.workspace }}/download - - name: Verify WorkSpace Contents - shell: pwsh + - name: Check out repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Install DocFX + run: dotnet tool install -g docfx --version 2.78.3 + + - name: Prepare documentation sources + run: | + cp LICENSE docs/LICENSE.md + cp README.md docs/index.md + cp -r docs/docfx/* docs/ 2>/dev/null || true + + - name: Generate API metadata + run: docfx metadata docs/docfx.json + + - name: Build documentation site + run: docfx docs/docfx.json + + - name: Copy CNAME and config run: | - Write-Host "`Extracting Site." - Expand-Archive -Path '${{ github.workspace }}/download/_site.zip' -DestinationPath '${{ github.workspace }}' - - name: Publish Site Content + cp docs/CNAME docs/_site/ 2>/dev/null || true + cp docs/_config.yml docs/_site/ 2>/dev/null || true + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: docfx-site + path: docs/_site + + - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@v4 with: - BRANCH: gh-pages - FOLDER: ${{ github.workspace }}/_site + branch: gh-pages + folder: docs/_site diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9e6789..f292a6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,25 @@ jobs: echo "assemblySemFileVer=$(echo $VERSION_JSON | jq -r '.AssemblySemFileVer')" >> $GITHUB_OUTPUT echo "informationalVersion=$(echo $VERSION_JSON | jq -r '.InformationalVersion')" >> $GITHUB_OUTPUT + - name: Setup Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: src/Werkr.Server/graph-ui/package-lock.json + + - name: Install graph-ui dependencies + run: npm ci --prefix src/Werkr.Server/graph-ui + + - name: Run JS tests + run: npm test --prefix src/Werkr.Server/graph-ui + + - name: Build JS bundles (production) + run: npm run build:prod --prefix src/Werkr.Server/graph-ui + + - name: Check bundle sizes + run: node src/Werkr.Server/graph-ui/scripts/check-bundle-size.mjs + - name: Restore dependencies run: | # Backup lock files before restore diff --git a/.gitignore b/.gitignore index a986f85..a58b8c7 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ docs/projects/* # Visual Studio Code Configuration .vscode/* +!.vscode/tasks.json # Visual Studio Configuration .vs/* @@ -78,6 +79,10 @@ docs/projects/* # SQLite Database Files *.db + +# Node / TypeScript build artifacts +node_modules/ +**/wwwroot/js/dist/ *.db-shm *.db-wal diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..63b4202 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,442 @@ +{ + "version": "2.0.0", + "inputs": [ + { + "id": "migrationName", + "type": "promptString", + "description": "Name for the EF Core migration", + "default": "migration" + } + ], + "tasks": [ + { + "label": "verify:restore", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "restore", + "Werkr.slnx" + ], + "group": "build", + "problemMatcher": "$msCompile", + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "showReuseMessage": false + } + }, + { + "label": "verify:build", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "build", + "Werkr.slnx" + ], + "group": "build", + "problemMatcher": "$msCompile", + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "showReuseMessage": false + } + }, + { + "label": "verify:format", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "format", + "Werkr.slnx" + ], + "problemMatcher": [], + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "showReuseMessage": false + } + }, + { + "label": "verify:test-unit", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "test", + "--project", + "src/Test/Werkr.Tests.Data/Werkr.Tests.Data.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:test-integration", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "test", + "--project", + "src/Test/Werkr.Tests.Server/Werkr.Tests.Server.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:test-e2e", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "test", + "--project", + "src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:start-apphost", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "run", + "--project", + "src/Werkr.AppHost/Werkr.AppHost.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:docker-check", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "docker", + "compose", + "config" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:docker-build", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-File", + "${workspaceFolder}/scripts/docker-build.ps1" + ], + "group": "build", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "docker:start", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "docker", + "compose", + "up", + "-d" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "docker:stop", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "docker", + "compose", + "down" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "docker:restart", + "dependsOn": [ + "docker:stop", + "docker:start" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:app:postgres", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data", + "--startup-project", + "src/Werkr.Api", + "--context", + "PostgresWerkrDbContext", + "--output-dir", + "Migrations/Postgres" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:app:sqlite", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data", + "--startup-project", + "src/Werkr.Api", + "--context", + "SqliteWerkrDbContext", + "--output-dir", + "Migrations/Sqlite" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:app:both", + "dependsOn": [ + "ef:app:postgres", + "ef:app:sqlite" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:identity:postgres", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data.Identity", + "--startup-project", + "src/Werkr.Server", + "--context", + "PostgresWerkrIdentityDbContext", + "--output-dir", + "Migrations/Postgres" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:identity:sqlite", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data.Identity", + "--startup-project", + "src/Werkr.Server", + "--context", + "SqliteWerkrIdentityDbContext", + "--output-dir", + "Migrations/Sqlite" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:identity:both", + "dependsOn": [ + "ef:identity:postgres", + "ef:identity:sqlite" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:all", + "dependsOn": [ + "ef:app:both", + "ef:identity:both" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:build-server", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet build src/Werkr.Server/Werkr.Server.csproj'" + }, + { + "label": "verify:test-e2e-verbose", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --verbosity normal 2>&1 | tail -50'" + }, + { + "label": "verify:test-e2e-failures", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj -- --report-trx 2>&1 | grep -i -E \"failed|error|FAIL\" | head -20'" + }, + { + "label": "verify:e2e-fail-detail", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --no-build 2>&1 | grep -i -E \"failed|FAIL\" | head -20'" + }, + { + "label": "verify:e2e-tail", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --no-build 2>&1 | tail -30'" + } + ] +} \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index b6542c9..f13b128 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,34 +15,34 @@ - - + + - + - + - - - + + + - + - - - - + + + + @@ -53,7 +53,7 @@ - + @@ -67,8 +67,8 @@ - - - + + + \ No newline at end of file diff --git a/src/Installer/Msi/Agent/packages.lock.json b/src/Installer/Msi/Agent/packages.lock.json index d8ec7ee..4c73bd4 100644 --- a/src/Installer/Msi/Agent/packages.lock.json +++ b/src/Installer/Msi/Agent/packages.lock.json @@ -1,23 +1,23 @@ -{ - "version": 2, - "dependencies": { - "native,Version=v0.0": { - "WixToolset.UI.wixext": { - "type": "Direct", - "requested": "[6.0.2, )", - "resolved": "6.0.2", - "contentHash": "1Hq+Kp4WTb9TLRLRpv/iGod6MnNadz1ZrmY1USS6SL/WVxFuiBvQVGGLXrcdQld9J7oVWCZ8k9eMFtaYJOJ5AQ==" - }, - "WixToolset.Util.wixext": { - "type": "Direct", - "requested": "[6.0.2, )", - "resolved": "6.0.2", - "contentHash": "plP64ub/0KjNbtLeaeiibVCPkKfr439WTKZmTwVSoQ4fznLHBZLsE0+wcyk6dA5cQuQsD5hlmnVGTKgPioiusQ==" - } - }, - "native,Version=v0.0/win": {}, - "native,Version=v0.0/win-arm64": {}, - "native,Version=v0.0/win-x64": {}, - "native,Version=v0.0/win-x86": {} - } +{ + "version": 2, + "dependencies": { + "native,Version=v0.0": { + "WixToolset.UI.wixext": { + "type": "Direct", + "requested": "[6.0.2, )", + "resolved": "6.0.2", + "contentHash": "1Hq+Kp4WTb9TLRLRpv/iGod6MnNadz1ZrmY1USS6SL/WVxFuiBvQVGGLXrcdQld9J7oVWCZ8k9eMFtaYJOJ5AQ==" + }, + "WixToolset.Util.wixext": { + "type": "Direct", + "requested": "[6.0.2, )", + "resolved": "6.0.2", + "contentHash": "plP64ub/0KjNbtLeaeiibVCPkKfr439WTKZmTwVSoQ4fznLHBZLsE0+wcyk6dA5cQuQsD5hlmnVGTKgPioiusQ==" + } + }, + "native,Version=v0.0/win": {}, + "native,Version=v0.0/win-arm64": {}, + "native,Version=v0.0/win-x64": {}, + "native,Version=v0.0/win-x86": {} + } } \ No newline at end of file diff --git a/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj b/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj index 14c2500..ae52c1e 100644 --- a/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj +++ b/src/Installer/Msi/CustomActions/Werkr.Installer.Msi.CustomActions.csproj @@ -30,7 +30,7 @@ - + diff --git a/src/Installer/Msi/CustomActions/packages.lock.json b/src/Installer/Msi/CustomActions/packages.lock.json index 52e6310..3ad53ee 100644 --- a/src/Installer/Msi/CustomActions/packages.lock.json +++ b/src/Installer/Msi/CustomActions/packages.lock.json @@ -4,16 +4,16 @@ ".NETFramework,Version=v4.8.1": { "System.Text.Json": { "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1tRPRt8D/kzjGL7em1uJ3iJlvVIC3G/sZJ+ZgSvtVYLXGGO26Clkqy2b5uts/pyR706Yw8/xA7exeI2PI50dpw==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "10.0.4", + "Microsoft.Bcl.AsyncInterfaces": "10.0.5", "System.Buffers": "4.6.1", - "System.IO.Pipelines": "10.0.4", + "System.IO.Pipelines": "10.0.5", "System.Memory": "4.6.3", "System.Runtime.CompilerServices.Unsafe": "6.1.2", - "System.Text.Encodings.Web": "10.0.4", + "System.Text.Encodings.Web": "10.0.5", "System.Threading.Tasks.Extensions": "4.6.3", "System.ValueTuple": "4.6.1" } @@ -35,8 +35,8 @@ }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "2JmoSZ1wDf1/TUyTtLTXeicXCnWxXkeStGnzRRmAw+5CBIGhg6q9ieJXu4FjeLzawSGd5PMhcropNa3lPJDaKA==", + "resolved": "10.0.5", + "contentHash": "hQB3Hq1LlF0NkGVNyZIvwIQIY3LM7Cw1oYjNiTvdNqmzzipVAWEK1c5sj2H5aFX0udnjgPLxSYKq2fupueS8ow==", "dependencies": { "System.Threading.Tasks.Extensions": "4.6.3" } @@ -48,8 +48,8 @@ }, "System.IO.Pipelines": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "V7+RO17s/tzCpgqyj6t5vb54HFCvrRaMEwTcKDwpoQK66DRROzSff6kqtzHyiWRj6hrQQUmW80NL4pFSNhYpYA==", + "resolved": "10.0.5", + "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==", "dependencies": { "System.Buffers": "4.6.1", "System.Memory": "4.6.3", @@ -78,8 +78,8 @@ }, "System.Text.Encodings.Web": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "6g3B7jNsPRNf4luuYt1qE4R8S3JI+zMsfGWL9Idv4Mk1Z9Gh+rCagp9sG3AejPS87yBj1DjopM4i3wSz0WnEqg==", + "resolved": "10.0.5", + "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==", "dependencies": { "System.Buffers": "4.6.1", "System.Memory": "4.6.3", diff --git a/src/Installer/Msi/Server/packages.lock.json b/src/Installer/Msi/Server/packages.lock.json index d8ec7ee..4c73bd4 100644 --- a/src/Installer/Msi/Server/packages.lock.json +++ b/src/Installer/Msi/Server/packages.lock.json @@ -1,23 +1,23 @@ -{ - "version": 2, - "dependencies": { - "native,Version=v0.0": { - "WixToolset.UI.wixext": { - "type": "Direct", - "requested": "[6.0.2, )", - "resolved": "6.0.2", - "contentHash": "1Hq+Kp4WTb9TLRLRpv/iGod6MnNadz1ZrmY1USS6SL/WVxFuiBvQVGGLXrcdQld9J7oVWCZ8k9eMFtaYJOJ5AQ==" - }, - "WixToolset.Util.wixext": { - "type": "Direct", - "requested": "[6.0.2, )", - "resolved": "6.0.2", - "contentHash": "plP64ub/0KjNbtLeaeiibVCPkKfr439WTKZmTwVSoQ4fznLHBZLsE0+wcyk6dA5cQuQsD5hlmnVGTKgPioiusQ==" - } - }, - "native,Version=v0.0/win": {}, - "native,Version=v0.0/win-arm64": {}, - "native,Version=v0.0/win-x64": {}, - "native,Version=v0.0/win-x86": {} - } +{ + "version": 2, + "dependencies": { + "native,Version=v0.0": { + "WixToolset.UI.wixext": { + "type": "Direct", + "requested": "[6.0.2, )", + "resolved": "6.0.2", + "contentHash": "1Hq+Kp4WTb9TLRLRpv/iGod6MnNadz1ZrmY1USS6SL/WVxFuiBvQVGGLXrcdQld9J7oVWCZ8k9eMFtaYJOJ5AQ==" + }, + "WixToolset.Util.wixext": { + "type": "Direct", + "requested": "[6.0.2, )", + "resolved": "6.0.2", + "contentHash": "plP64ub/0KjNbtLeaeiibVCPkKfr439WTKZmTwVSoQ4fznLHBZLsE0+wcyk6dA5cQuQsD5hlmnVGTKgPioiusQ==" + } + }, + "native,Version=v0.0/win": {}, + "native,Version=v0.0/win-arm64": {}, + "native,Version=v0.0/win-x64": {}, + "native,Version=v0.0/win-x86": {} + } } \ No newline at end of file diff --git a/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs b/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs index 9c7af21..d7b3816 100644 --- a/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs +++ b/src/Test/Werkr.Tests.Agent/Security/UrlValidatorTests.cs @@ -97,7 +97,10 @@ public void RelativeUrl_Rejected( ) { UnauthorizedAccessException ex = Assert.ThrowsExactly( ( ) => validator.ValidateUrl( "/api/data" ) ); - Assert.Contains( "absolute", ex.Message ); + // On Unix, Uri.TryCreate parses "/api/data" as file:///api/data (hitting scheme check). + // On Windows, it fails to parse (hitting the absolute-URI check). + bool matchesAbsoluteOrScheme = ex.Message.Contains( "absolute" ) || ex.Message.Contains( "scheme" ); + Assert.IsTrue( matchesAbsoluteOrScheme, $"Expected 'absolute' or 'scheme' in message: {ex.Message}" ); } // ══════════════════════════════════════════════════════════════════════════════ diff --git a/src/Test/Werkr.Tests.Agent/packages.lock.json b/src/Test/Werkr.Tests.Agent/packages.lock.json index 5be9d1d..21dee93 100644 --- a/src/Test/Werkr.Tests.Agent/packages.lock.json +++ b/src/Test/Werkr.Tests.Agent/packages.lock.json @@ -1,1623 +1,1623 @@ -{ - "version": 2, - "dependencies": { - "net10.0": { - "Microsoft.Extensions.TimeProvider.Testing": { - "type": "Direct", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "uJ8n9WUEzux9I2CjZh7imGBgZadfwhAKlxuBq7GsNGL8FJF81aHXAYaRMnwW+9EvRFQNytu7xo1ffeuuTncAzg==" - }, - "Microsoft.PowerShell.SDK": { - "type": "Direct", - "requested": "[7.6.0-rc.1, )", - "resolved": "7.6.0-rc.1", - "contentHash": "0AAObi5+pcXKD+4CACnn30WXhe0cP9+5VcZeopAEpKTQRhPrBtYiS2ACMTy72oHJGojCcaX6n+7WW2xuTEd8dg==", - "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "10.0.3", - "Microsoft.Extensions.ObjectPool": "10.0.3", - "Microsoft.Management.Infrastructure.CimCmdlets": "7.6.0-rc.1", - "Microsoft.PowerShell.Commands.Diagnostics": "7.6.0-rc.1", - "Microsoft.PowerShell.Commands.Management": "7.6.0-rc.1", - "Microsoft.PowerShell.Commands.Utility": "7.6.0-rc.1", - "Microsoft.PowerShell.ConsoleHost": "7.6.0-rc.1", - "Microsoft.PowerShell.Security": "7.6.0-rc.1", - "Microsoft.WSMan.Management": "7.6.0-rc.1", - "Microsoft.Win32.Registry.AccessControl": "10.0.3", - "Microsoft.Win32.SystemEvents": "10.0.3", - "Microsoft.Windows.Compatibility": "10.0.3", - "System.CodeDom": "10.0.3", - "System.ComponentModel.Composition": "10.0.3", - "System.ComponentModel.Composition.Registration": "10.0.3", - "System.Configuration.ConfigurationManager": "10.0.3", - "System.Data.Odbc": "10.0.3", - "System.Data.OleDb": "10.0.3", - "System.Data.SqlClient": "4.9.0", - "System.Diagnostics.EventLog": "10.0.3", - "System.Diagnostics.PerformanceCounter": "10.0.3", - "System.DirectoryServices": "10.0.3", - "System.DirectoryServices.AccountManagement": "10.0.3", - "System.DirectoryServices.Protocols": "10.0.3", - "System.Drawing.Common": "10.0.3", - "System.IO.Packaging": "10.0.3", - "System.IO.Ports": "10.0.3", - "System.Management": "10.0.3", - "System.Management.Automation": "7.6.0-rc.1", - "System.Net.Http.WinHttpHandler": "10.0.3", - "System.Reflection.Context": "10.0.3", - "System.Runtime.Caching": "10.0.3", - "System.Security.Cryptography.Pkcs": "10.0.3", - "System.Security.Cryptography.ProtectedData": "10.0.3", - "System.Security.Cryptography.Xml": "10.0.3", - "System.Security.Permissions": "10.0.3", - "System.ServiceModel.Http": "10.0.652802", - "System.ServiceModel.NetFramingBase": "10.0.652802", - "System.ServiceModel.NetTcp": "10.0.652802", - "System.ServiceModel.Primitives": "10.0.652802", - "System.ServiceModel.Syndication": "10.0.3", - "System.ServiceProcess.ServiceController": "10.0.3", - "System.Speech": "10.0.3" - } - }, - "MSTest": { - "type": "Direct", - "requested": "[4.1.0, )", - "resolved": "4.1.0", - "contentHash": "2bk47yg7HcHRyf6Zf0XgCZicTVTQj4D5lonYTO7lWMxCQB+x66VrQNc2dADBfzthKXfHaA37m8i+VV5h6SbWiA==", - "dependencies": { - "MSTest.TestAdapter": "4.1.0", - "MSTest.TestFramework": "4.1.0", - "Microsoft.NET.Test.Sdk": "18.0.1", - "Microsoft.Testing.Extensions.CodeCoverage": "18.4.1", - "Microsoft.Testing.Extensions.TrxReport": "2.1.0" - } - }, - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.6.2", - "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w==" - }, - "Grpc.AspNetCore.Server": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "diSC/ZeNdSdxHdYSOpYwuSBBDYpuNVtJQFJfiBB0WrYOQ4lVMmdxuUZJcViahQyo8pCvS3Mueo5lqFxwwMF/iw==", - "dependencies": { - "Grpc.Net.Common": "2.76.0" - } - }, - "Grpc.AspNetCore.Server.ClientFactory": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "y5KGO1GO0N2L/hCCMR05mmoK8j+v8rKvZ+9nothAxKx2Tf2CwV8f4TM5K0GkKfDsp4vrc4lm90MU6E+DeN7YIw==", - "dependencies": { - "Grpc.AspNetCore.Server": "2.76.0", - "Grpc.Net.ClientFactory": "2.76.0" - } - }, - "Grpc.Core.Api": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ==" - }, - "Grpc.Net.Common": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==", - "dependencies": { - "Grpc.Core.Api": "2.76.0" - } - }, - "Humanizer.Core": { - "type": "Transitive", - "resolved": "2.14.1", - "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" - }, - "Json.More.Net": { - "type": "Transitive", - "resolved": "2.1.1", - "contentHash": "ZXAKl2VsdnIZeUo1PFII3Oi1m1L4YQjEyDjygHfHln5vgsjgIo749X6xWkv7qFYp8RROES+vOEfDcvvoVgs8kA==" - }, - "JsonPointer.Net": { - "type": "Transitive", - "resolved": "5.3.1", - "contentHash": "3e2OJjU0OaE26XC/klgxbJuXvteFWTDJIJv0ITYWcJEoskq7jzUwPSC1s0iz4wPPQnfN7vwwFmg2gJfwRAPwgw==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Json.More.Net": "2.1.1" - } - }, - "JsonSchema.Net": { - "type": "Transitive", - "resolved": "7.4.0", - "contentHash": "5T3DWENwuCzLwFWz0qjXXVWA8+5+gC95OLkhqUBWpVpWBMr9gwfhWNeX8rWyr+fLQ7pIQ+lWuHIrmXRudxOOSw==", - "dependencies": { - "JsonPointer.Net": "5.3.1" - } - }, - "Markdig.Signed": { - "type": "Transitive", - "resolved": "0.44.0", - "contentHash": "mNxf8HrQA/clO8usqQhVc0BGlw0bJtZ76dic5KZGBPJZDX4UR67Jglwilkp5A//gPSMwcoY5EjLPppkZ/B4IMg==" - }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.23.0", - "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" - }, - "Microsoft.AspNetCore.Metadata": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w==" - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "TV62UsrJZPX6gbt3c4WrtXh7bmaDIcMqf9uft1cc4L6gJXOU07hDGEh+bFQh/L2Az0R1WVOkiT66lFqS6G2NmA==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.11.0", - "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.11.0", - "Microsoft.CodeAnalysis.Common": "[5.0.0]" - } - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" - }, - "Microsoft.DiaSymReader": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite.Core": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==", - "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.4", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.Extensions.AmbientMetadata.Application": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "bovnONzrr/JIc+w343i857rJEb7cQH9UzEjbV5n67agWBEYICGQb8xiqYz5+GoFXp6mKEKLwYCQGttMU1p5yXQ==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "uDRooaV6N3WZ0kdlNPMB68/MdGn/in1Fs7Db7DnIm85RBTPy4P321WO+daAImiYpH5dekjNggDqy1N44WaIlMA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Compliance.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "4WkknDbVrHNf+S6fwSt1OAXlGJ/G/QrtJlqx4aNzOLmeT3GRyxpGLZn+Q3UV+RMRAF6FfsijEZBg2ZAW8bTAkg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.FileExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Physical": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "NkvJ8aSr3AG30yabjv7ZWwTG/wq5OElNTlNq39Ok2HSEF3TIwAc1f1xnTJlR/GuoJmEgkfT7WBO9YbSXRk41+g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ==" - }, - "Microsoft.Extensions.DependencyInjection.AutoActivation": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "ksmUG2SFTcXzYdyoLOdeSM/qYLRGN6qbbSzYVkwMK9xsctfR1hYkUayeOpFCMd7L+QSlYX72mK9wxwdgQxyS4g==", - "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg==" - }, - "Microsoft.Extensions.Diagnostics": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "1/hQmONMWxRTKXuN0pQShQN9QsqIRTS1G4fdmKW0O9phuVZjyzIROQD9Fbfwyn2t+yvP8SzjatGAPX4jDRfgHg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Features": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "7to+nkZO+g/GiGQOBzAcrr8HcG8dXETI/hg58fJju0jPO9p/GvNLAis8kMPTBdsjfeTfslBrgFX9Yx1KRnKDww==" - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileProviders.Physical": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileSystemGlobbing": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileSystemGlobbing": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg==" - }, - "Microsoft.Extensions.Http": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "QRbs+A+WfiGTnV9KFNfWlF+My5euQNZnsvdVMulwRN6C/tEPaF+ZlQfedHoNvFHKLwjQMmqwm4z+TSO9eLvRQw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Http.Diagnostics": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "ybx2QcCWROCnUCbSj/IyHXn1c58brjjHzTTbueKgBl/qHsWk69mu25mjQ3oaMsO1I0+EcS6AhVuhIopL2q3IDw==", - "dependencies": { - "Microsoft.Extensions.Http": "10.0.4", - "Microsoft.Extensions.Telemetry": "10.4.0" - } - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "S8+6fCuMOhJZGk8sGFtOy3VsF9mk9x4UOL59GM91REiA/fmCDjunKKIw4RmStG87qyXPfxelDJf2pXIbTuaBdw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Logging.Configuration": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "XPXoOpUnWEh0pV7Vl2DK2wj47y73Krhrve5OkPrvGIWdZ4U2r47WO8hEdv+wKn65Kh4pmDdiWm7Ibo5pZX+vig==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "2pufIFOgNl/yWTOoIC9XgBnO9VxgfAjdRCnVwpE2+ICfcroGnjuEAGzJ5lTdZeAe0HvA31vMBWXtcmGB7TOq3g==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg==" - }, - "Microsoft.Extensions.Resilience": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "41CCbJJPsDWU6NsmKfANHkfT/+KCBlZZqQ1eBoQhhW0xqGCiWmUlMdi2BoaM/GcwKHX5WiQL/IESROmgk0Owfw==", - "dependencies": { - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.4.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4", - "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0", - "Polly.Extensions": "8.4.2", - "Polly.RateLimiting": "8.4.2" - } - }, - "Microsoft.Extensions.ServiceDiscovery.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "HkBb7cdi27tkQiQw1anQFbXe+A3pjRwDKgVbd/DD9fMAO2X9abK0FEyM/tNVXjW3lwOWl2tF+Xij/DqI6i+JTg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Features": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "AbHleTzdpGPjA6RpOjKVHEYx7SoBRnJ2bwAbbPa3aGB7HiVwBmeTJhBGhtIBiuIW0VpKDS8x+bV5iWqpBRIf4w==", - "dependencies": { - "Microsoft.Extensions.AmbientMetadata.Application": "10.4.0", - "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.4.0", - "Microsoft.Extensions.Logging.Configuration": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0" - } - }, - "Microsoft.Extensions.Telemetry.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "3b2uVa4voJfLLg39BPCKQS0ZgnpEZFkKf7YmnMVlM5FQJYBPOuePIQdnEK1/Oxd+w3GscxGYuE7IMOXDwixZtQ==", - "dependencies": { - "Microsoft.Extensions.Compliance.Abstractions": "10.4.0", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.IdentityModel.Abstractions": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.16.0" - } - }, - "Microsoft.Management.Infrastructure": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "cGZi0q5IujCTVYKo9h22Pw+UwfZDV82HXO8HTxMG2HqntPlT3Ls8jY6punLp4YzCypJNpfCAu2kae3TIyuAiJw==", - "dependencies": { - "Microsoft.Management.Infrastructure.Runtime.Unix": "3.0.0", - "Microsoft.Management.Infrastructure.Runtime.Win": "3.0.0" - } - }, - "Microsoft.Management.Infrastructure.CimCmdlets": { - "type": "Transitive", - "resolved": "7.6.0-rc.1", - "contentHash": "+iB/Rj2xnjHo//4E+ADeQMc8LcWy8/XNRp3HqmIAeB/vPcCeEMdY3zald6nlJ90wQ9iZFRIpBKX3MM0IDMU6kg==", - "dependencies": { - "System.Management.Automation": "7.6.0-rc.1" - } - }, - "Microsoft.Management.Infrastructure.Runtime.Unix": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "QZE3uEDvZ0m7LabQvcmNOYHp7v1QPBVMpB/ild0WEE8zqUVAP5y9rRI5we37ImI1bQmW5pZ+3HNC70POPm0jBQ==" - }, - "Microsoft.Management.Infrastructure.Runtime.Win": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "uwMyWN33+iQ8Wm/n1yoPXgFoiYNd0HzJyoqSVhaQZyJfaQrJR3udgcIHjqa1qbc3lS6kvfuUMN4TrF4U4refCQ==" - }, - "Microsoft.NET.Test.Sdk": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", - "dependencies": { - "Microsoft.CodeCoverage": "18.0.1", - "Microsoft.TestPlatform.TestHost": "18.0.1" - } - }, - "Microsoft.PowerShell.Commands.Diagnostics": { - "type": "Transitive", - "resolved": "7.6.0-rc.1", - "contentHash": "gyy6F2m0USZOLJKZrSfagZzU6gwgHY0Ttz8UqY1U3VM3KDo7hE9fq1XDyDJ/5nxUkMjKv76x2YGZoyFCqKmaww==", - "dependencies": { - "System.Management.Automation": "7.6.0-rc.1" - } - }, - "Microsoft.PowerShell.Commands.Management": { - "type": "Transitive", - "resolved": "7.6.0-rc.1", - "contentHash": "CA3FbDTpKaXWFxTqRFSD6IblV7WWQ/8ru7+xYlOpuMX6F4L7mwYkVXtytLcYLeWwpiOX5Jo8L8TdENlS3PGeVA==", - "dependencies": { - "Microsoft.PowerShell.Security": "7.6.0-rc.1", - "System.Diagnostics.EventLog": "10.0.3", - "System.ServiceProcess.ServiceController": "10.0.3" - } - }, - "Microsoft.PowerShell.Commands.Utility": { - "type": "Transitive", - "resolved": "7.6.0-rc.1", - "contentHash": "Bfece2H3c83JEoWQyrf6ka/OMEjJz4hS69GUzuLJOldtrRASnlJ8e6cIg3hjKomh2JTk+QjfL/Jt0GKTAz1nAw==", - "dependencies": { - "JsonSchema.Net": "7.4.0", - "Markdig.Signed": "0.44.0", - "Microsoft.CodeAnalysis.CSharp": "5.0.0", - "Microsoft.PowerShell.MarkdownRender": "7.2.1", - "Microsoft.Win32.SystemEvents": "10.0.3", - "System.Drawing.Common": "10.0.3", - "System.Management.Automation": "7.6.0-rc.1" - } - }, - "Microsoft.PowerShell.ConsoleHost": { - "type": "Transitive", - "resolved": "7.6.0-rc.1", - "contentHash": "tzijTbDr1gUtSTx17013KEtJS9cqU+7FXF8LldxXQOlNRnxTgJBhg4aw88EXdBDVXFe9ON1d1YpD6BignLk+6Q==", - "dependencies": { - "System.Management.Automation": "7.6.0-rc.1" - } - }, - "Microsoft.PowerShell.CoreCLR.Eventing": { - "type": "Transitive", - "resolved": "7.6.0-rc.1", - "contentHash": "4b0NmZ3mdlluet21RbUXkEMG9+aIY9f+BN2bLy/zmTeFq36MFkGE80JoY0NfIzwwJJA+NpfSj4vXWTYIwujr8Q==", - "dependencies": { - "System.Diagnostics.EventLog": "10.0.3" - } - }, - "Microsoft.PowerShell.MarkdownRender": { - "type": "Transitive", - "resolved": "7.2.1", - "contentHash": "o5oUwL23R/KnjQPD2Oi49WAG5j4O4VLo1fPRSyM/aq0HuTrY2RnF4B3MCGk13BfcmK51p9kPlHZ1+8a/ZjO4Jg==", - "dependencies": { - "Markdig.Signed": "0.31.0" - } - }, - "Microsoft.PowerShell.Native": { - "type": "Transitive", - "resolved": "700.0.0-rc.1", - "contentHash": "lJOCErHTSWwCzfp3wgeyqhNRi4t43McDc0CHqlbt3Cj3OomiqPlNHQXujSbgd+0Ir6/8QAmvU/VOYgqCyMki6A==" - }, - "Microsoft.PowerShell.Security": { - "type": "Transitive", - "resolved": "7.6.0-rc.1", - "contentHash": "Lcsqavzb3jp9MqLFr9TCsxFBylepRGUhPhkRAYDeSMYohK6K4fMkdKudWo23M5ZKgooNySkkQX2H1jF7w0ZU7w==", - "dependencies": { - "System.Management.Automation": "7.6.0-rc.1" - } - }, - "Microsoft.Security.Extensions": { - "type": "Transitive", - "resolved": "1.4.0", - "contentHash": "MnHXttc0jHbRrGdTJ+yJBbGDoa4OXhtnKXHQw70foMyAooFtPScZX/dN+Nib47nuglc9Gt29Gfb5Zl+1lAuTeA==" - }, - "Microsoft.Testing.Extensions.CodeCoverage": { - "type": "Transitive", - "resolved": "18.4.1", - "contentHash": "l1VZM9dg9s76L5D288ipAT4HRYDJ6Vxh8wX20gfS9VnpueedRfN4/aGNn4oA1g6pwq2WSM3Ci7IoSSGPiqu+WQ==", - "dependencies": { - "Microsoft.DiaSymReader": "2.0.0", - "Microsoft.Extensions.DependencyModel": "8.0.2", - "Microsoft.Testing.Platform": "2.0.2" - } - }, - "Microsoft.Testing.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "5TwgTx2u7k9Al/xbZ18QXq4Hdy2xewkVTI6K3sk+jY2ykqUkIKNuj7rFu3GOV5KnEUkevhw6eZcyZs77STHJIA==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.TrxReport": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "cXmP225WcMLLOSrW8xekaNhfzdBwXX3cbXbE5qSzmLbK0KZe3z8rAObKj70FWiPPPzm2W22x0ZW93gsmAfK6Mg==", - "dependencies": { - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.TrxReport.Abstractions": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "D8xmIJYQFJ6D49Rx5/vPrkZZxb338Jkew+eSqZLBfBiWKw4QZKy3i1BOXiLfz0lOmaNErwDz/YWRojCdNl+B9Q==", - "dependencies": { - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.VSTestBridge": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "bNRIEA2YoGr+Y+7LHdA7i1U80+7BAdf4K4Qh4Kx6eKkoBK/NV7QpoMg+GWPP0/eqAFzuUmUOIPVZ87Oo0Vyxmw==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", - "Microsoft.Testing.Extensions.Telemetry": "2.1.0", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Platform": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "aHkjNTGIA+Zbdw6RJgSFrbDrCjO0CgqpElqYcvkRSeUhBv2bKarnvU3ep786U7UqrPlArT/B7VmImRibJD0Zrg==" - }, - "Microsoft.Testing.Platform.MSBuild": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "UpfPebXQtHGrWz21+YLHmJSm+5zsuPE9U9pfdCtoB+67g75fDmWlNgpkH2ZmdVhSwkjNIed9Icg8Iu63z2ei5Q==", - "dependencies": { - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", - "Newtonsoft.Json": "13.0.3" - } - }, - "Microsoft.Win32.Registry.AccessControl": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "CxgQc/IHtQ2IhqRdN6nxZZwk/C+dnbE5GLWErze4jAUgCkPbGe+hlgbESMop57FQMGShOqrWZWQAKpZ65QTU8g==" - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "gYpwz5Gl0rs9pEFHBKctLbSi7SUGR4L1uRjXkU488nizWd2hvo2JP2+ATUsv0th7v6LDiXfdlUsnTbvC2MauFA==" - }, - "Microsoft.Windows.Compatibility": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "MIoN5L2qs5cqClZ3vvH0ZG4QxaSEAYyNeR7B5k88y0l55MKAKCn4uSfposEwpXeHqZrI1uUevA/c5YUe6hcAIw==", - "dependencies": { - "Microsoft.Win32.Registry.AccessControl": "10.0.3", - "Microsoft.Win32.SystemEvents": "10.0.3", - "System.CodeDom": "10.0.3", - "System.ComponentModel.Composition": "10.0.3", - "System.ComponentModel.Composition.Registration": "10.0.3", - "System.Configuration.ConfigurationManager": "10.0.3", - "System.Data.Odbc": "10.0.3", - "System.Data.OleDb": "10.0.3", - "System.Data.SqlClient": "4.9.0", - "System.Diagnostics.EventLog": "10.0.3", - "System.Diagnostics.PerformanceCounter": "10.0.3", - "System.DirectoryServices": "10.0.3", - "System.DirectoryServices.AccountManagement": "10.0.3", - "System.DirectoryServices.Protocols": "10.0.3", - "System.Drawing.Common": "10.0.3", - "System.IO.Packaging": "10.0.3", - "System.IO.Ports": "10.0.3", - "System.Management": "10.0.3", - "System.Reflection.Context": "10.0.3", - "System.Runtime.Caching": "10.0.3", - "System.Security.Cryptography.Pkcs": "10.0.3", - "System.Security.Cryptography.ProtectedData": "10.0.3", - "System.Security.Cryptography.Xml": "10.0.3", - "System.Security.Permissions": "10.0.3", - "System.ServiceModel.Http": "8.1.2", - "System.ServiceModel.NetTcp": "8.1.2", - "System.ServiceModel.Primitives": "8.1.2", - "System.ServiceModel.Syndication": "10.0.3", - "System.ServiceProcess.ServiceController": "10.0.3", - "System.Speech": "10.0.3", - "System.Web.Services.Description": "8.1.2" - } - }, - "Microsoft.WSMan.Management": { - "type": "Transitive", - "resolved": "7.6.0-rc.1", - "contentHash": "AsZLXiO6qyrvmk6e4S4qabpbysq+5+veGmhDJUDMJZdyq0ORmFB7GiWD5aYHwSebKeJSItV51lwNcIdo1JgbQA==", - "dependencies": { - "Microsoft.WSMan.Runtime": "7.6.0-rc.1", - "System.Diagnostics.EventLog": "10.0.3", - "System.Management.Automation": "7.6.0-rc.1", - "System.ServiceProcess.ServiceController": "10.0.3" - } - }, - "Microsoft.WSMan.Runtime": { - "type": "Transitive", - "resolved": "7.6.0-rc.1", - "contentHash": "HFG8NIyKCrZ7SM/lgewFpEQ13kbsRQm6c8ihdo2v+Am0XfftCw+cxzKOpNXlti8ZBEkPksZzkbxlPeAds0PZGA==" - }, - "MimeKit": { - "type": "Transitive", - "resolved": "4.15.1", - "contentHash": "cxCcQhD0zhboFoG136jJuJtQjNRDJ+BxBm3f2vWn+53bff/CRo+K1mAkWjsW4Wuyy5O22F40MdMG2nRzQu1cJw==", - "dependencies": { - "BouncyCastle.Cryptography": "2.6.2", - "System.Security.Cryptography.Pkcs": "10.0.0" - } - }, - "MSTest.Analyzers": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "4ElL/aqomiUInr090VN4udqz46AuszXLrifHkLrgj0zb7na8eAoyUQt3BwDLTcGd1bSkmk3SfD02rZtKU+ZiqQ==" - }, - "MSTest.TestAdapter": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "bRW1Hftwq0XbcVExcAbj4YAfSZDRAziL0mygDkPBvaUe2nSsWFQIatze5lHVjPFJMvSFgWnItku4pguIy5FowQ==", - "dependencies": { - "MSTest.TestFramework": "4.1.0", - "Microsoft.Testing.Extensions.VSTestBridge": "2.1.0", - "Microsoft.Testing.Platform.MSBuild": "2.1.0" - } - }, - "MSTest.TestFramework": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "BzpvsK+CRbk6khwY62h+7HfYzIxtJXyPv9tOI9T90cy5CVy+WI1JkN4ZaNL4Dobqb6dywSwabLTIbPZKpdrr+A==", - "dependencies": { - "MSTest.Analyzers": "4.1.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.4", - "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" - }, - "Npgsql": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "10.0.0" - } - }, - "OpenTelemetry": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==", - "dependencies": { - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", - "Microsoft.Extensions.Logging.Configuration": "10.0.0", - "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0" - } - }, - "OpenTelemetry.Api": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w==" - }, - "OpenTelemetry.Api.ProviderBuilderExtensions": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", - "OpenTelemetry.Api": "1.15.0" - } - }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, - "Polly.Extensions": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Polly.Core": "8.4.2" - } - }, - "Polly.RateLimiting": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", - "dependencies": { - "Polly.Core": "8.4.2", - "System.Threading.RateLimiting": "8.0.0" - } - }, - "runtime.android-arm.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "6W4qZX0X7FF+PHM9Kaa5ZsTLcGJAzCU7FB4Tjy1vTg2rUIEjDqijWTtpz8vY6gBzZaG+tD0/EKUyGnfq6d/d/Q==" - }, - "runtime.android-arm64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "4OdNg2Du1kvm0b4tSErkA4wfH32YmgUqeSLzekk6NdCR61brCp4vttTJl0epZMwbRppXuy/jY7pV0WtP4ymnfQ==" - }, - "runtime.android-x64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "nP7xJSDSqRktV4kEd0kr4n0xhzJFuQjh7L/AgrKB3C4QK5TA/8NimJmTl1ogngvnXn2vilH2Hk+keBIvuoGWCA==" - }, - "runtime.android-x86.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "f/neqijoVpgl/PcrXYeLNfCC2t74XYIcaHvtSaF3zaIdJ3hf5wrwRC9gzYLXHRLg/j4DRsCHEw8rlux/dNh7XA==" - }, - "runtime.linux-arm.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "cwiQmy95Zd42K4kMzDt8GkXNKHWDkRZIIyb3MteRrgJKubH2DMA6VY3JiFeR+1hQ7i31Ck+dYyt8wUkElZ/NrQ==" - }, - "runtime.linux-arm64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "cEx+Xm0ZNPsuoYTFwXZ6qRstwXQ7vJjbb3jWzo5s5xIeEgpTDdfiUjTgK3Gl618mWgb7+Gn6Vt5pT07RxFC28Q==" - }, - "runtime.linux-bionic-arm64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "kB21sNzlKgTj3V/LZhnTLFuViiGKtkiUG2GdEW+z3jdeUtRoxqBBXryzMqy0oUOy5idBW2pA6bh+iLGaj+GThA==" - }, - "runtime.linux-bionic-x64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "/Ud7EYdpnBGJ/x7DdqVFEJOQtEcf9rsD6/1iyUBJNfhhs8JljZLewJHp1cXr1DVCaWWc9jSWIc4g+QpMct/22A==" - }, - "runtime.linux-musl-arm.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "pBubukkmXD9e4Ju004bEHL3TE5mYahONCVwrJ5b+7/cj8smSY2H5sKTyURiVF1EfX55yORKyTyQ3dALA5yTTlg==" - }, - "runtime.linux-musl-arm64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "+FR6SvpZya66Wv26MVEK4l2gnjfZnBVUz675eaTcMQ/KWAy+GMaPyhRaBIMnQJf5gfuu3x1qgeAzpmeFkOPGBw==" - }, - "runtime.linux-musl-x64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "Ee1XnOSKgeJGBcnd7LLLkyXEbE/JcpQ3pUlbPXqugl0YD55jVubpYebzGYI+ilrnBLCRiV0BR822b2XmerIKGw==" - }, - "runtime.linux-x64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "U+Zr1KuBDIIwgR2gTMFbBAtkr4WnKdEXxmknPa3X8Qy6oCi4+MTRmq2zmTh7wxXQXTdmhfXg2tyy7DuVHtB+xQ==" - }, - "runtime.maccatalyst-arm64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "9yTgh3ZKCBTGEO4YS76g/K15s2qSdeEZDqWteixUPeRsETsuBUwUPF39Fk39OPlRgwhqEuhtHy1Hf6BSOETfAg==" - }, - "runtime.maccatalyst-x64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "0gxqUZYQ1T1o5VzgyEMKiSPKJtPbleF2nbjZa5QKZQvPgSlrZFL+FrW4LywIk2scOpXu+Vd5Mv8AGGV7slhYWA==" - }, - "runtime.native.System.Data.SqlClient.sni": { - "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "A8v6PGmk+UGbfWo5Ixup0lPM4swuSwOiayJExZwKIOjTlFFQIsu3QnDXECosBEyrWSPryxBVrdqtJyhK3BaupQ==", - "dependencies": { - "runtime.win-arm64.runtime.native.System.Data.SqlClient.sni": "4.4.0", - "runtime.win-x64.runtime.native.System.Data.SqlClient.sni": "4.4.0", - "runtime.win-x86.runtime.native.System.Data.SqlClient.sni": "4.4.0" - } - }, - "runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "7U3HW0JzAeg9PWaYBcyWLFMq2RSexgJ8uVCR3E2QVJJ/IZnRT0wYRhyEx3zBSbKYzN3KE6MDiRd6CN4hR7YkqA==", - "dependencies": { - "runtime.android-arm.runtime.native.System.IO.Ports": "10.0.3", - "runtime.android-arm64.runtime.native.System.IO.Ports": "10.0.3", - "runtime.android-x64.runtime.native.System.IO.Ports": "10.0.3", - "runtime.android-x86.runtime.native.System.IO.Ports": "10.0.3", - "runtime.linux-arm.runtime.native.System.IO.Ports": "10.0.3", - "runtime.linux-arm64.runtime.native.System.IO.Ports": "10.0.3", - "runtime.linux-bionic-arm64.runtime.native.System.IO.Ports": "10.0.3", - "runtime.linux-bionic-x64.runtime.native.System.IO.Ports": "10.0.3", - "runtime.linux-musl-arm.runtime.native.System.IO.Ports": "10.0.3", - "runtime.linux-musl-arm64.runtime.native.System.IO.Ports": "10.0.3", - "runtime.linux-musl-x64.runtime.native.System.IO.Ports": "10.0.3", - "runtime.linux-x64.runtime.native.System.IO.Ports": "10.0.3", - "runtime.maccatalyst-arm64.runtime.native.System.IO.Ports": "10.0.3", - "runtime.maccatalyst-x64.runtime.native.System.IO.Ports": "10.0.3", - "runtime.osx-arm64.runtime.native.System.IO.Ports": "10.0.3", - "runtime.osx-x64.runtime.native.System.IO.Ports": "10.0.3" - } - }, - "runtime.osx-arm64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "qnEjXlIazxaRAhBet0EupWoJQ9PKsXjThpMKGP/ZsCjZgHEb+zs9wft82wY27OagyPOCg9D4hJHWKt5LTbJNZg==" - }, - "runtime.osx-x64.runtime.native.System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "L1gcOL5kcfeeujKQbqJmpeTLsT0c6DeaJ9w6PY9oZ+/WESyya+Zj7StKguXR5j93o3o86FjcN05yufpoCz7R2g==" - }, - "runtime.win-arm64.runtime.native.System.Data.SqlClient.sni": { - "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "LbrynESTp3bm5O/+jGL8v0Qg5SJlTV08lpIpFesXjF6uGNMWqFnUQbYBJwZTeua6E/Y7FIM1C54Ey1btLWupdg==" - }, - "runtime.win-x64.runtime.native.System.Data.SqlClient.sni": { - "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "38ugOfkYJqJoX9g6EYRlZB5U2ZJH51UP8ptxZgdpS07FgOEToV+lS11ouNK2PM12Pr6X/PpT5jK82G3DwH/SxQ==" - }, - "runtime.win-x86.runtime.native.System.Data.SqlClient.sni": { - "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" - }, - "Serilog": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" - }, - "Serilog.Extensions.Hosting": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", - "Microsoft.Extensions.Logging.Abstractions": "10.0.0", - "Serilog": "4.3.0", - "Serilog.Extensions.Logging": "10.0.0" - } - }, - "Serilog.Extensions.Logging": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", - "dependencies": { - "Microsoft.Extensions.Logging": "10.0.0", - "Serilog": "4.2.0" - } - }, - "Serilog.Formatting.Compact": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "Serilog.Settings.Configuration": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "10.0.0", - "Microsoft.Extensions.DependencyModel": "10.0.0", - "Serilog": "4.3.0" - } - }, - "Serilog.Sinks.Debug": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "SQLitePCLRaw.bundle_e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", - "dependencies": { - "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", - "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" - } - }, - "SQLitePCLRaw.core": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" - }, - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" - }, - "SQLitePCLRaw.provider.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", - "dependencies": { - "SQLitePCLRaw.core": "2.1.11" - } - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "+G1mBhHJp8taiDcHK1gmckOZ884n9JeIrS1dzFYZhSa8oTiIF+EagIyAyCOQzdBbbES7eb9AkjGBlUZqqoCaeg==" - }, - "System.ComponentModel.Composition": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "qBShXNCMMY4rTWvh8Y9BombIyng4LUqbfZd3yx93D88YKf2w1MuYpC8J4XSxSVQDubcoB9AMUwWe6IcVzo3/Sg==" - }, - "System.ComponentModel.Composition.Registration": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "Dtb5UBzyHH3RnVbVSOTE62X6eIYmbStV3z4jk7aew34YVTMltAeqN6p5KffqdOo8MO8ARFmpZZDcfwJeIbuGsQ==", - "dependencies": { - "System.ComponentModel.Composition": "10.0.3", - "System.Reflection.Context": "10.0.3" - } - }, - "System.Configuration.ConfigurationManager": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "69ZT/MYxQSxwdiiHRtI08noXiG5drj/bXDDZISmeWkNUtbIfYgmTiof16tCVOLTdmSQY7W7gwxkMliKdreWHGQ==", - "dependencies": { - "System.Diagnostics.EventLog": "10.0.3", - "System.Security.Cryptography.ProtectedData": "10.0.3" - } - }, - "System.Data.Odbc": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "/uKHUwmcXdq06LisgTUC85pKx8xFwFBWKoVLwawW88mKIrUXMCtDxE8c1bixoHOWCI2tesvmZuxo6EgyDVZO/w==" - }, - "System.Data.OleDb": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "/YVeBa7iHwhvokst2neLLVBwhcTEcOSjLZPPhjBEPHVxkY5WsffnJ6pya2DhdaKsbCYsWAJfyvnoAPGIE1gwYA==", - "dependencies": { - "System.Configuration.ConfigurationManager": "10.0.3", - "System.Diagnostics.PerformanceCounter": "10.0.3" - } - }, - "System.Data.SqlClient": { - "type": "Transitive", - "resolved": "4.9.0", - "contentHash": "j4KJO+vC62NyUtNHz854njEqXbT8OmAa5jb1nrGfYWBOcggyYUQE0w/snXeaCjdvkSKWuUD+hfvlbN8pTrJTXg==", - "dependencies": { - "runtime.native.System.Data.SqlClient.sni": "4.4.0" - } - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "+bZnyzt0/vt4g3QSllhsRNGTpa09p7Juy5K8spcK73cOTOefu4+HoY89hZOgIOmzB5A4hqPyEDKnzra7KKnhZw==" - }, - "System.Diagnostics.PerformanceCounter": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "pQHdkk3QNDYRsNzWVUIamlTiHb3/I50PxGXMRaEqiLmUe+FkInuMj92lCOhvTT6KbLlnMWGRgZq1rDaMJdQixw==", - "dependencies": { - "System.Configuration.ConfigurationManager": "10.0.3" - } - }, - "System.DirectoryServices": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "sqjtVOs35ortRSABZBhUq1YYtP9ScGMyix+F12K214KkAnfL1D8dS3oXl4U5uA4tsuP/WAiFf3rMvktkh/x1hA==" - }, - "System.DirectoryServices.AccountManagement": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "mllqB05qE2q9AHpyy5yKMSdz66AKCYQPP1I7Ny4+YjaMy7ttI91Ga/NWjOf7iPCXv1PHvgKHVtDrltm7yGKJPw==", - "dependencies": { - "System.Configuration.ConfigurationManager": "10.0.3", - "System.DirectoryServices": "10.0.3", - "System.DirectoryServices.Protocols": "10.0.3" - } - }, - "System.DirectoryServices.Protocols": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "LsmWlhBLelsI0+oHjgZd+WeeX/60TazKv/7xE+z88zONMerx2JWklfmpmOO6VHr1sys5bIJWHtjxhjRruVa4jg==" - }, - "System.Drawing.Common": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "bJlT89G7EqMpiLCvIPmb7BHpcSxjVeFsahRyQk3/mrUt5YgHKiFcEv88db97gKcpRn5opxfBwI0ohUmJlxuGJg==", - "dependencies": { - "Microsoft.Win32.SystemEvents": "10.0.3" - } - }, - "System.IO.Packaging": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "/4CRIbxg4yhfUOCUjochR+iZKLUnewZ4gd3y9iQlKnFLzHkVFYEaO/WARPVgSBZ9W3+yCrERxlA2/4FXOm80og==" - }, - "System.IO.Ports": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "Zs04mZ/dQtaFQ+hpQNDtijBs+6aM9j2fQPp8zNZTfh8DboVNNv7Sw6gH00hT+PVcAhEozlfT+n59Svm6Ug8ROA==", - "dependencies": { - "runtime.native.System.IO.Ports": "10.0.3" - } - }, - "System.Management": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "VyK/nnG1ZgwP/wY8HGrNrdha7kenyI+bkfE0miywiRaWVwqtenuYshA6pmP6Xm7lPsTE2ZRZ+BkzmUVg/VZtTg==", - "dependencies": { - "System.CodeDom": "10.0.3" - } - }, - "System.Management.Automation": { - "type": "Transitive", - "resolved": "7.6.0-rc.1", - "contentHash": "1uKnZxJh3AHXo5tM9Isv3kBlMZPIcXUv3fP46E3/L1I1oCleeYgD1uewdarAGbs1Z2B3qXzIxdM+5WkeoKi7GQ==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Management.Infrastructure": "3.0.0", - "Microsoft.PowerShell.CoreCLR.Eventing": "7.6.0-rc.1", - "Microsoft.PowerShell.Native": "700.0.0-rc.1", - "Microsoft.Security.Extensions": "1.4.0", - "Microsoft.Win32.Registry.AccessControl": "10.0.3", - "Newtonsoft.Json": "13.0.4", - "System.CodeDom": "10.0.3", - "System.Configuration.ConfigurationManager": "10.0.3", - "System.Diagnostics.EventLog": "10.0.3", - "System.DirectoryServices": "10.0.3", - "System.Management": "10.0.3", - "System.Security.Cryptography.Pkcs": "10.0.3", - "System.Security.Cryptography.ProtectedData": "10.0.3", - "System.Security.Permissions": "10.0.3", - "System.Windows.Extensions": "10.0.3" - } - }, - "System.Net.Http.WinHttpHandler": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "BjcA6p1XJk9prN0Ekrg74SLxeXWVNjbdY4L9WWCbSbKxCeLodOWZROKOHn5M1kJomfLn5a4aqFcVD57/6IQStg==" - }, - "System.Reflection.Context": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "5uMIsgbNDFKFL5G+N/3S+nJBxxJKOQOrCLYrPMfgzqjgFADiBr8EDOPUFY+YS4KPcFduOJnqXvhLfqYIZeNJVQ==" - }, - "System.Runtime.Caching": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "+93HrquK0Wen9JL/Suli4mlyaVT/7Fo3mJPp3ozRvBskGx5rgRX5rNqna8XwvHTzCad9YP2wt5H3m2l0KysECQ==", - "dependencies": { - "System.Configuration.ConfigurationManager": "10.0.3" - } - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "Vwbm2siKxaGl515m/5C32J4VCG6VmytrH2ACV6hcWtfj4XQ1zN0cjuuDs49QoDi6/QS3pl2/wPyDYgODO9KxYA==" - }, - "System.Security.Cryptography.Xml": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "Egnmhk/Im38UtSidZtqg0IOB6xQEpn56WR0j+od4qHQ7BUfo6JvqLBQsUSCHMEZ0kc7TIVGSH7aiAOick25Sfg==", - "dependencies": { - "System.Security.Cryptography.Pkcs": "10.0.3" - } - }, - "System.Security.Permissions": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "kuqE2IzQ4lE92519tV9z5asKOXk/budCwryp1C53zCdaUls0+mWlGfmxiiQOpOkVkPPeIx1U7yCswaknBjhEOQ==", - "dependencies": { - "System.Windows.Extensions": "10.0.3" - } - }, - "System.ServiceModel.Http": { - "type": "Transitive", - "resolved": "10.0.652802", - "contentHash": "G02XZvmccf42QCU5MjviBIg69MSMAVHwL1inVPsNSpfp5g+t5BkQM3DyvWRLN4qmeFDWSF/mA1rIYONIDu/6Dg==", - "dependencies": { - "System.ServiceModel.Primitives": "10.0.652802" - } - }, - "System.ServiceModel.NetFramingBase": { - "type": "Transitive", - "resolved": "10.0.652802", - "contentHash": "8/wx/Xnfm9LmGmK0banr05JJYNZmJzlxa8J5lfR7v3MM78QzSG8C3/HDi0/BjlOMeMZd21sX7oEFUhoucrk49w==", - "dependencies": { - "System.ServiceModel.Primitives": "10.0.652802" - } - }, - "System.ServiceModel.NetTcp": { - "type": "Transitive", - "resolved": "10.0.652802", - "contentHash": "VFQgu0IRWUPuPTxHZkMmhPNGYqcu9RwpFcZpW5L941dunUY8nJAErtAWEZYKnj2zAWsm/88nLAEoFc4cuoC2zw==", - "dependencies": { - "System.ServiceModel.NetFramingBase": "10.0.652802", - "System.ServiceModel.Primitives": "10.0.652802" - } - }, - "System.ServiceModel.Primitives": { - "type": "Transitive", - "resolved": "10.0.652802", - "contentHash": "ULfGNl75BNXkpF42wNV2CDXJ64dUZZEa8xO2mBsc4tqbW9QjruxjEB6bAr4Z/T1rNU+leOztIjCJQYsBGFWYlw==", - "dependencies": { - "Microsoft.Extensions.ObjectPool": "10.0.0", - "System.Security.Cryptography.Xml": "10.0.0" - } - }, - "System.ServiceModel.Syndication": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "mloJBxfbjYXgfcfMvH40UWwzekATlUzHLMKPQpg4qab+gpHRgQkhgo4DLz3v7Kacn6000sqoNkxiyk0pJ5x/ig==" - }, - "System.ServiceProcess.ServiceController": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "lOvEbCrTMl4EB7Ckp1suhgcnUEwUs2qRWLYZvquKU33hpRLZfjTXBSHFWQiRshxqd7dA+Nj9Yiw8EbOwd3T/eQ==", - "dependencies": { - "System.Diagnostics.EventLog": "10.0.3" - } - }, - "System.Speech": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "1n6Sn16yLtonZSK7tGzKiGnanuCs38kiE0EIbssNoC6+S8Bx2iih0jNcvU9KxNeRYwMQYXlQUVAUhrkYS+CSHQ==" - }, - "System.Threading.RateLimiting": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" - }, - "System.Web.Services.Description": { - "type": "Transitive", - "resolved": "8.1.2", - "contentHash": "FziIBleSpygZOBudSeMkawLgfarnSam7paGkTtV9ITyTmw/TdEqB+moS0TeApmNfAMWGbcWXDXr3djckuLgGDg==" - }, - "System.Windows.Extensions": { - "type": "Transitive", - "resolved": "10.0.3", - "contentHash": "TZ/9e7JLxAT3j66XuNh7Bbqfz1m7UZC92uLknoldmALo82OYhrb4e8LGQ0S3umh2M+JQ94qQpLvPXIkmyuMraw==" - }, - "werkr.agent": { - "type": "Project", - "dependencies": { - "Grpc.AspNetCore": "[2.76.0, )", - "MailKit": "[4.15.1, )", - "Microsoft.PowerShell.SDK": "[7.6.0-rc.1, )", - "Serilog.AspNetCore": "[10.0.0, )", - "Serilog.Sinks.Console": "[6.1.1, )", - "Serilog.Sinks.File": "[7.0.0, )", - "Serilog.Sinks.OpenTelemetry": "[4.2.0, )", - "Werkr.Common": "[1.0.0, )", - "Werkr.Core": "[1.0.0, )", - "Werkr.Data": "[1.0.0, )", - "Werkr.ServiceDefaults": "[1.0.0, )" - } - }, - "werkr.common": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.34.0, )", - "Microsoft.AspNetCore.Authorization": "[10.0.4, )", - "Microsoft.Extensions.Configuration.Json": "[10.0.4, )", - "Microsoft.IdentityModel.Tokens": "[8.16.0, )", - "Werkr.Common.Configuration": "[1.0.0, )" - } - }, - "werkr.common.configuration": { - "type": "Project" - }, - "werkr.core": { - "type": "Project", - "dependencies": { - "Grpc.Net.Client": "[2.76.0, )", - "Microsoft.Extensions.Hosting.Abstractions": "[10.0.4, )", - "System.Security.Cryptography.ProtectedData": "[10.0.4, )", - "Werkr.Common": "[1.0.0, )", - "Werkr.Data": "[1.0.0, )" - } - }, - "werkr.data": { - "type": "Project", - "dependencies": { - "EFCore.NamingConventions": "[10.0.1, )", - "Microsoft.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )", - "Werkr.Common": "[1.0.0, )" - } - }, - "werkr.servicedefaults": { - "type": "Project", - "dependencies": { - "Microsoft.Extensions.Http.Resilience": "[10.4.0, )", - "Microsoft.Extensions.ServiceDiscovery": "[10.4.0, )", - "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.0, )", - "OpenTelemetry.Extensions.Hosting": "[1.15.0, )" - } - }, - "EFCore.NamingConventions": { - "type": "CentralTransitive", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" - } - }, - "Google.Protobuf": { - "type": "CentralTransitive", - "requested": "[3.34.0, )", - "resolved": "3.34.0", - "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" - }, - "Grpc.AspNetCore": { - "type": "CentralTransitive", - "requested": "[2.76.0, )", - "resolved": "2.76.0", - "contentHash": "LyXMmpN2Ba0TE35SOLSKbGqIYtJuhc1UgiaGfoW1X8KJERV70QI5KGW+ckEY7MrXoFWN/uWo4B70siVhbDmCgQ==", - "dependencies": { - "Google.Protobuf": "3.31.1", - "Grpc.AspNetCore.Server.ClientFactory": "2.76.0", - "Grpc.Tools": "2.76.0" - } - }, - "Grpc.Net.Client": { - "type": "CentralTransitive", - "requested": "[2.76.0, )", - "resolved": "2.76.0", - "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==", - "dependencies": { - "Grpc.Net.Common": "2.76.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Grpc.Net.ClientFactory": { - "type": "CentralTransitive", - "requested": "[2.76.0, )", - "resolved": "2.76.0", - "contentHash": "XI+kO69L9AV8B9N0UQOmH911r6MOEp9huHiavEsY56DJYuzJ9KAxNGy37dpV6CLbgCaN2uKmpOsZ9Pao6bmpVQ==", - "dependencies": { - "Grpc.Net.Client": "2.76.0", - "Microsoft.Extensions.Http": "8.0.0" - } - }, - "MailKit": { - "type": "CentralTransitive", - "requested": "[4.15.1, )", - "resolved": "4.15.1", - "contentHash": "4mLbqTbH3ctd0NlukHjVQbU3ZnNDuCtB6ttNZDLPZLWMA2Dr31rh/eCSTqOwDojUX8zfDOVaxstMgJTE9PwZNA==", - "dependencies": { - "MimeKit": "4.15.1" - } - }, - "Microsoft.AspNetCore.Authorization": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==", - "dependencies": { - "Microsoft.AspNetCore.Metadata": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Data.Sqlite.Core": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==", - "dependencies": { - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.EntityFrameworkCore": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "+5mQrqlBhqNUaPyDmFSNM/qiWStvE9LMxZW1MRF0NhEbO781xNeKryXNR9gGDJ0PmYFDAVoMT9ffX+I15LPFTw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Http.Resilience": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "HbkUsPUC7vLy2TaDbdA9aooW64n9yX4sUppRuiJ1cOzzU1FUW+MVEotm6kYVq6AuUI9xwFSBhRFzA03blmk3VA==", - "dependencies": { - "Microsoft.Extensions.Http.Diagnostics": "10.4.0", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Resilience": "10.4.0" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.ServiceDiscovery": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "RznZAH6L4RNvroECT5JpqfFQJjHTn+8N7+ThSgYutbshkuymFeL/uBIZt1CM8LOdpPPhn4//a5fLUah9/k7ayQ==", - "dependencies": { - "Microsoft.Extensions.Http": "10.0.4", - "Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.4.0" - } - }, - "Microsoft.IdentityModel.Tokens": { - "type": "CentralTransitive", - "requested": "[8.16.0, )", - "resolved": "8.16.0", - "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "10.0.0", - "Microsoft.IdentityModel.Logging": "8.16.0" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)", - "Npgsql": "10.0.0" - } - }, - "OpenTelemetry.Exporter.OpenTelemetryProtocol": { - "type": "CentralTransitive", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==", - "dependencies": { - "OpenTelemetry": "1.15.0" - } - }, - "OpenTelemetry.Extensions.Hosting": { - "type": "CentralTransitive", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==", - "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", - "OpenTelemetry": "1.15.0" - } - }, - "Serilog.AspNetCore": { - "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", - "dependencies": { - "Serilog": "4.3.0", - "Serilog.Extensions.Hosting": "10.0.0", - "Serilog.Formatting.Compact": "3.0.0", - "Serilog.Settings.Configuration": "10.0.0", - "Serilog.Sinks.Console": "6.1.1", - "Serilog.Sinks.Debug": "3.0.0", - "Serilog.Sinks.File": "7.0.0" - } - }, - "Serilog.Sinks.Console": { - "type": "CentralTransitive", - "requested": "[6.1.1, )", - "resolved": "6.1.1", - "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "Serilog.Sinks.File": { - "type": "CentralTransitive", - "requested": "[7.0.0, )", - "resolved": "7.0.0", - "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", - "dependencies": { - "Serilog": "4.2.0" - } - }, - "Serilog.Sinks.OpenTelemetry": { - "type": "CentralTransitive", - "requested": "[4.2.0, )", - "resolved": "4.2.0", - "contentHash": "PzMCyE5G19tjr5IZEi5qg+4UU5QrxBEoBEMu/hhYybTrGKXqUDiSGWKZNUDBgelaVKqLADlsmlJVyKce5SyPrg==", - "dependencies": { - "Google.Protobuf": "3.30.1", - "Grpc.Net.Client": "2.70.0", - "Serilog": "4.2.0" - } - }, - "System.Security.Cryptography.ProtectedData": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g==" - } - } - } +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.Extensions.TimeProvider.Testing": { + "type": "Direct", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "uJ8n9WUEzux9I2CjZh7imGBgZadfwhAKlxuBq7GsNGL8FJF81aHXAYaRMnwW+9EvRFQNytu7xo1ffeuuTncAzg==" + }, + "Microsoft.PowerShell.SDK": { + "type": "Direct", + "requested": "[7.6.0-rc.1, )", + "resolved": "7.6.0-rc.1", + "contentHash": "0AAObi5+pcXKD+4CACnn30WXhe0cP9+5VcZeopAEpKTQRhPrBtYiS2ACMTy72oHJGojCcaX6n+7WW2xuTEd8dg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.3", + "Microsoft.Extensions.ObjectPool": "10.0.3", + "Microsoft.Management.Infrastructure.CimCmdlets": "7.6.0-rc.1", + "Microsoft.PowerShell.Commands.Diagnostics": "7.6.0-rc.1", + "Microsoft.PowerShell.Commands.Management": "7.6.0-rc.1", + "Microsoft.PowerShell.Commands.Utility": "7.6.0-rc.1", + "Microsoft.PowerShell.ConsoleHost": "7.6.0-rc.1", + "Microsoft.PowerShell.Security": "7.6.0-rc.1", + "Microsoft.WSMan.Management": "7.6.0-rc.1", + "Microsoft.Win32.Registry.AccessControl": "10.0.3", + "Microsoft.Win32.SystemEvents": "10.0.3", + "Microsoft.Windows.Compatibility": "10.0.3", + "System.CodeDom": "10.0.3", + "System.ComponentModel.Composition": "10.0.3", + "System.ComponentModel.Composition.Registration": "10.0.3", + "System.Configuration.ConfigurationManager": "10.0.3", + "System.Data.Odbc": "10.0.3", + "System.Data.OleDb": "10.0.3", + "System.Data.SqlClient": "4.9.0", + "System.Diagnostics.EventLog": "10.0.3", + "System.Diagnostics.PerformanceCounter": "10.0.3", + "System.DirectoryServices": "10.0.3", + "System.DirectoryServices.AccountManagement": "10.0.3", + "System.DirectoryServices.Protocols": "10.0.3", + "System.Drawing.Common": "10.0.3", + "System.IO.Packaging": "10.0.3", + "System.IO.Ports": "10.0.3", + "System.Management": "10.0.3", + "System.Management.Automation": "7.6.0-rc.1", + "System.Net.Http.WinHttpHandler": "10.0.3", + "System.Reflection.Context": "10.0.3", + "System.Runtime.Caching": "10.0.3", + "System.Security.Cryptography.Pkcs": "10.0.3", + "System.Security.Cryptography.ProtectedData": "10.0.3", + "System.Security.Cryptography.Xml": "10.0.3", + "System.Security.Permissions": "10.0.3", + "System.ServiceModel.Http": "10.0.652802", + "System.ServiceModel.NetFramingBase": "10.0.652802", + "System.ServiceModel.NetTcp": "10.0.652802", + "System.ServiceModel.Primitives": "10.0.652802", + "System.ServiceModel.Syndication": "10.0.3", + "System.ServiceProcess.ServiceController": "10.0.3", + "System.Speech": "10.0.3" + } + }, + "MSTest": { + "type": "Direct", + "requested": "[4.1.0, )", + "resolved": "4.1.0", + "contentHash": "2bk47yg7HcHRyf6Zf0XgCZicTVTQj4D5lonYTO7lWMxCQB+x66VrQNc2dADBfzthKXfHaA37m8i+VV5h6SbWiA==", + "dependencies": { + "MSTest.TestAdapter": "4.1.0", + "MSTest.TestFramework": "4.1.0", + "Microsoft.NET.Test.Sdk": "18.0.1", + "Microsoft.Testing.Extensions.CodeCoverage": "18.4.1", + "Microsoft.Testing.Extensions.TrxReport": "2.1.0" + } + }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.6.2", + "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w==" + }, + "Grpc.AspNetCore.Server": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "diSC/ZeNdSdxHdYSOpYwuSBBDYpuNVtJQFJfiBB0WrYOQ4lVMmdxuUZJcViahQyo8pCvS3Mueo5lqFxwwMF/iw==", + "dependencies": { + "Grpc.Net.Common": "2.76.0" + } + }, + "Grpc.AspNetCore.Server.ClientFactory": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "y5KGO1GO0N2L/hCCMR05mmoK8j+v8rKvZ+9nothAxKx2Tf2CwV8f4TM5K0GkKfDsp4vrc4lm90MU6E+DeN7YIw==", + "dependencies": { + "Grpc.AspNetCore.Server": "2.76.0", + "Grpc.Net.ClientFactory": "2.76.0" + } + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ==" + }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==", + "dependencies": { + "Grpc.Core.Api": "2.76.0" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Json.More.Net": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "ZXAKl2VsdnIZeUo1PFII3Oi1m1L4YQjEyDjygHfHln5vgsjgIo749X6xWkv7qFYp8RROES+vOEfDcvvoVgs8kA==" + }, + "JsonPointer.Net": { + "type": "Transitive", + "resolved": "5.3.1", + "contentHash": "3e2OJjU0OaE26XC/klgxbJuXvteFWTDJIJv0ITYWcJEoskq7jzUwPSC1s0iz4wPPQnfN7vwwFmg2gJfwRAPwgw==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Json.More.Net": "2.1.1" + } + }, + "JsonSchema.Net": { + "type": "Transitive", + "resolved": "7.4.0", + "contentHash": "5T3DWENwuCzLwFWz0qjXXVWA8+5+gC95OLkhqUBWpVpWBMr9gwfhWNeX8rWyr+fLQ7pIQ+lWuHIrmXRudxOOSw==", + "dependencies": { + "JsonPointer.Net": "5.3.1" + } + }, + "Markdig.Signed": { + "type": "Transitive", + "resolved": "0.44.0", + "contentHash": "mNxf8HrQA/clO8usqQhVc0BGlw0bJtZ76dic5KZGBPJZDX4UR67Jglwilkp5A//gPSMwcoY5EjLPppkZ/B4IMg==" + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "TV62UsrJZPX6gbt3c4WrtXh7bmaDIcMqf9uft1cc4L6gJXOU07hDGEh+bFQh/L2Az0R1WVOkiT66lFqS6G2NmA==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.AmbientMetadata.Application": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "bovnONzrr/JIc+w343i857rJEb7cQH9UzEjbV5n67agWBEYICGQb8xiqYz5+GoFXp6mKEKLwYCQGttMU1p5yXQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.4", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "4WkknDbVrHNf+S6fwSt1OAXlGJ/G/QrtJlqx4aNzOLmeT3GRyxpGLZn+Q3UV+RMRAF6FfsijEZBg2ZAW8bTAkg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" + }, + "Microsoft.Extensions.DependencyInjection.AutoActivation": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "ksmUG2SFTcXzYdyoLOdeSM/qYLRGN6qbbSzYVkwMK9xsctfR1hYkUayeOpFCMd7L+QSlYX72mK9wxwdgQxyS4g==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.4" + } + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "1/hQmONMWxRTKXuN0pQShQN9QsqIRTS1G4fdmKW0O9phuVZjyzIROQD9Fbfwyn2t+yvP8SzjatGAPX4jDRfgHg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + } + }, + "Microsoft.Extensions.Features": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "7to+nkZO+g/GiGQOBzAcrr8HcG8dXETI/hg58fJju0jPO9p/GvNLAis8kMPTBdsjfeTfslBrgFX9Yx1KRnKDww==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" + }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "QRbs+A+WfiGTnV9KFNfWlF+My5euQNZnsvdVMulwRN6C/tEPaF+ZlQfedHoNvFHKLwjQMmqwm4z+TSO9eLvRQw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.Diagnostics": "10.0.4", + "Microsoft.Extensions.Logging": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.Extensions.Http.Diagnostics": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "ybx2QcCWROCnUCbSj/IyHXn1c58brjjHzTTbueKgBl/qHsWk69mu25mjQ3oaMsO1I0+EcS6AhVuhIopL2q3IDw==", + "dependencies": { + "Microsoft.Extensions.Http": "10.0.4", + "Microsoft.Extensions.Telemetry": "10.4.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "XPXoOpUnWEh0pV7Vl2DK2wj47y73Krhrve5OkPrvGIWdZ4U2r47WO8hEdv+wKn65Kh4pmDdiWm7Ibo5pZX+vig==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", + "Microsoft.Extensions.Configuration.Binder": "10.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.Logging": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "2pufIFOgNl/yWTOoIC9XgBnO9VxgfAjdRCnVwpE2+ICfcroGnjuEAGzJ5lTdZeAe0HvA31vMBWXtcmGB7TOq3g==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.Extensions.Resilience": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "41CCbJJPsDWU6NsmKfANHkfT/+KCBlZZqQ1eBoQhhW0xqGCiWmUlMdi2BoaM/GcwKHX5WiQL/IESROmgk0Owfw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics": "10.0.4", + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.4.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4", + "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0", + "Polly.Extensions": "8.4.2", + "Polly.RateLimiting": "8.4.2" + } + }, + "Microsoft.Extensions.ServiceDiscovery.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "HkBb7cdi27tkQiQw1anQFbXe+A3pjRwDKgVbd/DD9fMAO2X9abK0FEyM/tNVXjW3lwOWl2tF+Xij/DqI6i+JTg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", + "Microsoft.Extensions.Configuration.Binder": "10.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.Features": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4", + "Microsoft.Extensions.Primitives": "10.0.4" + } + }, + "Microsoft.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "AbHleTzdpGPjA6RpOjKVHEYx7SoBRnJ2bwAbbPa3aGB7HiVwBmeTJhBGhtIBiuIW0VpKDS8x+bV5iWqpBRIf4w==", + "dependencies": { + "Microsoft.Extensions.AmbientMetadata.Application": "10.4.0", + "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.4.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0" + } + }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "3b2uVa4voJfLLg39BPCKQS0ZgnpEZFkKf7YmnMVlM5FQJYBPOuePIQdnEK1/Oxd+w3GscxGYuE7IMOXDwixZtQ==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "10.4.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.16.0" + } + }, + "Microsoft.Management.Infrastructure": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "cGZi0q5IujCTVYKo9h22Pw+UwfZDV82HXO8HTxMG2HqntPlT3Ls8jY6punLp4YzCypJNpfCAu2kae3TIyuAiJw==", + "dependencies": { + "Microsoft.Management.Infrastructure.Runtime.Unix": "3.0.0", + "Microsoft.Management.Infrastructure.Runtime.Win": "3.0.0" + } + }, + "Microsoft.Management.Infrastructure.CimCmdlets": { + "type": "Transitive", + "resolved": "7.6.0-rc.1", + "contentHash": "+iB/Rj2xnjHo//4E+ADeQMc8LcWy8/XNRp3HqmIAeB/vPcCeEMdY3zald6nlJ90wQ9iZFRIpBKX3MM0IDMU6kg==", + "dependencies": { + "System.Management.Automation": "7.6.0-rc.1" + } + }, + "Microsoft.Management.Infrastructure.Runtime.Unix": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "QZE3uEDvZ0m7LabQvcmNOYHp7v1QPBVMpB/ild0WEE8zqUVAP5y9rRI5we37ImI1bQmW5pZ+3HNC70POPm0jBQ==" + }, + "Microsoft.Management.Infrastructure.Runtime.Win": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "uwMyWN33+iQ8Wm/n1yoPXgFoiYNd0HzJyoqSVhaQZyJfaQrJR3udgcIHjqa1qbc3lS6kvfuUMN4TrF4U4refCQ==" + }, + "Microsoft.NET.Test.Sdk": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", + "dependencies": { + "Microsoft.CodeCoverage": "18.0.1", + "Microsoft.TestPlatform.TestHost": "18.0.1" + } + }, + "Microsoft.PowerShell.Commands.Diagnostics": { + "type": "Transitive", + "resolved": "7.6.0-rc.1", + "contentHash": "gyy6F2m0USZOLJKZrSfagZzU6gwgHY0Ttz8UqY1U3VM3KDo7hE9fq1XDyDJ/5nxUkMjKv76x2YGZoyFCqKmaww==", + "dependencies": { + "System.Management.Automation": "7.6.0-rc.1" + } + }, + "Microsoft.PowerShell.Commands.Management": { + "type": "Transitive", + "resolved": "7.6.0-rc.1", + "contentHash": "CA3FbDTpKaXWFxTqRFSD6IblV7WWQ/8ru7+xYlOpuMX6F4L7mwYkVXtytLcYLeWwpiOX5Jo8L8TdENlS3PGeVA==", + "dependencies": { + "Microsoft.PowerShell.Security": "7.6.0-rc.1", + "System.Diagnostics.EventLog": "10.0.3", + "System.ServiceProcess.ServiceController": "10.0.3" + } + }, + "Microsoft.PowerShell.Commands.Utility": { + "type": "Transitive", + "resolved": "7.6.0-rc.1", + "contentHash": "Bfece2H3c83JEoWQyrf6ka/OMEjJz4hS69GUzuLJOldtrRASnlJ8e6cIg3hjKomh2JTk+QjfL/Jt0GKTAz1nAw==", + "dependencies": { + "JsonSchema.Net": "7.4.0", + "Markdig.Signed": "0.44.0", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.PowerShell.MarkdownRender": "7.2.1", + "Microsoft.Win32.SystemEvents": "10.0.3", + "System.Drawing.Common": "10.0.3", + "System.Management.Automation": "7.6.0-rc.1" + } + }, + "Microsoft.PowerShell.ConsoleHost": { + "type": "Transitive", + "resolved": "7.6.0-rc.1", + "contentHash": "tzijTbDr1gUtSTx17013KEtJS9cqU+7FXF8LldxXQOlNRnxTgJBhg4aw88EXdBDVXFe9ON1d1YpD6BignLk+6Q==", + "dependencies": { + "System.Management.Automation": "7.6.0-rc.1" + } + }, + "Microsoft.PowerShell.CoreCLR.Eventing": { + "type": "Transitive", + "resolved": "7.6.0-rc.1", + "contentHash": "4b0NmZ3mdlluet21RbUXkEMG9+aIY9f+BN2bLy/zmTeFq36MFkGE80JoY0NfIzwwJJA+NpfSj4vXWTYIwujr8Q==", + "dependencies": { + "System.Diagnostics.EventLog": "10.0.3" + } + }, + "Microsoft.PowerShell.MarkdownRender": { + "type": "Transitive", + "resolved": "7.2.1", + "contentHash": "o5oUwL23R/KnjQPD2Oi49WAG5j4O4VLo1fPRSyM/aq0HuTrY2RnF4B3MCGk13BfcmK51p9kPlHZ1+8a/ZjO4Jg==", + "dependencies": { + "Markdig.Signed": "0.31.0" + } + }, + "Microsoft.PowerShell.Native": { + "type": "Transitive", + "resolved": "700.0.0-rc.1", + "contentHash": "lJOCErHTSWwCzfp3wgeyqhNRi4t43McDc0CHqlbt3Cj3OomiqPlNHQXujSbgd+0Ir6/8QAmvU/VOYgqCyMki6A==" + }, + "Microsoft.PowerShell.Security": { + "type": "Transitive", + "resolved": "7.6.0-rc.1", + "contentHash": "Lcsqavzb3jp9MqLFr9TCsxFBylepRGUhPhkRAYDeSMYohK6K4fMkdKudWo23M5ZKgooNySkkQX2H1jF7w0ZU7w==", + "dependencies": { + "System.Management.Automation": "7.6.0-rc.1" + } + }, + "Microsoft.Security.Extensions": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "MnHXttc0jHbRrGdTJ+yJBbGDoa4OXhtnKXHQw70foMyAooFtPScZX/dN+Nib47nuglc9Gt29Gfb5Zl+1lAuTeA==" + }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Transitive", + "resolved": "18.4.1", + "contentHash": "l1VZM9dg9s76L5D288ipAT4HRYDJ6Vxh8wX20gfS9VnpueedRfN4/aGNn4oA1g6pwq2WSM3Ci7IoSSGPiqu+WQ==", + "dependencies": { + "Microsoft.DiaSymReader": "2.0.0", + "Microsoft.Extensions.DependencyModel": "8.0.2", + "Microsoft.Testing.Platform": "2.0.2" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "5TwgTx2u7k9Al/xbZ18QXq4Hdy2xewkVTI6K3sk+jY2ykqUkIKNuj7rFu3GOV5KnEUkevhw6eZcyZs77STHJIA==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.TrxReport": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "cXmP225WcMLLOSrW8xekaNhfzdBwXX3cbXbE5qSzmLbK0KZe3z8rAObKj70FWiPPPzm2W22x0ZW93gsmAfK6Mg==", + "dependencies": { + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "D8xmIJYQFJ6D49Rx5/vPrkZZxb338Jkew+eSqZLBfBiWKw4QZKy3i1BOXiLfz0lOmaNErwDz/YWRojCdNl+B9Q==", + "dependencies": { + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.VSTestBridge": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "bNRIEA2YoGr+Y+7LHdA7i1U80+7BAdf4K4Qh4Kx6eKkoBK/NV7QpoMg+GWPP0/eqAFzuUmUOIPVZ87Oo0Vyxmw==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Microsoft.Testing.Extensions.Telemetry": "2.1.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "aHkjNTGIA+Zbdw6RJgSFrbDrCjO0CgqpElqYcvkRSeUhBv2bKarnvU3ep786U7UqrPlArT/B7VmImRibJD0Zrg==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "UpfPebXQtHGrWz21+YLHmJSm+5zsuPE9U9pfdCtoB+67g75fDmWlNgpkH2ZmdVhSwkjNIed9Icg8Iu63z2ei5Q==", + "dependencies": { + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.Win32.Registry.AccessControl": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "CxgQc/IHtQ2IhqRdN6nxZZwk/C+dnbE5GLWErze4jAUgCkPbGe+hlgbESMop57FQMGShOqrWZWQAKpZ65QTU8g==" + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "gYpwz5Gl0rs9pEFHBKctLbSi7SUGR4L1uRjXkU488nizWd2hvo2JP2+ATUsv0th7v6LDiXfdlUsnTbvC2MauFA==" + }, + "Microsoft.Windows.Compatibility": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "MIoN5L2qs5cqClZ3vvH0ZG4QxaSEAYyNeR7B5k88y0l55MKAKCn4uSfposEwpXeHqZrI1uUevA/c5YUe6hcAIw==", + "dependencies": { + "Microsoft.Win32.Registry.AccessControl": "10.0.3", + "Microsoft.Win32.SystemEvents": "10.0.3", + "System.CodeDom": "10.0.3", + "System.ComponentModel.Composition": "10.0.3", + "System.ComponentModel.Composition.Registration": "10.0.3", + "System.Configuration.ConfigurationManager": "10.0.3", + "System.Data.Odbc": "10.0.3", + "System.Data.OleDb": "10.0.3", + "System.Data.SqlClient": "4.9.0", + "System.Diagnostics.EventLog": "10.0.3", + "System.Diagnostics.PerformanceCounter": "10.0.3", + "System.DirectoryServices": "10.0.3", + "System.DirectoryServices.AccountManagement": "10.0.3", + "System.DirectoryServices.Protocols": "10.0.3", + "System.Drawing.Common": "10.0.3", + "System.IO.Packaging": "10.0.3", + "System.IO.Ports": "10.0.3", + "System.Management": "10.0.3", + "System.Reflection.Context": "10.0.3", + "System.Runtime.Caching": "10.0.3", + "System.Security.Cryptography.Pkcs": "10.0.3", + "System.Security.Cryptography.ProtectedData": "10.0.3", + "System.Security.Cryptography.Xml": "10.0.3", + "System.Security.Permissions": "10.0.3", + "System.ServiceModel.Http": "8.1.2", + "System.ServiceModel.NetTcp": "8.1.2", + "System.ServiceModel.Primitives": "8.1.2", + "System.ServiceModel.Syndication": "10.0.3", + "System.ServiceProcess.ServiceController": "10.0.3", + "System.Speech": "10.0.3", + "System.Web.Services.Description": "8.1.2" + } + }, + "Microsoft.WSMan.Management": { + "type": "Transitive", + "resolved": "7.6.0-rc.1", + "contentHash": "AsZLXiO6qyrvmk6e4S4qabpbysq+5+veGmhDJUDMJZdyq0ORmFB7GiWD5aYHwSebKeJSItV51lwNcIdo1JgbQA==", + "dependencies": { + "Microsoft.WSMan.Runtime": "7.6.0-rc.1", + "System.Diagnostics.EventLog": "10.0.3", + "System.Management.Automation": "7.6.0-rc.1", + "System.ServiceProcess.ServiceController": "10.0.3" + } + }, + "Microsoft.WSMan.Runtime": { + "type": "Transitive", + "resolved": "7.6.0-rc.1", + "contentHash": "HFG8NIyKCrZ7SM/lgewFpEQ13kbsRQm6c8ihdo2v+Am0XfftCw+cxzKOpNXlti8ZBEkPksZzkbxlPeAds0PZGA==" + }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.15.1", + "contentHash": "cxCcQhD0zhboFoG136jJuJtQjNRDJ+BxBm3f2vWn+53bff/CRo+K1mAkWjsW4Wuyy5O22F40MdMG2nRzQu1cJw==", + "dependencies": { + "BouncyCastle.Cryptography": "2.6.2", + "System.Security.Cryptography.Pkcs": "10.0.0" + } + }, + "MSTest.Analyzers": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "4ElL/aqomiUInr090VN4udqz46AuszXLrifHkLrgj0zb7na8eAoyUQt3BwDLTcGd1bSkmk3SfD02rZtKU+ZiqQ==" + }, + "MSTest.TestAdapter": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "bRW1Hftwq0XbcVExcAbj4YAfSZDRAziL0mygDkPBvaUe2nSsWFQIatze5lHVjPFJMvSFgWnItku4pguIy5FowQ==", + "dependencies": { + "MSTest.TestFramework": "4.1.0", + "Microsoft.Testing.Extensions.VSTestBridge": "2.1.0", + "Microsoft.Testing.Platform.MSBuild": "2.1.0" + } + }, + "MSTest.TestFramework": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "BzpvsK+CRbk6khwY62h+7HfYzIxtJXyPv9tOI9T90cy5CVy+WI1JkN4ZaNL4Dobqb6dywSwabLTIbPZKpdrr+A==", + "dependencies": { + "MSTest.Analyzers": "4.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.15.0" + } + }, + "Polly.Core": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" + }, + "Polly.Extensions": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Polly.Core": "8.4.2" + } + }, + "Polly.RateLimiting": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", + "dependencies": { + "Polly.Core": "8.4.2", + "System.Threading.RateLimiting": "8.0.0" + } + }, + "runtime.android-arm.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "6W4qZX0X7FF+PHM9Kaa5ZsTLcGJAzCU7FB4Tjy1vTg2rUIEjDqijWTtpz8vY6gBzZaG+tD0/EKUyGnfq6d/d/Q==" + }, + "runtime.android-arm64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "4OdNg2Du1kvm0b4tSErkA4wfH32YmgUqeSLzekk6NdCR61brCp4vttTJl0epZMwbRppXuy/jY7pV0WtP4ymnfQ==" + }, + "runtime.android-x64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "nP7xJSDSqRktV4kEd0kr4n0xhzJFuQjh7L/AgrKB3C4QK5TA/8NimJmTl1ogngvnXn2vilH2Hk+keBIvuoGWCA==" + }, + "runtime.android-x86.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "f/neqijoVpgl/PcrXYeLNfCC2t74XYIcaHvtSaF3zaIdJ3hf5wrwRC9gzYLXHRLg/j4DRsCHEw8rlux/dNh7XA==" + }, + "runtime.linux-arm.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "cwiQmy95Zd42K4kMzDt8GkXNKHWDkRZIIyb3MteRrgJKubH2DMA6VY3JiFeR+1hQ7i31Ck+dYyt8wUkElZ/NrQ==" + }, + "runtime.linux-arm64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "cEx+Xm0ZNPsuoYTFwXZ6qRstwXQ7vJjbb3jWzo5s5xIeEgpTDdfiUjTgK3Gl618mWgb7+Gn6Vt5pT07RxFC28Q==" + }, + "runtime.linux-bionic-arm64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "kB21sNzlKgTj3V/LZhnTLFuViiGKtkiUG2GdEW+z3jdeUtRoxqBBXryzMqy0oUOy5idBW2pA6bh+iLGaj+GThA==" + }, + "runtime.linux-bionic-x64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "/Ud7EYdpnBGJ/x7DdqVFEJOQtEcf9rsD6/1iyUBJNfhhs8JljZLewJHp1cXr1DVCaWWc9jSWIc4g+QpMct/22A==" + }, + "runtime.linux-musl-arm.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "pBubukkmXD9e4Ju004bEHL3TE5mYahONCVwrJ5b+7/cj8smSY2H5sKTyURiVF1EfX55yORKyTyQ3dALA5yTTlg==" + }, + "runtime.linux-musl-arm64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "+FR6SvpZya66Wv26MVEK4l2gnjfZnBVUz675eaTcMQ/KWAy+GMaPyhRaBIMnQJf5gfuu3x1qgeAzpmeFkOPGBw==" + }, + "runtime.linux-musl-x64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "Ee1XnOSKgeJGBcnd7LLLkyXEbE/JcpQ3pUlbPXqugl0YD55jVubpYebzGYI+ilrnBLCRiV0BR822b2XmerIKGw==" + }, + "runtime.linux-x64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "U+Zr1KuBDIIwgR2gTMFbBAtkr4WnKdEXxmknPa3X8Qy6oCi4+MTRmq2zmTh7wxXQXTdmhfXg2tyy7DuVHtB+xQ==" + }, + "runtime.maccatalyst-arm64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "9yTgh3ZKCBTGEO4YS76g/K15s2qSdeEZDqWteixUPeRsETsuBUwUPF39Fk39OPlRgwhqEuhtHy1Hf6BSOETfAg==" + }, + "runtime.maccatalyst-x64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "0gxqUZYQ1T1o5VzgyEMKiSPKJtPbleF2nbjZa5QKZQvPgSlrZFL+FrW4LywIk2scOpXu+Vd5Mv8AGGV7slhYWA==" + }, + "runtime.native.System.Data.SqlClient.sni": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "A8v6PGmk+UGbfWo5Ixup0lPM4swuSwOiayJExZwKIOjTlFFQIsu3QnDXECosBEyrWSPryxBVrdqtJyhK3BaupQ==", + "dependencies": { + "runtime.win-arm64.runtime.native.System.Data.SqlClient.sni": "4.4.0", + "runtime.win-x64.runtime.native.System.Data.SqlClient.sni": "4.4.0", + "runtime.win-x86.runtime.native.System.Data.SqlClient.sni": "4.4.0" + } + }, + "runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "7U3HW0JzAeg9PWaYBcyWLFMq2RSexgJ8uVCR3E2QVJJ/IZnRT0wYRhyEx3zBSbKYzN3KE6MDiRd6CN4hR7YkqA==", + "dependencies": { + "runtime.android-arm.runtime.native.System.IO.Ports": "10.0.3", + "runtime.android-arm64.runtime.native.System.IO.Ports": "10.0.3", + "runtime.android-x64.runtime.native.System.IO.Ports": "10.0.3", + "runtime.android-x86.runtime.native.System.IO.Ports": "10.0.3", + "runtime.linux-arm.runtime.native.System.IO.Ports": "10.0.3", + "runtime.linux-arm64.runtime.native.System.IO.Ports": "10.0.3", + "runtime.linux-bionic-arm64.runtime.native.System.IO.Ports": "10.0.3", + "runtime.linux-bionic-x64.runtime.native.System.IO.Ports": "10.0.3", + "runtime.linux-musl-arm.runtime.native.System.IO.Ports": "10.0.3", + "runtime.linux-musl-arm64.runtime.native.System.IO.Ports": "10.0.3", + "runtime.linux-musl-x64.runtime.native.System.IO.Ports": "10.0.3", + "runtime.linux-x64.runtime.native.System.IO.Ports": "10.0.3", + "runtime.maccatalyst-arm64.runtime.native.System.IO.Ports": "10.0.3", + "runtime.maccatalyst-x64.runtime.native.System.IO.Ports": "10.0.3", + "runtime.osx-arm64.runtime.native.System.IO.Ports": "10.0.3", + "runtime.osx-x64.runtime.native.System.IO.Ports": "10.0.3" + } + }, + "runtime.osx-arm64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "qnEjXlIazxaRAhBet0EupWoJQ9PKsXjThpMKGP/ZsCjZgHEb+zs9wft82wY27OagyPOCg9D4hJHWKt5LTbJNZg==" + }, + "runtime.osx-x64.runtime.native.System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "L1gcOL5kcfeeujKQbqJmpeTLsT0c6DeaJ9w6PY9oZ+/WESyya+Zj7StKguXR5j93o3o86FjcN05yufpoCz7R2g==" + }, + "runtime.win-arm64.runtime.native.System.Data.SqlClient.sni": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "LbrynESTp3bm5O/+jGL8v0Qg5SJlTV08lpIpFesXjF6uGNMWqFnUQbYBJwZTeua6E/Y7FIM1C54Ey1btLWupdg==" + }, + "runtime.win-x64.runtime.native.System.Data.SqlClient.sni": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "38ugOfkYJqJoX9g6EYRlZB5U2ZJH51UP8ptxZgdpS07FgOEToV+lS11ouNK2PM12Pr6X/PpT5jK82G3DwH/SxQ==" + }, + "runtime.win-x86.runtime.native.System.Data.SqlClient.sni": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "YhEdSQUsTx+C8m8Bw7ar5/VesXvCFMItyZF7G1AUY+OM0VPZUOeAVpJ4Wl6fydBGUYZxojTDR3I6Bj/+BPkJNA==" + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "+G1mBhHJp8taiDcHK1gmckOZ884n9JeIrS1dzFYZhSa8oTiIF+EagIyAyCOQzdBbbES7eb9AkjGBlUZqqoCaeg==" + }, + "System.ComponentModel.Composition": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "qBShXNCMMY4rTWvh8Y9BombIyng4LUqbfZd3yx93D88YKf2w1MuYpC8J4XSxSVQDubcoB9AMUwWe6IcVzo3/Sg==" + }, + "System.ComponentModel.Composition.Registration": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "Dtb5UBzyHH3RnVbVSOTE62X6eIYmbStV3z4jk7aew34YVTMltAeqN6p5KffqdOo8MO8ARFmpZZDcfwJeIbuGsQ==", + "dependencies": { + "System.ComponentModel.Composition": "10.0.3", + "System.Reflection.Context": "10.0.3" + } + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "69ZT/MYxQSxwdiiHRtI08noXiG5drj/bXDDZISmeWkNUtbIfYgmTiof16tCVOLTdmSQY7W7gwxkMliKdreWHGQ==", + "dependencies": { + "System.Diagnostics.EventLog": "10.0.3", + "System.Security.Cryptography.ProtectedData": "10.0.3" + } + }, + "System.Data.Odbc": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "/uKHUwmcXdq06LisgTUC85pKx8xFwFBWKoVLwawW88mKIrUXMCtDxE8c1bixoHOWCI2tesvmZuxo6EgyDVZO/w==" + }, + "System.Data.OleDb": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "/YVeBa7iHwhvokst2neLLVBwhcTEcOSjLZPPhjBEPHVxkY5WsffnJ6pya2DhdaKsbCYsWAJfyvnoAPGIE1gwYA==", + "dependencies": { + "System.Configuration.ConfigurationManager": "10.0.3", + "System.Diagnostics.PerformanceCounter": "10.0.3" + } + }, + "System.Data.SqlClient": { + "type": "Transitive", + "resolved": "4.9.0", + "contentHash": "j4KJO+vC62NyUtNHz854njEqXbT8OmAa5jb1nrGfYWBOcggyYUQE0w/snXeaCjdvkSKWuUD+hfvlbN8pTrJTXg==", + "dependencies": { + "runtime.native.System.Data.SqlClient.sni": "4.4.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "+bZnyzt0/vt4g3QSllhsRNGTpa09p7Juy5K8spcK73cOTOefu4+HoY89hZOgIOmzB5A4hqPyEDKnzra7KKnhZw==" + }, + "System.Diagnostics.PerformanceCounter": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "pQHdkk3QNDYRsNzWVUIamlTiHb3/I50PxGXMRaEqiLmUe+FkInuMj92lCOhvTT6KbLlnMWGRgZq1rDaMJdQixw==", + "dependencies": { + "System.Configuration.ConfigurationManager": "10.0.3" + } + }, + "System.DirectoryServices": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "sqjtVOs35ortRSABZBhUq1YYtP9ScGMyix+F12K214KkAnfL1D8dS3oXl4U5uA4tsuP/WAiFf3rMvktkh/x1hA==" + }, + "System.DirectoryServices.AccountManagement": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "mllqB05qE2q9AHpyy5yKMSdz66AKCYQPP1I7Ny4+YjaMy7ttI91Ga/NWjOf7iPCXv1PHvgKHVtDrltm7yGKJPw==", + "dependencies": { + "System.Configuration.ConfigurationManager": "10.0.3", + "System.DirectoryServices": "10.0.3", + "System.DirectoryServices.Protocols": "10.0.3" + } + }, + "System.DirectoryServices.Protocols": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "LsmWlhBLelsI0+oHjgZd+WeeX/60TazKv/7xE+z88zONMerx2JWklfmpmOO6VHr1sys5bIJWHtjxhjRruVa4jg==" + }, + "System.Drawing.Common": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "bJlT89G7EqMpiLCvIPmb7BHpcSxjVeFsahRyQk3/mrUt5YgHKiFcEv88db97gKcpRn5opxfBwI0ohUmJlxuGJg==", + "dependencies": { + "Microsoft.Win32.SystemEvents": "10.0.3" + } + }, + "System.IO.Packaging": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "/4CRIbxg4yhfUOCUjochR+iZKLUnewZ4gd3y9iQlKnFLzHkVFYEaO/WARPVgSBZ9W3+yCrERxlA2/4FXOm80og==" + }, + "System.IO.Ports": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "Zs04mZ/dQtaFQ+hpQNDtijBs+6aM9j2fQPp8zNZTfh8DboVNNv7Sw6gH00hT+PVcAhEozlfT+n59Svm6Ug8ROA==", + "dependencies": { + "runtime.native.System.IO.Ports": "10.0.3" + } + }, + "System.Management": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "VyK/nnG1ZgwP/wY8HGrNrdha7kenyI+bkfE0miywiRaWVwqtenuYshA6pmP6Xm7lPsTE2ZRZ+BkzmUVg/VZtTg==", + "dependencies": { + "System.CodeDom": "10.0.3" + } + }, + "System.Management.Automation": { + "type": "Transitive", + "resolved": "7.6.0-rc.1", + "contentHash": "1uKnZxJh3AHXo5tM9Isv3kBlMZPIcXUv3fP46E3/L1I1oCleeYgD1uewdarAGbs1Z2B3qXzIxdM+5WkeoKi7GQ==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Management.Infrastructure": "3.0.0", + "Microsoft.PowerShell.CoreCLR.Eventing": "7.6.0-rc.1", + "Microsoft.PowerShell.Native": "700.0.0-rc.1", + "Microsoft.Security.Extensions": "1.4.0", + "Microsoft.Win32.Registry.AccessControl": "10.0.3", + "Newtonsoft.Json": "13.0.4", + "System.CodeDom": "10.0.3", + "System.Configuration.ConfigurationManager": "10.0.3", + "System.Diagnostics.EventLog": "10.0.3", + "System.DirectoryServices": "10.0.3", + "System.Management": "10.0.3", + "System.Security.Cryptography.Pkcs": "10.0.3", + "System.Security.Cryptography.ProtectedData": "10.0.3", + "System.Security.Permissions": "10.0.3", + "System.Windows.Extensions": "10.0.3" + } + }, + "System.Net.Http.WinHttpHandler": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "BjcA6p1XJk9prN0Ekrg74SLxeXWVNjbdY4L9WWCbSbKxCeLodOWZROKOHn5M1kJomfLn5a4aqFcVD57/6IQStg==" + }, + "System.Reflection.Context": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "5uMIsgbNDFKFL5G+N/3S+nJBxxJKOQOrCLYrPMfgzqjgFADiBr8EDOPUFY+YS4KPcFduOJnqXvhLfqYIZeNJVQ==" + }, + "System.Runtime.Caching": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "+93HrquK0Wen9JL/Suli4mlyaVT/7Fo3mJPp3ozRvBskGx5rgRX5rNqna8XwvHTzCad9YP2wt5H3m2l0KysECQ==", + "dependencies": { + "System.Configuration.ConfigurationManager": "10.0.3" + } + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "Vwbm2siKxaGl515m/5C32J4VCG6VmytrH2ACV6hcWtfj4XQ1zN0cjuuDs49QoDi6/QS3pl2/wPyDYgODO9KxYA==" + }, + "System.Security.Cryptography.Xml": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "Egnmhk/Im38UtSidZtqg0IOB6xQEpn56WR0j+od4qHQ7BUfo6JvqLBQsUSCHMEZ0kc7TIVGSH7aiAOick25Sfg==", + "dependencies": { + "System.Security.Cryptography.Pkcs": "10.0.3" + } + }, + "System.Security.Permissions": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "kuqE2IzQ4lE92519tV9z5asKOXk/budCwryp1C53zCdaUls0+mWlGfmxiiQOpOkVkPPeIx1U7yCswaknBjhEOQ==", + "dependencies": { + "System.Windows.Extensions": "10.0.3" + } + }, + "System.ServiceModel.Http": { + "type": "Transitive", + "resolved": "10.0.652802", + "contentHash": "G02XZvmccf42QCU5MjviBIg69MSMAVHwL1inVPsNSpfp5g+t5BkQM3DyvWRLN4qmeFDWSF/mA1rIYONIDu/6Dg==", + "dependencies": { + "System.ServiceModel.Primitives": "10.0.652802" + } + }, + "System.ServiceModel.NetFramingBase": { + "type": "Transitive", + "resolved": "10.0.652802", + "contentHash": "8/wx/Xnfm9LmGmK0banr05JJYNZmJzlxa8J5lfR7v3MM78QzSG8C3/HDi0/BjlOMeMZd21sX7oEFUhoucrk49w==", + "dependencies": { + "System.ServiceModel.Primitives": "10.0.652802" + } + }, + "System.ServiceModel.NetTcp": { + "type": "Transitive", + "resolved": "10.0.652802", + "contentHash": "VFQgu0IRWUPuPTxHZkMmhPNGYqcu9RwpFcZpW5L941dunUY8nJAErtAWEZYKnj2zAWsm/88nLAEoFc4cuoC2zw==", + "dependencies": { + "System.ServiceModel.NetFramingBase": "10.0.652802", + "System.ServiceModel.Primitives": "10.0.652802" + } + }, + "System.ServiceModel.Primitives": { + "type": "Transitive", + "resolved": "10.0.652802", + "contentHash": "ULfGNl75BNXkpF42wNV2CDXJ64dUZZEa8xO2mBsc4tqbW9QjruxjEB6bAr4Z/T1rNU+leOztIjCJQYsBGFWYlw==", + "dependencies": { + "Microsoft.Extensions.ObjectPool": "10.0.0", + "System.Security.Cryptography.Xml": "10.0.0" + } + }, + "System.ServiceModel.Syndication": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "mloJBxfbjYXgfcfMvH40UWwzekATlUzHLMKPQpg4qab+gpHRgQkhgo4DLz3v7Kacn6000sqoNkxiyk0pJ5x/ig==" + }, + "System.ServiceProcess.ServiceController": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "lOvEbCrTMl4EB7Ckp1suhgcnUEwUs2qRWLYZvquKU33hpRLZfjTXBSHFWQiRshxqd7dA+Nj9Yiw8EbOwd3T/eQ==", + "dependencies": { + "System.Diagnostics.EventLog": "10.0.3" + } + }, + "System.Speech": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "1n6Sn16yLtonZSK7tGzKiGnanuCs38kiE0EIbssNoC6+S8Bx2iih0jNcvU9KxNeRYwMQYXlQUVAUhrkYS+CSHQ==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "System.Web.Services.Description": { + "type": "Transitive", + "resolved": "8.1.2", + "contentHash": "FziIBleSpygZOBudSeMkawLgfarnSam7paGkTtV9ITyTmw/TdEqB+moS0TeApmNfAMWGbcWXDXr3djckuLgGDg==" + }, + "System.Windows.Extensions": { + "type": "Transitive", + "resolved": "10.0.3", + "contentHash": "TZ/9e7JLxAT3j66XuNh7Bbqfz1m7UZC92uLknoldmALo82OYhrb4e8LGQ0S3umh2M+JQ94qQpLvPXIkmyuMraw==" + }, + "werkr.agent": { + "type": "Project", + "dependencies": { + "Grpc.AspNetCore": "[2.76.0, )", + "MailKit": "[4.15.1, )", + "Microsoft.PowerShell.SDK": "[7.6.0-rc.1, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "Serilog.Sinks.File": "[7.0.0, )", + "Serilog.Sinks.OpenTelemetry": "[4.2.0, )", + "Werkr.Common": "[1.0.0, )", + "Werkr.Core": "[1.0.0, )", + "Werkr.Data": "[1.0.0, )", + "Werkr.ServiceDefaults": "[1.0.0, )" + } + }, + "werkr.common": { + "type": "Project", + "dependencies": { + "Google.Protobuf": "[3.34.0, )", + "Microsoft.AspNetCore.Authorization": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Json": "[10.0.5, )", + "Microsoft.IdentityModel.Tokens": "[8.16.0, )", + "Werkr.Common.Configuration": "[1.0.0, )" + } + }, + "werkr.common.configuration": { + "type": "Project" + }, + "werkr.core": { + "type": "Project", + "dependencies": { + "Grpc.Net.Client": "[2.76.0, )", + "Microsoft.Extensions.Hosting.Abstractions": "[10.0.5, )", + "System.Security.Cryptography.ProtectedData": "[10.0.5, )", + "Werkr.Common": "[1.0.0, )", + "Werkr.Data": "[1.0.0, )" + } + }, + "werkr.data": { + "type": "Project", + "dependencies": { + "EFCore.NamingConventions": "[10.0.1, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "Werkr.Common": "[1.0.0, )" + } + }, + "werkr.servicedefaults": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Http.Resilience": "[10.4.0, )", + "Microsoft.Extensions.ServiceDiscovery": "[10.4.0, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.0, )", + "OpenTelemetry.Extensions.Hosting": "[1.15.0, )" + } + }, + "EFCore.NamingConventions": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + } + }, + "Google.Protobuf": { + "type": "CentralTransitive", + "requested": "[3.34.0, )", + "resolved": "3.34.0", + "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" + }, + "Grpc.AspNetCore": { + "type": "CentralTransitive", + "requested": "[2.76.0, )", + "resolved": "2.76.0", + "contentHash": "LyXMmpN2Ba0TE35SOLSKbGqIYtJuhc1UgiaGfoW1X8KJERV70QI5KGW+ckEY7MrXoFWN/uWo4B70siVhbDmCgQ==", + "dependencies": { + "Google.Protobuf": "3.31.1", + "Grpc.AspNetCore.Server.ClientFactory": "2.76.0", + "Grpc.Tools": "2.76.0" + } + }, + "Grpc.Net.Client": { + "type": "CentralTransitive", + "requested": "[2.76.0, )", + "resolved": "2.76.0", + "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==", + "dependencies": { + "Grpc.Net.Common": "2.76.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "Grpc.Net.ClientFactory": { + "type": "CentralTransitive", + "requested": "[2.76.0, )", + "resolved": "2.76.0", + "contentHash": "XI+kO69L9AV8B9N0UQOmH911r6MOEp9huHiavEsY56DJYuzJ9KAxNGy37dpV6CLbgCaN2uKmpOsZ9Pao6bmpVQ==", + "dependencies": { + "Grpc.Net.Client": "2.76.0", + "Microsoft.Extensions.Http": "8.0.0" + } + }, + "MailKit": { + "type": "CentralTransitive", + "requested": "[4.15.1, )", + "resolved": "4.15.1", + "contentHash": "4mLbqTbH3ctd0NlukHjVQbU3ZnNDuCtB6ttNZDLPZLWMA2Dr31rh/eCSTqOwDojUX8zfDOVaxstMgJTE9PwZNA==", + "dependencies": { + "MimeKit": "4.15.1" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Data.Sqlite.Core": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Http.Resilience": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "HbkUsPUC7vLy2TaDbdA9aooW64n9yX4sUppRuiJ1cOzzU1FUW+MVEotm6kYVq6AuUI9xwFSBhRFzA03blmk3VA==", + "dependencies": { + "Microsoft.Extensions.Http.Diagnostics": "10.4.0", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Resilience": "10.4.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.ServiceDiscovery": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "RznZAH6L4RNvroECT5JpqfFQJjHTn+8N7+ThSgYutbshkuymFeL/uBIZt1CM8LOdpPPhn4//a5fLUah9/k7ayQ==", + "dependencies": { + "Microsoft.Extensions.Http": "10.0.4", + "Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.4.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "CentralTransitive", + "requested": "[8.16.0, )", + "resolved": "8.16.0", + "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.IdentityModel.Logging": "8.16.0" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==", + "dependencies": { + "OpenTelemetry": "1.15.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.15.0" + } + }, + "Serilog.AspNetCore": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "Serilog.Sinks.OpenTelemetry": { + "type": "CentralTransitive", + "requested": "[4.2.0, )", + "resolved": "4.2.0", + "contentHash": "PzMCyE5G19tjr5IZEi5qg+4UU5QrxBEoBEMu/hhYybTrGKXqUDiSGWKZNUDBgelaVKqLADlsmlJVyKce5SyPrg==", + "dependencies": { + "Google.Protobuf": "3.30.1", + "Grpc.Net.Client": "2.70.0", + "Serilog": "4.2.0" + } + }, + "System.Security.Cryptography.ProtectedData": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg==" + } + } + } } \ No newline at end of file diff --git a/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs index c8bb8c9..b64aeaf 100644 --- a/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs +++ b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs @@ -48,9 +48,9 @@ public async Task Default_RoundTrips_AsDefaultString( ) { // Arrange — create a workflow and task to host the step Workflow workflow = new( ) { Name = "RoundTrip_WF", Description = "test" }; - _dbContext.Workflows.Add( workflow ); + _ = _dbContext.Workflows.Add( workflow ); WerkrTask task = new( ) { Name = "RoundTrip_Task", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] }; - _dbContext.Tasks.Add( task ); + _ = _dbContext.Tasks.Add( task ); _ = await _dbContext.SaveChangesAsync( ct ); WorkflowStep step = new( ) { @@ -59,7 +59,7 @@ public async Task Default_RoundTrips_AsDefaultString( ) { Order = 0, ControlStatement = ControlStatement.Default, }; - _dbContext.WorkflowSteps.Add( step ); + _ = _dbContext.WorkflowSteps.Add( step ); _ = await _dbContext.SaveChangesAsync( ct ); // Detach so the next query hits the database @@ -92,9 +92,9 @@ public async Task AllEnumValues_RoundTrip_Correctly( ControlStatement value, str CancellationToken ct = TestContext.CancellationToken; Workflow workflow = new( ) { Name = $"RoundTrip_{value}", Description = "test" }; - _dbContext.Workflows.Add( workflow ); + _ = _dbContext.Workflows.Add( workflow ); WerkrTask task = new( ) { Name = $"Task_{value}", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] }; - _dbContext.Tasks.Add( task ); + _ = _dbContext.Tasks.Add( task ); _ = await _dbContext.SaveChangesAsync( ct ); WorkflowStep step = new( ) { @@ -105,7 +105,7 @@ public async Task AllEnumValues_RoundTrip_Correctly( ControlStatement value, str ConditionExpression = value is ControlStatement.If or ControlStatement.ElseIf or ControlStatement.While or ControlStatement.Do ? "$? -eq $true" : null, }; - _dbContext.WorkflowSteps.Add( step ); + _ = _dbContext.WorkflowSteps.Add( step ); _ = await _dbContext.SaveChangesAsync( ct ); _dbContext.ChangeTracker.Clear( ); @@ -129,9 +129,9 @@ public async Task LegacySequential_ReadsAs_Default( ) { CancellationToken ct = TestContext.CancellationToken; Workflow workflow = new( ) { Name = "Legacy_WF", Description = "test" }; - _dbContext.Workflows.Add( workflow ); + _ = _dbContext.Workflows.Add( workflow ); WerkrTask task = new( ) { Name = "Legacy_Task", ActionType = TaskActionType.ShellCommand, Content = "echo test", TargetTags = ["test"] }; - _dbContext.Tasks.Add( task ); + _ = _dbContext.Tasks.Add( task ); _ = await _dbContext.SaveChangesAsync( ct ); // Insert a step with "Default" first (to get a valid row) @@ -141,7 +141,7 @@ public async Task LegacySequential_ReadsAs_Default( ) { Order = 0, ControlStatement = ControlStatement.Default, }; - _dbContext.WorkflowSteps.Add( step ); + _ = _dbContext.WorkflowSteps.Add( step ); _ = await _dbContext.SaveChangesAsync( ct ); // Manually overwrite the stored string to the legacy "Sequential" value diff --git a/src/Test/Werkr.Tests.Data/packages.lock.json b/src/Test/Werkr.Tests.Data/packages.lock.json index 233f97e..d150605 100644 --- a/src/Test/Werkr.Tests.Data/packages.lock.json +++ b/src/Test/Werkr.Tests.Data/packages.lock.json @@ -1,1015 +1,1015 @@ -{ - "version": 2, - "dependencies": { - "net10.0": { - "MSTest": { - "type": "Direct", - "requested": "[4.1.0, )", - "resolved": "4.1.0", - "contentHash": "2bk47yg7HcHRyf6Zf0XgCZicTVTQj4D5lonYTO7lWMxCQB+x66VrQNc2dADBfzthKXfHaA37m8i+VV5h6SbWiA==", - "dependencies": { - "MSTest.TestAdapter": "4.1.0", - "MSTest.TestFramework": "4.1.0", - "Microsoft.NET.Test.Sdk": "18.0.1", - "Microsoft.Testing.Extensions.CodeCoverage": "18.4.1", - "Microsoft.Testing.Extensions.TrxReport": "2.1.0" - } - }, - "Grpc.AspNetCore.Server": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "diSC/ZeNdSdxHdYSOpYwuSBBDYpuNVtJQFJfiBB0WrYOQ4lVMmdxuUZJcViahQyo8pCvS3Mueo5lqFxwwMF/iw==", - "dependencies": { - "Grpc.Net.Common": "2.76.0" - } - }, - "Grpc.AspNetCore.Server.ClientFactory": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "y5KGO1GO0N2L/hCCMR05mmoK8j+v8rKvZ+9nothAxKx2Tf2CwV8f4TM5K0GkKfDsp4vrc4lm90MU6E+DeN7YIw==", - "dependencies": { - "Grpc.AspNetCore.Server": "2.76.0", - "Grpc.Net.ClientFactory": "2.76.0" - } - }, - "Grpc.Core.Api": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ==" - }, - "Grpc.Net.Common": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==", - "dependencies": { - "Grpc.Core.Api": "2.76.0" - } - }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.23.0", - "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" - }, - "Microsoft.AspNetCore.Metadata": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" - }, - "Microsoft.DiaSymReader": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite.Core": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==", - "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.4", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.Extensions.AmbientMetadata.Application": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "bovnONzrr/JIc+w343i857rJEb7cQH9UzEjbV5n67agWBEYICGQb8xiqYz5+GoFXp6mKEKLwYCQGttMU1p5yXQ==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "uDRooaV6N3WZ0kdlNPMB68/MdGn/in1Fs7Db7DnIm85RBTPy4P321WO+daAImiYpH5dekjNggDqy1N44WaIlMA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Compliance.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "4WkknDbVrHNf+S6fwSt1OAXlGJ/G/QrtJlqx4aNzOLmeT3GRyxpGLZn+Q3UV+RMRAF6FfsijEZBg2ZAW8bTAkg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.FileExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Physical": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "NkvJ8aSr3AG30yabjv7ZWwTG/wq5OElNTlNq39Ok2HSEF3TIwAc1f1xnTJlR/GuoJmEgkfT7WBO9YbSXRk41+g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ==" - }, - "Microsoft.Extensions.DependencyInjection.AutoActivation": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "ksmUG2SFTcXzYdyoLOdeSM/qYLRGN6qbbSzYVkwMK9xsctfR1hYkUayeOpFCMd7L+QSlYX72mK9wxwdgQxyS4g==", - "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg==" - }, - "Microsoft.Extensions.Diagnostics": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "1/hQmONMWxRTKXuN0pQShQN9QsqIRTS1G4fdmKW0O9phuVZjyzIROQD9Fbfwyn2t+yvP8SzjatGAPX4jDRfgHg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Features": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "7to+nkZO+g/GiGQOBzAcrr8HcG8dXETI/hg58fJju0jPO9p/GvNLAis8kMPTBdsjfeTfslBrgFX9Yx1KRnKDww==" - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileProviders.Physical": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileSystemGlobbing": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileSystemGlobbing": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg==" - }, - "Microsoft.Extensions.Http": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "QRbs+A+WfiGTnV9KFNfWlF+My5euQNZnsvdVMulwRN6C/tEPaF+ZlQfedHoNvFHKLwjQMmqwm4z+TSO9eLvRQw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Http.Diagnostics": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "ybx2QcCWROCnUCbSj/IyHXn1c58brjjHzTTbueKgBl/qHsWk69mu25mjQ3oaMsO1I0+EcS6AhVuhIopL2q3IDw==", - "dependencies": { - "Microsoft.Extensions.Http": "10.0.4", - "Microsoft.Extensions.Telemetry": "10.4.0" - } - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "S8+6fCuMOhJZGk8sGFtOy3VsF9mk9x4UOL59GM91REiA/fmCDjunKKIw4RmStG87qyXPfxelDJf2pXIbTuaBdw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Logging.Configuration": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "XPXoOpUnWEh0pV7Vl2DK2wj47y73Krhrve5OkPrvGIWdZ4U2r47WO8hEdv+wKn65Kh4pmDdiWm7Ibo5pZX+vig==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "2pufIFOgNl/yWTOoIC9XgBnO9VxgfAjdRCnVwpE2+ICfcroGnjuEAGzJ5lTdZeAe0HvA31vMBWXtcmGB7TOq3g==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg==" - }, - "Microsoft.Extensions.Resilience": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "41CCbJJPsDWU6NsmKfANHkfT/+KCBlZZqQ1eBoQhhW0xqGCiWmUlMdi2BoaM/GcwKHX5WiQL/IESROmgk0Owfw==", - "dependencies": { - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.4.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4", - "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0", - "Polly.Extensions": "8.4.2", - "Polly.RateLimiting": "8.4.2" - } - }, - "Microsoft.Extensions.ServiceDiscovery.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "HkBb7cdi27tkQiQw1anQFbXe+A3pjRwDKgVbd/DD9fMAO2X9abK0FEyM/tNVXjW3lwOWl2tF+Xij/DqI6i+JTg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Features": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "AbHleTzdpGPjA6RpOjKVHEYx7SoBRnJ2bwAbbPa3aGB7HiVwBmeTJhBGhtIBiuIW0VpKDS8x+bV5iWqpBRIf4w==", - "dependencies": { - "Microsoft.Extensions.AmbientMetadata.Application": "10.4.0", - "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.4.0", - "Microsoft.Extensions.Logging.Configuration": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0" - } - }, - "Microsoft.Extensions.Telemetry.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "3b2uVa4voJfLLg39BPCKQS0ZgnpEZFkKf7YmnMVlM5FQJYBPOuePIQdnEK1/Oxd+w3GscxGYuE7IMOXDwixZtQ==", - "dependencies": { - "Microsoft.Extensions.Compliance.Abstractions": "10.4.0", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.IdentityModel.Abstractions": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.16.0" - } - }, - "Microsoft.IdentityModel.Protocols": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.0.1" - } - }, - "Microsoft.IdentityModel.Protocols.OpenIdConnect": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", - "dependencies": { - "Microsoft.IdentityModel.Protocols": "8.0.1", - "System.IdentityModel.Tokens.Jwt": "8.0.1" - } - }, - "Microsoft.NET.Test.Sdk": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", - "dependencies": { - "Microsoft.CodeCoverage": "18.0.1", - "Microsoft.TestPlatform.TestHost": "18.0.1" - } - }, - "Microsoft.OpenApi": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "GGYLfzV/G/ct80OZ45JxnWP7NvMX1BCugn/lX7TH5o0lcVaviavsLMTxmFV2AybXWjbi3h6FF1vgZiTK6PXndw==" - }, - "Microsoft.Testing.Extensions.CodeCoverage": { - "type": "Transitive", - "resolved": "18.4.1", - "contentHash": "l1VZM9dg9s76L5D288ipAT4HRYDJ6Vxh8wX20gfS9VnpueedRfN4/aGNn4oA1g6pwq2WSM3Ci7IoSSGPiqu+WQ==", - "dependencies": { - "Microsoft.DiaSymReader": "2.0.0", - "Microsoft.Extensions.DependencyModel": "8.0.2", - "Microsoft.Testing.Platform": "2.0.2" - } - }, - "Microsoft.Testing.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "5TwgTx2u7k9Al/xbZ18QXq4Hdy2xewkVTI6K3sk+jY2ykqUkIKNuj7rFu3GOV5KnEUkevhw6eZcyZs77STHJIA==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.TrxReport": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "cXmP225WcMLLOSrW8xekaNhfzdBwXX3cbXbE5qSzmLbK0KZe3z8rAObKj70FWiPPPzm2W22x0ZW93gsmAfK6Mg==", - "dependencies": { - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.TrxReport.Abstractions": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "D8xmIJYQFJ6D49Rx5/vPrkZZxb338Jkew+eSqZLBfBiWKw4QZKy3i1BOXiLfz0lOmaNErwDz/YWRojCdNl+B9Q==", - "dependencies": { - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.VSTestBridge": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "bNRIEA2YoGr+Y+7LHdA7i1U80+7BAdf4K4Qh4Kx6eKkoBK/NV7QpoMg+GWPP0/eqAFzuUmUOIPVZ87Oo0Vyxmw==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", - "Microsoft.Testing.Extensions.Telemetry": "2.1.0", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Platform": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "aHkjNTGIA+Zbdw6RJgSFrbDrCjO0CgqpElqYcvkRSeUhBv2bKarnvU3ep786U7UqrPlArT/B7VmImRibJD0Zrg==" - }, - "Microsoft.Testing.Platform.MSBuild": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "UpfPebXQtHGrWz21+YLHmJSm+5zsuPE9U9pfdCtoB+67g75fDmWlNgpkH2ZmdVhSwkjNIed9Icg8Iu63z2ei5Q==", - "dependencies": { - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", - "Newtonsoft.Json": "13.0.3" - } - }, - "MSTest.Analyzers": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "4ElL/aqomiUInr090VN4udqz46AuszXLrifHkLrgj0zb7na8eAoyUQt3BwDLTcGd1bSkmk3SfD02rZtKU+ZiqQ==" - }, - "MSTest.TestAdapter": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "bRW1Hftwq0XbcVExcAbj4YAfSZDRAziL0mygDkPBvaUe2nSsWFQIatze5lHVjPFJMvSFgWnItku4pguIy5FowQ==", - "dependencies": { - "MSTest.TestFramework": "4.1.0", - "Microsoft.Testing.Extensions.VSTestBridge": "2.1.0", - "Microsoft.Testing.Platform.MSBuild": "2.1.0" - } - }, - "MSTest.TestFramework": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "BzpvsK+CRbk6khwY62h+7HfYzIxtJXyPv9tOI9T90cy5CVy+WI1JkN4ZaNL4Dobqb6dywSwabLTIbPZKpdrr+A==", - "dependencies": { - "MSTest.Analyzers": "4.1.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "Npgsql": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "10.0.0" - } - }, - "OpenTelemetry": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==", - "dependencies": { - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", - "Microsoft.Extensions.Logging.Configuration": "10.0.0", - "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0" - } - }, - "OpenTelemetry.Api": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w==" - }, - "OpenTelemetry.Api.ProviderBuilderExtensions": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", - "OpenTelemetry.Api": "1.15.0" - } - }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, - "Polly.Extensions": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Polly.Core": "8.4.2" - } - }, - "Polly.RateLimiting": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", - "dependencies": { - "Polly.Core": "8.4.2", - "System.Threading.RateLimiting": "8.0.0" - } - }, - "Serilog": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" - }, - "Serilog.Extensions.Hosting": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", - "Microsoft.Extensions.Logging.Abstractions": "10.0.0", - "Serilog": "4.3.0", - "Serilog.Extensions.Logging": "10.0.0" - } - }, - "Serilog.Extensions.Logging": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", - "dependencies": { - "Microsoft.Extensions.Logging": "10.0.0", - "Serilog": "4.2.0" - } - }, - "Serilog.Formatting.Compact": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "Serilog.Settings.Configuration": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "10.0.0", - "Microsoft.Extensions.DependencyModel": "10.0.0", - "Serilog": "4.3.0" - } - }, - "Serilog.Sinks.Debug": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "SQLitePCLRaw.bundle_e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", - "dependencies": { - "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", - "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" - } - }, - "SQLitePCLRaw.core": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" - }, - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" - }, - "SQLitePCLRaw.provider.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", - "dependencies": { - "SQLitePCLRaw.core": "2.1.11" - } - }, - "System.Threading.RateLimiting": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" - }, - "werkr.api": { - "type": "Project", - "dependencies": { - "Grpc.AspNetCore": "[2.76.0, )", - "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.4, )", - "Microsoft.AspNetCore.OpenApi": "[10.0.4, )", - "Microsoft.IdentityModel.JsonWebTokens": "[8.16.0, )", - "Serilog.AspNetCore": "[10.0.0, )", - "Serilog.Sinks.Console": "[6.1.1, )", - "Serilog.Sinks.File": "[7.0.0, )", - "Serilog.Sinks.OpenTelemetry": "[4.2.0, )", - "Werkr.Common": "[1.0.0, )", - "Werkr.Core": "[1.0.0, )", - "Werkr.Data": "[1.0.0, )", - "Werkr.ServiceDefaults": "[1.0.0, )" - } - }, - "werkr.common": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.34.0, )", - "Microsoft.AspNetCore.Authorization": "[10.0.4, )", - "Microsoft.Extensions.Configuration.Json": "[10.0.4, )", - "Microsoft.IdentityModel.Tokens": "[8.16.0, )", - "Werkr.Common.Configuration": "[1.0.0, )" - } - }, - "werkr.common.configuration": { - "type": "Project" - }, - "werkr.core": { - "type": "Project", - "dependencies": { - "Grpc.Net.Client": "[2.76.0, )", - "Microsoft.Extensions.Hosting.Abstractions": "[10.0.4, )", - "System.Security.Cryptography.ProtectedData": "[10.0.4, )", - "Werkr.Common": "[1.0.0, )", - "Werkr.Data": "[1.0.0, )" - } - }, - "werkr.data": { - "type": "Project", - "dependencies": { - "EFCore.NamingConventions": "[10.0.1, )", - "Microsoft.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )", - "Werkr.Common": "[1.0.0, )" - } - }, - "werkr.servicedefaults": { - "type": "Project", - "dependencies": { - "Microsoft.Extensions.Http.Resilience": "[10.4.0, )", - "Microsoft.Extensions.ServiceDiscovery": "[10.4.0, )", - "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.0, )", - "OpenTelemetry.Extensions.Hosting": "[1.15.0, )" - } - }, - "EFCore.NamingConventions": { - "type": "CentralTransitive", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" - } - }, - "Google.Protobuf": { - "type": "CentralTransitive", - "requested": "[3.34.0, )", - "resolved": "3.34.0", - "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" - }, - "Grpc.AspNetCore": { - "type": "CentralTransitive", - "requested": "[2.76.0, )", - "resolved": "2.76.0", - "contentHash": "LyXMmpN2Ba0TE35SOLSKbGqIYtJuhc1UgiaGfoW1X8KJERV70QI5KGW+ckEY7MrXoFWN/uWo4B70siVhbDmCgQ==", - "dependencies": { - "Google.Protobuf": "3.31.1", - "Grpc.AspNetCore.Server.ClientFactory": "2.76.0", - "Grpc.Tools": "2.76.0" - } - }, - "Grpc.Net.Client": { - "type": "CentralTransitive", - "requested": "[2.76.0, )", - "resolved": "2.76.0", - "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==", - "dependencies": { - "Grpc.Net.Common": "2.76.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Grpc.Net.ClientFactory": { - "type": "CentralTransitive", - "requested": "[2.76.0, )", - "resolved": "2.76.0", - "contentHash": "XI+kO69L9AV8B9N0UQOmH911r6MOEp9huHiavEsY56DJYuzJ9KAxNGy37dpV6CLbgCaN2uKmpOsZ9Pao6bmpVQ==", - "dependencies": { - "Grpc.Net.Client": "2.76.0", - "Microsoft.Extensions.Http": "8.0.0" - } - }, - "Microsoft.AspNetCore.Authentication.JwtBearer": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1mUwGyeGrGETdhlU/ZzNZeN2J6ugqf9EztZyG+WQIZwkaH5+lFNza15+cNCXhXNA0MKo1iohr5vj60eqR2J44Q==", - "dependencies": { - "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" - } - }, - "Microsoft.AspNetCore.Authorization": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==", - "dependencies": { - "Microsoft.AspNetCore.Metadata": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.AspNetCore.OpenApi": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "OsEhbmT4Xenukau5YCR867gr/HmuAJ9DqMBPQGTcmdNU/TqBqdcnB+yLNwD/mTdkHzLBB+XG7cI4H1L5B1jx+Q==", - "dependencies": { - "Microsoft.OpenApi": "2.0.0" - } - }, - "Microsoft.Data.Sqlite.Core": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==", - "dependencies": { - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.EntityFrameworkCore": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "+5mQrqlBhqNUaPyDmFSNM/qiWStvE9LMxZW1MRF0NhEbO781xNeKryXNR9gGDJ0PmYFDAVoMT9ffX+I15LPFTw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Http.Resilience": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "HbkUsPUC7vLy2TaDbdA9aooW64n9yX4sUppRuiJ1cOzzU1FUW+MVEotm6kYVq6AuUI9xwFSBhRFzA03blmk3VA==", - "dependencies": { - "Microsoft.Extensions.Http.Diagnostics": "10.4.0", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Resilience": "10.4.0" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.ServiceDiscovery": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "RznZAH6L4RNvroECT5JpqfFQJjHTn+8N7+ThSgYutbshkuymFeL/uBIZt1CM8LOdpPPhn4//a5fLUah9/k7ayQ==", - "dependencies": { - "Microsoft.Extensions.Http": "10.0.4", - "Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.4.0" - } - }, - "Microsoft.IdentityModel.JsonWebTokens": { - "type": "CentralTransitive", - "requested": "[8.16.0, )", - "resolved": "8.16.0", - "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.16.0" - } - }, - "Microsoft.IdentityModel.Tokens": { - "type": "CentralTransitive", - "requested": "[8.16.0, )", - "resolved": "8.16.0", - "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "10.0.0", - "Microsoft.IdentityModel.Logging": "8.16.0" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)", - "Npgsql": "10.0.0" - } - }, - "OpenTelemetry.Exporter.OpenTelemetryProtocol": { - "type": "CentralTransitive", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==", - "dependencies": { - "OpenTelemetry": "1.15.0" - } - }, - "OpenTelemetry.Extensions.Hosting": { - "type": "CentralTransitive", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==", - "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", - "OpenTelemetry": "1.15.0" - } - }, - "Serilog.AspNetCore": { - "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", - "dependencies": { - "Serilog": "4.3.0", - "Serilog.Extensions.Hosting": "10.0.0", - "Serilog.Formatting.Compact": "3.0.0", - "Serilog.Settings.Configuration": "10.0.0", - "Serilog.Sinks.Console": "6.1.1", - "Serilog.Sinks.Debug": "3.0.0", - "Serilog.Sinks.File": "7.0.0" - } - }, - "Serilog.Sinks.Console": { - "type": "CentralTransitive", - "requested": "[6.1.1, )", - "resolved": "6.1.1", - "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "Serilog.Sinks.File": { - "type": "CentralTransitive", - "requested": "[7.0.0, )", - "resolved": "7.0.0", - "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", - "dependencies": { - "Serilog": "4.2.0" - } - }, - "Serilog.Sinks.OpenTelemetry": { - "type": "CentralTransitive", - "requested": "[4.2.0, )", - "resolved": "4.2.0", - "contentHash": "PzMCyE5G19tjr5IZEi5qg+4UU5QrxBEoBEMu/hhYybTrGKXqUDiSGWKZNUDBgelaVKqLADlsmlJVyKce5SyPrg==", - "dependencies": { - "Google.Protobuf": "3.30.1", - "Grpc.Net.Client": "2.70.0", - "Serilog": "4.2.0" - } - }, - "System.IdentityModel.Tokens.Jwt": { - "type": "CentralTransitive", - "requested": "[8.16.0, )", - "resolved": "8.0.1", - "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", - "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", - "Microsoft.IdentityModel.Tokens": "8.0.1" - } - }, - "System.Security.Cryptography.ProtectedData": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g==" - } - } - } +{ + "version": 2, + "dependencies": { + "net10.0": { + "MSTest": { + "type": "Direct", + "requested": "[4.1.0, )", + "resolved": "4.1.0", + "contentHash": "2bk47yg7HcHRyf6Zf0XgCZicTVTQj4D5lonYTO7lWMxCQB+x66VrQNc2dADBfzthKXfHaA37m8i+VV5h6SbWiA==", + "dependencies": { + "MSTest.TestAdapter": "4.1.0", + "MSTest.TestFramework": "4.1.0", + "Microsoft.NET.Test.Sdk": "18.0.1", + "Microsoft.Testing.Extensions.CodeCoverage": "18.4.1", + "Microsoft.Testing.Extensions.TrxReport": "2.1.0" + } + }, + "Grpc.AspNetCore.Server": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "diSC/ZeNdSdxHdYSOpYwuSBBDYpuNVtJQFJfiBB0WrYOQ4lVMmdxuUZJcViahQyo8pCvS3Mueo5lqFxwwMF/iw==", + "dependencies": { + "Grpc.Net.Common": "2.76.0" + } + }, + "Grpc.AspNetCore.Server.ClientFactory": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "y5KGO1GO0N2L/hCCMR05mmoK8j+v8rKvZ+9nothAxKx2Tf2CwV8f4TM5K0GkKfDsp4vrc4lm90MU6E+DeN7YIw==", + "dependencies": { + "Grpc.AspNetCore.Server": "2.76.0", + "Grpc.Net.ClientFactory": "2.76.0" + } + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ==" + }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==", + "dependencies": { + "Grpc.Core.Api": "2.76.0" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.AmbientMetadata.Application": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "bovnONzrr/JIc+w343i857rJEb7cQH9UzEjbV5n67agWBEYICGQb8xiqYz5+GoFXp6mKEKLwYCQGttMU1p5yXQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.4", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "4WkknDbVrHNf+S6fwSt1OAXlGJ/G/QrtJlqx4aNzOLmeT3GRyxpGLZn+Q3UV+RMRAF6FfsijEZBg2ZAW8bTAkg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" + }, + "Microsoft.Extensions.DependencyInjection.AutoActivation": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "ksmUG2SFTcXzYdyoLOdeSM/qYLRGN6qbbSzYVkwMK9xsctfR1hYkUayeOpFCMd7L+QSlYX72mK9wxwdgQxyS4g==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.4" + } + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "1/hQmONMWxRTKXuN0pQShQN9QsqIRTS1G4fdmKW0O9phuVZjyzIROQD9Fbfwyn2t+yvP8SzjatGAPX4jDRfgHg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + } + }, + "Microsoft.Extensions.Features": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "7to+nkZO+g/GiGQOBzAcrr8HcG8dXETI/hg58fJju0jPO9p/GvNLAis8kMPTBdsjfeTfslBrgFX9Yx1KRnKDww==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" + }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "QRbs+A+WfiGTnV9KFNfWlF+My5euQNZnsvdVMulwRN6C/tEPaF+ZlQfedHoNvFHKLwjQMmqwm4z+TSO9eLvRQw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.Diagnostics": "10.0.4", + "Microsoft.Extensions.Logging": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.Extensions.Http.Diagnostics": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "ybx2QcCWROCnUCbSj/IyHXn1c58brjjHzTTbueKgBl/qHsWk69mu25mjQ3oaMsO1I0+EcS6AhVuhIopL2q3IDw==", + "dependencies": { + "Microsoft.Extensions.Http": "10.0.4", + "Microsoft.Extensions.Telemetry": "10.4.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "XPXoOpUnWEh0pV7Vl2DK2wj47y73Krhrve5OkPrvGIWdZ4U2r47WO8hEdv+wKn65Kh4pmDdiWm7Ibo5pZX+vig==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", + "Microsoft.Extensions.Configuration.Binder": "10.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.Logging": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "2pufIFOgNl/yWTOoIC9XgBnO9VxgfAjdRCnVwpE2+ICfcroGnjuEAGzJ5lTdZeAe0HvA31vMBWXtcmGB7TOq3g==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.Extensions.Resilience": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "41CCbJJPsDWU6NsmKfANHkfT/+KCBlZZqQ1eBoQhhW0xqGCiWmUlMdi2BoaM/GcwKHX5WiQL/IESROmgk0Owfw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics": "10.0.4", + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.4.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4", + "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0", + "Polly.Extensions": "8.4.2", + "Polly.RateLimiting": "8.4.2" + } + }, + "Microsoft.Extensions.ServiceDiscovery.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "HkBb7cdi27tkQiQw1anQFbXe+A3pjRwDKgVbd/DD9fMAO2X9abK0FEyM/tNVXjW3lwOWl2tF+Xij/DqI6i+JTg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", + "Microsoft.Extensions.Configuration.Binder": "10.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.Features": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4", + "Microsoft.Extensions.Primitives": "10.0.4" + } + }, + "Microsoft.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "AbHleTzdpGPjA6RpOjKVHEYx7SoBRnJ2bwAbbPa3aGB7HiVwBmeTJhBGhtIBiuIW0VpKDS8x+bV5iWqpBRIf4w==", + "dependencies": { + "Microsoft.Extensions.AmbientMetadata.Application": "10.4.0", + "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.4.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0" + } + }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "3b2uVa4voJfLLg39BPCKQS0ZgnpEZFkKf7YmnMVlM5FQJYBPOuePIQdnEK1/Oxd+w3GscxGYuE7IMOXDwixZtQ==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "10.4.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.16.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", + "dependencies": { + "Microsoft.CodeCoverage": "18.0.1", + "Microsoft.TestPlatform.TestHost": "18.0.1" + } + }, + "Microsoft.OpenApi": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "GGYLfzV/G/ct80OZ45JxnWP7NvMX1BCugn/lX7TH5o0lcVaviavsLMTxmFV2AybXWjbi3h6FF1vgZiTK6PXndw==" + }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Transitive", + "resolved": "18.4.1", + "contentHash": "l1VZM9dg9s76L5D288ipAT4HRYDJ6Vxh8wX20gfS9VnpueedRfN4/aGNn4oA1g6pwq2WSM3Ci7IoSSGPiqu+WQ==", + "dependencies": { + "Microsoft.DiaSymReader": "2.0.0", + "Microsoft.Extensions.DependencyModel": "8.0.2", + "Microsoft.Testing.Platform": "2.0.2" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "5TwgTx2u7k9Al/xbZ18QXq4Hdy2xewkVTI6K3sk+jY2ykqUkIKNuj7rFu3GOV5KnEUkevhw6eZcyZs77STHJIA==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.TrxReport": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "cXmP225WcMLLOSrW8xekaNhfzdBwXX3cbXbE5qSzmLbK0KZe3z8rAObKj70FWiPPPzm2W22x0ZW93gsmAfK6Mg==", + "dependencies": { + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "D8xmIJYQFJ6D49Rx5/vPrkZZxb338Jkew+eSqZLBfBiWKw4QZKy3i1BOXiLfz0lOmaNErwDz/YWRojCdNl+B9Q==", + "dependencies": { + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.VSTestBridge": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "bNRIEA2YoGr+Y+7LHdA7i1U80+7BAdf4K4Qh4Kx6eKkoBK/NV7QpoMg+GWPP0/eqAFzuUmUOIPVZ87Oo0Vyxmw==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Microsoft.Testing.Extensions.Telemetry": "2.1.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "aHkjNTGIA+Zbdw6RJgSFrbDrCjO0CgqpElqYcvkRSeUhBv2bKarnvU3ep786U7UqrPlArT/B7VmImRibJD0Zrg==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "UpfPebXQtHGrWz21+YLHmJSm+5zsuPE9U9pfdCtoB+67g75fDmWlNgpkH2ZmdVhSwkjNIed9Icg8Iu63z2ei5Q==", + "dependencies": { + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "MSTest.Analyzers": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "4ElL/aqomiUInr090VN4udqz46AuszXLrifHkLrgj0zb7na8eAoyUQt3BwDLTcGd1bSkmk3SfD02rZtKU+ZiqQ==" + }, + "MSTest.TestAdapter": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "bRW1Hftwq0XbcVExcAbj4YAfSZDRAziL0mygDkPBvaUe2nSsWFQIatze5lHVjPFJMvSFgWnItku4pguIy5FowQ==", + "dependencies": { + "MSTest.TestFramework": "4.1.0", + "Microsoft.Testing.Extensions.VSTestBridge": "2.1.0", + "Microsoft.Testing.Platform.MSBuild": "2.1.0" + } + }, + "MSTest.TestFramework": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "BzpvsK+CRbk6khwY62h+7HfYzIxtJXyPv9tOI9T90cy5CVy+WI1JkN4ZaNL4Dobqb6dywSwabLTIbPZKpdrr+A==", + "dependencies": { + "MSTest.Analyzers": "4.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.15.0" + } + }, + "Polly.Core": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" + }, + "Polly.Extensions": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Polly.Core": "8.4.2" + } + }, + "Polly.RateLimiting": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", + "dependencies": { + "Polly.Core": "8.4.2", + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "werkr.api": { + "type": "Project", + "dependencies": { + "Grpc.AspNetCore": "[2.76.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.5, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.5, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.16.0, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "Serilog.Sinks.File": "[7.0.0, )", + "Serilog.Sinks.OpenTelemetry": "[4.2.0, )", + "Werkr.Common": "[1.0.0, )", + "Werkr.Core": "[1.0.0, )", + "Werkr.Data": "[1.0.0, )", + "Werkr.ServiceDefaults": "[1.0.0, )" + } + }, + "werkr.common": { + "type": "Project", + "dependencies": { + "Google.Protobuf": "[3.34.0, )", + "Microsoft.AspNetCore.Authorization": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Json": "[10.0.5, )", + "Microsoft.IdentityModel.Tokens": "[8.16.0, )", + "Werkr.Common.Configuration": "[1.0.0, )" + } + }, + "werkr.common.configuration": { + "type": "Project" + }, + "werkr.core": { + "type": "Project", + "dependencies": { + "Grpc.Net.Client": "[2.76.0, )", + "Microsoft.Extensions.Hosting.Abstractions": "[10.0.5, )", + "System.Security.Cryptography.ProtectedData": "[10.0.5, )", + "Werkr.Common": "[1.0.0, )", + "Werkr.Data": "[1.0.0, )" + } + }, + "werkr.data": { + "type": "Project", + "dependencies": { + "EFCore.NamingConventions": "[10.0.1, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "Werkr.Common": "[1.0.0, )" + } + }, + "werkr.servicedefaults": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Http.Resilience": "[10.4.0, )", + "Microsoft.Extensions.ServiceDiscovery": "[10.4.0, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.0, )", + "OpenTelemetry.Extensions.Hosting": "[1.15.0, )" + } + }, + "EFCore.NamingConventions": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + } + }, + "Google.Protobuf": { + "type": "CentralTransitive", + "requested": "[3.34.0, )", + "resolved": "3.34.0", + "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" + }, + "Grpc.AspNetCore": { + "type": "CentralTransitive", + "requested": "[2.76.0, )", + "resolved": "2.76.0", + "contentHash": "LyXMmpN2Ba0TE35SOLSKbGqIYtJuhc1UgiaGfoW1X8KJERV70QI5KGW+ckEY7MrXoFWN/uWo4B70siVhbDmCgQ==", + "dependencies": { + "Google.Protobuf": "3.31.1", + "Grpc.AspNetCore.Server.ClientFactory": "2.76.0", + "Grpc.Tools": "2.76.0" + } + }, + "Grpc.Net.Client": { + "type": "CentralTransitive", + "requested": "[2.76.0, )", + "resolved": "2.76.0", + "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==", + "dependencies": { + "Grpc.Net.Common": "2.76.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "Grpc.Net.ClientFactory": { + "type": "CentralTransitive", + "requested": "[2.76.0, )", + "resolved": "2.76.0", + "contentHash": "XI+kO69L9AV8B9N0UQOmH911r6MOEp9huHiavEsY56DJYuzJ9KAxNGy37dpV6CLbgCaN2uKmpOsZ9Pao6bmpVQ==", + "dependencies": { + "Grpc.Net.Client": "2.76.0", + "Microsoft.Extensions.Http": "8.0.0" + } + }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==", + "dependencies": { + "Microsoft.OpenApi": "2.0.0" + } + }, + "Microsoft.Data.Sqlite.Core": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Http.Resilience": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "HbkUsPUC7vLy2TaDbdA9aooW64n9yX4sUppRuiJ1cOzzU1FUW+MVEotm6kYVq6AuUI9xwFSBhRFzA03blmk3VA==", + "dependencies": { + "Microsoft.Extensions.Http.Diagnostics": "10.4.0", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Resilience": "10.4.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.ServiceDiscovery": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "RznZAH6L4RNvroECT5JpqfFQJjHTn+8N7+ThSgYutbshkuymFeL/uBIZt1CM8LOdpPPhn4//a5fLUah9/k7ayQ==", + "dependencies": { + "Microsoft.Extensions.Http": "10.0.4", + "Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.4.0" + } + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "CentralTransitive", + "requested": "[8.16.0, )", + "resolved": "8.16.0", + "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.16.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "CentralTransitive", + "requested": "[8.16.0, )", + "resolved": "8.16.0", + "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.IdentityModel.Logging": "8.16.0" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==", + "dependencies": { + "OpenTelemetry": "1.15.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.15.0" + } + }, + "Serilog.AspNetCore": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "Serilog.Sinks.OpenTelemetry": { + "type": "CentralTransitive", + "requested": "[4.2.0, )", + "resolved": "4.2.0", + "contentHash": "PzMCyE5G19tjr5IZEi5qg+4UU5QrxBEoBEMu/hhYybTrGKXqUDiSGWKZNUDBgelaVKqLADlsmlJVyKce5SyPrg==", + "dependencies": { + "Google.Protobuf": "3.30.1", + "Grpc.Net.Client": "2.70.0", + "Serilog": "4.2.0" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "CentralTransitive", + "requested": "[8.16.0, )", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "System.Security.Cryptography.ProtectedData": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg==" + } + } + } } \ No newline at end of file diff --git a/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs b/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs index c648a1a..f47764d 100644 --- a/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs +++ b/src/Test/Werkr.Tests.Server/Authorization/PageAuthorizationTests.cs @@ -45,7 +45,8 @@ public class PageAuthorizationTests { ["/tasks/create"] = string.Empty, ["/tasks/{Id:long}"] = string.Empty, ["/workflows"] = string.Empty, - ["/workflows/create"] = string.Empty, + ["/workflows/create"] = "Admin,Operator", + ["/workflows/new"] = "Admin,Operator", ["/workflows/{Id:long}"] = string.Empty, ["/workflows/{WorkflowId:long}/runs"] = string.Empty, ["/workflows/runs/{RunId:guid}"] = string.Empty, diff --git a/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs b/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs index 76b7505..8a6b506 100644 --- a/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs +++ b/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs @@ -22,7 +22,7 @@ public void Renders_StructuredMode_ByDefault( ) { .Add( p => p.ExpressionChanged, EventCallback.Factory.Create( this, _ => { } ) ) ); // Should not show the raw input - Assert.ThrowsExactly( ( ) => cut.Find( "input[type=text]" ) ); + _ = Assert.ThrowsExactly( ( ) => cut.Find( "input[type=text]" ) ); // Should show the structured dropdowns — Type select should exist IReadOnlyList selects = cut.FindAll( "select" ); diff --git a/src/Test/Werkr.Tests.Server/Components/TaskCreateModalTests.cs b/src/Test/Werkr.Tests.Server/Components/TaskCreateModalTests.cs deleted file mode 100644 index 1a930f1..0000000 --- a/src/Test/Werkr.Tests.Server/Components/TaskCreateModalTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; -using Bunit; -using Microsoft.AspNetCore.Components; -using Microsoft.Extensions.DependencyInjection; -using Werkr.Common.Models; -using Werkr.Server.Components.Shared; - -namespace Werkr.Tests.Server.Components; - -/// -/// bUnit tests for the component. Validates modal -/// visibility, form rendering, successful submission, and cancellation. -/// -[TestClass] -public class TaskCreateModalTests : BunitContext { - - /// - /// Registers a fake that returns an - /// backed by the given handler. - /// - private void RegisterHttpClient( HttpMessageHandler handler ) { - HttpClient client = new( handler ) { BaseAddress = new Uri( "http://localhost" ) }; - IHttpClientFactory factory = new FakeHttpClientFactory( client ); - Services.AddSingleton( factory ); - } - - /// - /// Verifies that the modal is hidden by default (no modal markup rendered). - /// - [TestMethod] - public void Modal_IsHidden_ByDefault( ) { - RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); - - IRenderedComponent cut = Render( parameters => - parameters.Add( p => p.WorkflowId, 42L ) - .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); - - // Modal should not be visible - Assert.ThrowsExactly( ( ) => cut.Find( ".modal" ) ); - } - - /// - /// Verifies that calling Show() makes the modal visible with the expected form fields. - /// - [TestMethod] - public void Show_RendersModalWithFormFields( ) { - RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); - - IRenderedComponent cut = Render( parameters => - parameters.Add( p => p.WorkflowId, 42L ) - .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); - - cut.InvokeAsync( ( ) => cut.Instance.Show( ) ); - - // Modal should be visible - AngleSharp.Dom.IElement modal = cut.Find( ".modal" ); - Assert.IsNotNull( modal ); - - // Should have a Name input - Assert.Contains( "Name", cut.Markup, "Should render Name field." ); - - // Should have Action Type select - Assert.Contains( "Action Type", cut.Markup, "Should render Action Type field." ); - - // Should have Create Task submit button - Assert.Contains( "Create Task", cut.Markup, "Should render submit button." ); - - // Should have Cancel button - Assert.Contains( "Cancel", cut.Markup, "Should render cancel button." ); - } - - /// - /// Verifies that clicking Cancel closes the modal. - /// - [TestMethod] - public void Cancel_ClosesModal( ) { - RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); - - IRenderedComponent cut = Render( parameters => - parameters.Add( p => p.WorkflowId, 42L ) - .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); - - cut.InvokeAsync( ( ) => cut.Instance.Show( ) ); - - // Click Cancel - AngleSharp.Dom.IElement cancelButton = cut.Find( "button.btn-outline-secondary" ); - cancelButton.Click( ); - - // Modal should be hidden again - Assert.ThrowsExactly( ( ) => cut.Find( ".modal" ) ); - } - - /// - /// Verifies that the Action Type dropdown contains the expected options. - /// - [TestMethod] - public void ActionTypeDropdown_HasExpectedOptions( ) { - RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); - - IRenderedComponent cut = Render( parameters => - parameters.Add( p => p.WorkflowId, 42L ) - .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); - - cut.InvokeAsync( ( ) => cut.Instance.Show( ) ); - - IReadOnlyList options = cut.FindAll( ".form-select option" ); - string[] optionValues = [.. options.Select( o => o.GetAttribute( "value" ) ?? "" )]; - - Assert.Contains( "PowerShellCommand", optionValues, "Should have PowerShellCommand." ); - Assert.Contains( "ShellCommand", optionValues, "Should have ShellCommand." ); - Assert.Contains( "ShellScript", optionValues, "Should have ShellScript." ); - Assert.Contains( "Action", optionValues, "Should have Action." ); - } - - // ── Helpers ── - - /// Minimal fake that always returns the given response. - private sealed class StubHandler( HttpStatusCode statusCode, string content ) : HttpMessageHandler { - protected override Task SendAsync( - HttpRequestMessage request, CancellationToken cancellationToken ) => - Task.FromResult( new HttpResponseMessage( statusCode ) { - Content = new StringContent( content, System.Text.Encoding.UTF8, "application/json" ), - } ); - } - - /// Fake that always returns the same client. - private sealed class FakeHttpClientFactory( HttpClient client ) : IHttpClientFactory { - public HttpClient CreateClient( string name ) => client; - } -} diff --git a/src/Test/Werkr.Tests.Server/Components/TaskSetupModalTests.cs b/src/Test/Werkr.Tests.Server/Components/TaskSetupModalTests.cs new file mode 100644 index 0000000..6c85fd2 --- /dev/null +++ b/src/Test/Werkr.Tests.Server/Components/TaskSetupModalTests.cs @@ -0,0 +1,265 @@ +using System.Net; +using System.Text.Json; +using Bunit; +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.DependencyInjection; +using Werkr.Common.Models; +using Werkr.Server.Components.Shared; + +namespace Werkr.Tests.Server.Components; + +/// +/// bUnit tests for the component. Validates modal +/// visibility, form rendering, successful submission, cancellation, and edit mode. +/// +[TestClass] +public class TaskSetupModalTests : BunitContext { + + /// + /// Registers a fake that returns an + /// backed by the given handler. + /// + private void RegisterHttpClient( HttpMessageHandler handler ) { + HttpClient client = new( handler ) { BaseAddress = new Uri( "http://localhost" ) }; + IHttpClientFactory factory = new FakeHttpClientFactory( client ); + _ = Services.AddSingleton( factory ); + } + + /// + /// Verifies that the modal is hidden by default (no modal markup rendered). + /// + [TestMethod] + public void Modal_IsHidden_ByDefault( ) { + RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); + + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.WorkflowId, 42L ) + .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); + + // Modal should not be visible + _ = Assert.ThrowsExactly( ( ) => cut.Find( ".modal" ) ); + } + + /// + /// Verifies that calling Show() makes the modal visible with the expected form fields. + /// + [TestMethod] + public void Show_RendersModalWithFormFields( ) { + RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); + + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.WorkflowId, 42L ) + .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); + + _ = cut.InvokeAsync( ( ) => cut.Instance.Show( ) ); + + // Modal should be visible + AngleSharp.Dom.IElement modal = cut.Find( ".modal" ); + Assert.IsNotNull( modal ); + + // Should have a Name input + Assert.Contains( "Name", cut.Markup, "Should render Name field." ); + + // Should have Action Type select + Assert.Contains( "Action Type", cut.Markup, "Should render Action Type field." ); + + // Should have Create Task submit button + Assert.Contains( "Create Task", cut.Markup, "Should render submit button." ); + + // Should have Cancel button + Assert.Contains( "Cancel", cut.Markup, "Should render cancel button." ); + } + + /// + /// Verifies that clicking Cancel closes the modal. + /// + [TestMethod] + public void Cancel_ClosesModal( ) { + RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); + + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.WorkflowId, 42L ) + .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); + + _ = cut.InvokeAsync( ( ) => cut.Instance.Show( ) ); + + // Click Cancel + AngleSharp.Dom.IElement cancelButton = cut.Find( "button.btn-outline-secondary" ); + cancelButton.Click( ); + + // Modal should be hidden again + _ = Assert.ThrowsExactly( ( ) => cut.Find( ".modal" ) ); + } + + /// + /// Verifies that the Action Type dropdown contains the expected options. + /// + [TestMethod] + public void ActionTypeDropdown_HasExpectedOptions( ) { + RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); + + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.WorkflowId, 42L ) + .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) ); + + _ = cut.InvokeAsync( ( ) => cut.Instance.Show( ) ); + + IReadOnlyList options = cut.FindAll( ".form-select option" ); + string[] optionValues = [.. options.Select( o => o.GetAttribute( "value" ) ?? "" )]; + + Assert.Contains( "PowerShellCommand", optionValues, "Should have PowerShellCommand." ); + Assert.Contains( "ShellCommand", optionValues, "Should have ShellCommand." ); + Assert.Contains( "ShellScript", optionValues, "Should have ShellScript." ); + Assert.Contains( "Action", optionValues, "Should have Action." ); + } + + /// + /// Verifies that calling Show(TaskDto) populates the form for editing with the correct title and button text. + /// + [TestMethod] + public void Show_WithTaskDto_PopulatesFormForEditing( ) { + RegisterHttpClient( new StubHandler( HttpStatusCode.OK, "{}" ) ); + + TaskDto existingTask = new( + Id: 7, + Name: "My Task", + Description: "Test desc", + ActionType: "ShellCommand", + Content: "echo hello", + Arguments: ["--verbose"], + TargetTags: ["linux"], + Enabled: true, + TimeoutMinutes: 30, + SyncIntervalMinutes: 5, + SuccessCriteria: null, + EffectiveSuccessCriteria: "ExitCodeZero", + WorkflowId: 42 ); + + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.WorkflowId, 42L ) + .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) + .Add( p => p.OnTaskUpdated, EventCallback.Factory.Create( this, _ => { } ) ) ); + + _ = cut.InvokeAsync( ( ) => cut.Instance.Show( existingTask ) ); + + // Modal should be visible with edit title + Assert.Contains( "Edit Task", cut.Markup, "Should render edit mode title." ); + + // Should have Save Changes button instead of Create Task + Assert.Contains( "Save Changes", cut.Markup, "Should render save button in edit mode." ); + + // Should NOT have Create Task button + Assert.DoesNotContain( "Create Task", cut.Markup, "Should not render create button in edit mode." ); + } + + /// + /// Verifies that submitting in edit mode sends a PUT request to the correct endpoint. + /// + [TestMethod] + public void Submit_InEditMode_SendsPutRequest( ) { + HttpMethod? capturedMethod = null; + string? capturedUri = null; + + CapturingHandler handler = new( HttpStatusCode.OK, + JsonSerializer.Serialize( new TaskDto( + Id: 7, Name: "Updated", Description: "Updated desc", + ActionType: "ShellCommand", Content: "echo updated", + Arguments: null, TargetTags: ["linux"], Enabled: true, + TimeoutMinutes: 30, SyncIntervalMinutes: 5, + SuccessCriteria: null, EffectiveSuccessCriteria: "ExitCodeZero", + WorkflowId: 42 ) ), + ( method, uri ) => { capturedMethod = method; capturedUri = uri; } ); + + RegisterHttpClient( handler ); + + TaskDto existingTask = new( + Id: 7, Name: "My Task", Description: "Test desc", + ActionType: "ShellCommand", Content: "echo hello", + Arguments: null, TargetTags: ["linux"], Enabled: true, + TimeoutMinutes: 30, SyncIntervalMinutes: 5, + SuccessCriteria: null, EffectiveSuccessCriteria: "ExitCodeZero", + WorkflowId: 42 ); + + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.WorkflowId, 42L ) + .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) + .Add( p => p.OnTaskUpdated, EventCallback.Factory.Create( this, _ => { } ) ) ); + + _ = cut.InvokeAsync( ( ) => cut.Instance.Show( existingTask ) ); + + // Submit the form + cut.Find( "form" ).Submit( ); + + Assert.AreEqual( HttpMethod.Put, capturedMethod, "Should send PUT request in edit mode." ); + Assert.AreEqual( "/api/tasks/7", capturedUri, "Should target the correct task endpoint." ); + } + + /// + /// Verifies that a successful edit invokes the OnTaskUpdated callback. + /// + [TestMethod] + public void Submit_InEditMode_InvokesOnTaskUpdated( ) { + TaskDto? updatedResult = null; + + TaskDto responseDto = new( + Id: 7, Name: "Updated Task", Description: "Updated desc", + ActionType: "ShellCommand", Content: "echo updated", + Arguments: null, TargetTags: ["linux"], Enabled: true, + TimeoutMinutes: 30, SyncIntervalMinutes: 5, + SuccessCriteria: null, EffectiveSuccessCriteria: "ExitCodeZero", + WorkflowId: 42 ); + + RegisterHttpClient( new StubHandler( HttpStatusCode.OK, JsonSerializer.Serialize( responseDto ) ) ); + + TaskDto existingTask = new( + Id: 7, Name: "My Task", Description: "Test desc", + ActionType: "ShellCommand", Content: "echo hello", + Arguments: null, TargetTags: ["linux"], Enabled: true, + TimeoutMinutes: 30, SyncIntervalMinutes: 5, + SuccessCriteria: null, EffectiveSuccessCriteria: "ExitCodeZero", + WorkflowId: 42 ); + + IRenderedComponent cut = Render( parameters => + parameters.Add( p => p.WorkflowId, 42L ) + .Add( p => p.OnTaskCreated, EventCallback.Factory.Create( this, _ => { } ) ) + .Add( p => p.OnTaskUpdated, EventCallback.Factory.Create( this, t => updatedResult = t ) ) ); + + _ = cut.InvokeAsync( ( ) => cut.Instance.Show( existingTask ) ); + + // Submit the form + cut.Find( "form" ).Submit( ); + + Assert.IsNotNull( updatedResult, "OnTaskUpdated callback should have been invoked." ); + Assert.AreEqual( "Updated Task", updatedResult!.Name, "Should receive the updated task DTO." ); + } + + // ── Helpers ── + + /// Minimal fake that always returns the given response. + private sealed class StubHandler( HttpStatusCode statusCode, string content ) : HttpMessageHandler { + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken ) => + Task.FromResult( new HttpResponseMessage( statusCode ) { + Content = new StringContent( content, System.Text.Encoding.UTF8, "application/json" ), + } ); + } + + /// Captures the HTTP method and URI before returning the configured response. + private sealed class CapturingHandler( + HttpStatusCode statusCode, + string content, + Action onSend ) : HttpMessageHandler { + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken ) { + onSend( request.Method, request.RequestUri?.AbsolutePath ); + return Task.FromResult( new HttpResponseMessage( statusCode ) { + Content = new StringContent( content, System.Text.Encoding.UTF8, "application/json" ), + } ); + } + } + + /// Fake that always returns the same client. + private sealed class FakeHttpClientFactory( HttpClient client ) : IHttpClientFactory { + public HttpClient CreateClient( string name ) => client; + } +} diff --git a/src/Test/Werkr.Tests.Server/Pages/PageDiscoveryTests.cs b/src/Test/Werkr.Tests.Server/Pages/PageDiscoveryTests.cs index 01a7081..0c77d8b 100644 --- a/src/Test/Werkr.Tests.Server/Pages/PageDiscoveryTests.cs +++ b/src/Test/Werkr.Tests.Server/Pages/PageDiscoveryTests.cs @@ -25,7 +25,9 @@ public class PageDiscoveryTests { "/tasks/{Id:long}", "/workflows", "/workflows/create", + "/workflows/new/dag-editor", "/workflows/{Id:long}", + "/workflows/{Id:long}/dag-editor", "/workflows/{WorkflowId:long}/runs", "/workflows/runs/{RunId:guid}", ]; diff --git a/src/Test/Werkr.Tests.Server/packages.lock.json b/src/Test/Werkr.Tests.Server/packages.lock.json index 9707325..d67fcd5 100644 --- a/src/Test/Werkr.Tests.Server/packages.lock.json +++ b/src/Test/Werkr.Tests.Server/packages.lock.json @@ -1,1162 +1,1227 @@ -{ - "version": 2, - "dependencies": { - "net10.0": { - "bunit": { - "type": "Direct", - "requested": "[2.6.2, )", - "resolved": "2.6.2", - "contentHash": "Xx7EFwuSLurxSP/NqzBQWYFsPWPtBOHChDZZwji3bRYhZSnqVfyI4eXmUTMoj3Wb9Xtm1bPoDZTdDQEzgoQDUg==", - "dependencies": { - "AngleSharp": "1.4.0", - "AngleSharp.Css": "1.0.0-beta.157", - "AngleSharp.Diffing": "1.1.1", - "Microsoft.AspNetCore.Components": "10.0.0", - "Microsoft.AspNetCore.Components.Authorization": "10.0.0", - "Microsoft.AspNetCore.Components.Web": "10.0.0", - "Microsoft.AspNetCore.Components.WebAssembly": "10.0.0", - "Microsoft.AspNetCore.Components.WebAssembly.Authentication": "10.0.0", - "Microsoft.Extensions.Caching.Memory": "10.0.0", - "Microsoft.Extensions.Localization.Abstractions": "10.0.0", - "Microsoft.Extensions.Logging": "10.0.0", - "Microsoft.Extensions.Logging.Abstractions": "10.0.0" - } - }, - "Microsoft.EntityFrameworkCore.InMemory": { - "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "5iV0F645m863ccamkNhndJZsFCVVZ/08yTZ8x5uxYp41rbT7z5FFjLm2uM6IP2CBMYGTOBy+QWL/MiDKMc3FPA==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.IdentityModel.JsonWebTokens": { - "type": "Direct", - "requested": "[8.16.0, )", - "resolved": "8.16.0", - "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.16.0" - } - }, - "MSTest": { - "type": "Direct", - "requested": "[4.1.0, )", - "resolved": "4.1.0", - "contentHash": "2bk47yg7HcHRyf6Zf0XgCZicTVTQj4D5lonYTO7lWMxCQB+x66VrQNc2dADBfzthKXfHaA37m8i+VV5h6SbWiA==", - "dependencies": { - "MSTest.TestAdapter": "4.1.0", - "MSTest.TestFramework": "4.1.0", - "Microsoft.NET.Test.Sdk": "18.0.1", - "Microsoft.Testing.Extensions.CodeCoverage": "18.4.1", - "Microsoft.Testing.Extensions.TrxReport": "2.1.0" - } - }, - "AngleSharp": { - "type": "Transitive", - "resolved": "1.4.0", - "contentHash": "6ph8mpaQx0KL0COYRt0kI8MB9gSp1PtKijKMhJU//+aVFgKAJLKDesG/+26JSaVCOrHNgPf12wpfoyRcMYOeXg==" - }, - "AngleSharp.Css": { - "type": "Transitive", - "resolved": "1.0.0-beta.157", - "contentHash": "yptHmuGt2tzNGCeD7Orh6HRs4j7MH3IUhI+FM640OlmSwWRMd1yjKnicVcNG147IdF/tjOvgqtvpTU0pH6wVpA==", - "dependencies": { - "AngleSharp": "[1.0.0, 2.0.0)" - } - }, - "AngleSharp.Diffing": { - "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", - "dependencies": { - "AngleSharp": "1.1.2", - "AngleSharp.Css": "1.0.0-beta.144" - } - }, - "Grpc.Core.Api": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ==" - }, - "Grpc.Net.Common": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==", - "dependencies": { - "Grpc.Core.Api": "2.76.0" - } - }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.23.0", - "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" - }, - "Microsoft.AspNetCore.Components": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "ljLBmYTPkIe/WhOn/y3kpMCJ4A/xg76H/SRm9XkoOBqlM96Ntcbr+WFJqV/0bEqjaUqQYIMITPnJXrVJ242cWQ==", - "dependencies": { - "Microsoft.AspNetCore.Authorization": "10.0.0", - "Microsoft.AspNetCore.Components.Analyzers": "10.0.0" - } - }, - "Microsoft.AspNetCore.Components.Analyzers": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "5j9dQ/U51yLLKo8hKJOU7uTPppPsjLTua0PFzRotaePcmz7M9W7pM69pBXDEgkBfGD1tp2ufH0zHRHkwg1ikDg==" - }, - "Microsoft.AspNetCore.Components.Authorization": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "z/4yuTmCD1A3x++Dr8s2H3yD6nUGglqxvjsPL65BUynJlEO0ZaOaI0TIQ58+OyU8IAw98eprHlezix8UyRMqKg==", - "dependencies": { - "Microsoft.AspNetCore.Authorization": "10.0.0", - "Microsoft.AspNetCore.Components": "10.0.0" - } - }, - "Microsoft.AspNetCore.Components.Forms": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "5kMwH8DqYFfUpyMNv+IzIoDHxR8fHTPKeJeFNAyjY5tMxo3yyM7ezmuYNcVt9QJ+6K2soHQjBnCvFs8yNd3UGQ==", - "dependencies": { - "Microsoft.AspNetCore.Components": "10.0.0", - "Microsoft.Extensions.Validation": "10.0.0" - } - }, - "Microsoft.AspNetCore.Components.Web": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "TAv9+cOrU27YPKuHE7AlG2UBWtTWivvcDHmfdN2yngzQFtas+3p2IBDAF6odNfOJiW3Cs1lHwzqLAoNw5Sc59Q==", - "dependencies": { - "Microsoft.AspNetCore.Components": "10.0.0", - "Microsoft.AspNetCore.Components.Forms": "10.0.0", - "Microsoft.Extensions.DependencyInjection": "10.0.0", - "Microsoft.Extensions.Primitives": "10.0.0", - "Microsoft.JSInterop": "10.0.0" - } - }, - "Microsoft.AspNetCore.Components.WebAssembly": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "ens4WMX0PBTHxRDYpS3i//VktXi+24my01qXI0P91q2rDMHcIKRN0/sdE+4kB1ZwJpHec9vYxDmmnw/JOcXCwQ==", - "dependencies": { - "Microsoft.AspNetCore.Components.Web": "10.0.0", - "Microsoft.Extensions.Configuration.Binder": "10.0.0", - "Microsoft.Extensions.Configuration.Json": "10.0.0", - "Microsoft.Extensions.Logging": "10.0.0", - "Microsoft.JSInterop.WebAssembly": "10.0.0" - } - }, - "Microsoft.AspNetCore.Components.WebAssembly.Authentication": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "u4cqo7IdawVPAPHDBlyiTmEodGom6m/OnqXnioz71ASA2l7pAebLed5vaErtsGT9Ud0Z1Nudthy+3FBk0BmHjQ==", - "dependencies": { - "Microsoft.AspNetCore.Components.Authorization": "10.0.0", - "Microsoft.AspNetCore.Components.Web": "10.0.0" - } - }, - "Microsoft.AspNetCore.Cryptography.Internal": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "63eZHvKVbBnjX3QDbac2S+05SKbaX/2yiQYZI0VR10EegeZGMhT1LPgi0ax76UfLkFOgap7UGpybPbjgzUrlag==" - }, - "Microsoft.AspNetCore.Cryptography.KeyDerivation": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "+Bi0aTctq7Z+j2lpSfQ/CdrA8x7JcsmJDd8KC6IC93rvNokRbi3wJ4H0gJqlNysTGp764D/KGsL93fsqddlLEw==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "10.0.4" - } - }, - "Microsoft.AspNetCore.Metadata": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" - }, - "Microsoft.DiaSymReader": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite.Core": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==", - "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.4", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.Extensions.AmbientMetadata.Application": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "bovnONzrr/JIc+w343i857rJEb7cQH9UzEjbV5n67agWBEYICGQb8xiqYz5+GoFXp6mKEKLwYCQGttMU1p5yXQ==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "uDRooaV6N3WZ0kdlNPMB68/MdGn/in1Fs7Db7DnIm85RBTPy4P321WO+daAImiYpH5dekjNggDqy1N44WaIlMA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Compliance.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "4WkknDbVrHNf+S6fwSt1OAXlGJ/G/QrtJlqx4aNzOLmeT3GRyxpGLZn+Q3UV+RMRAF6FfsijEZBg2ZAW8bTAkg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.FileExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Physical": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "NkvJ8aSr3AG30yabjv7ZWwTG/wq5OElNTlNq39Ok2HSEF3TIwAc1f1xnTJlR/GuoJmEgkfT7WBO9YbSXRk41+g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ==" - }, - "Microsoft.Extensions.DependencyInjection.AutoActivation": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "ksmUG2SFTcXzYdyoLOdeSM/qYLRGN6qbbSzYVkwMK9xsctfR1hYkUayeOpFCMd7L+QSlYX72mK9wxwdgQxyS4g==", - "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg==" - }, - "Microsoft.Extensions.Diagnostics": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "1/hQmONMWxRTKXuN0pQShQN9QsqIRTS1G4fdmKW0O9phuVZjyzIROQD9Fbfwyn2t+yvP8SzjatGAPX4jDRfgHg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Features": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "7to+nkZO+g/GiGQOBzAcrr8HcG8dXETI/hg58fJju0jPO9p/GvNLAis8kMPTBdsjfeTfslBrgFX9Yx1KRnKDww==" - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileProviders.Embedded": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "yM9BTzWdqLDk3v08UcmRJqUAbFiiP/cYFkkoHRphDv+YdMFS7cIc6YL2cbfaFlLfBomBA4Zx8+8lB6lRxvDFxA==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.FileProviders.Physical": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileSystemGlobbing": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileSystemGlobbing": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg==" - }, - "Microsoft.Extensions.Http": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "QRbs+A+WfiGTnV9KFNfWlF+My5euQNZnsvdVMulwRN6C/tEPaF+ZlQfedHoNvFHKLwjQMmqwm4z+TSO9eLvRQw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Http.Diagnostics": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "ybx2QcCWROCnUCbSj/IyHXn1c58brjjHzTTbueKgBl/qHsWk69mu25mjQ3oaMsO1I0+EcS6AhVuhIopL2q3IDw==", - "dependencies": { - "Microsoft.Extensions.Http": "10.0.4", - "Microsoft.Extensions.Telemetry": "10.4.0" - } - }, - "Microsoft.Extensions.Identity.Core": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "n2MIwxPu+5+voKM94XFzTTUPRKFc26fhDH4vYqU1BjpNXQIYA9N3Dg811q7K6gOr9YgukaxZqyimkKKj9qUqEA==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Identity.Stores": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "PY0WbLDw8DiQgj59faevWLCTVo0T6+0Qhed1rghLo909xsZC9mRiBnF8O6U6KpFHbpS8CpPDvh+3JfasWYV4tw==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.4", - "Microsoft.Extensions.Identity.Core": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.Extensions.Localization.Abstractions": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "+gopxHC5+vWE61Qq2i+aFblxjSiS4UyjUG5kpfXodWbeBVwXPeP66s8uAi0LPAwnhS5jInmYJNRFYUMn+sdtKg==" - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "S8+6fCuMOhJZGk8sGFtOy3VsF9mk9x4UOL59GM91REiA/fmCDjunKKIw4RmStG87qyXPfxelDJf2pXIbTuaBdw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Logging.Configuration": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "XPXoOpUnWEh0pV7Vl2DK2wj47y73Krhrve5OkPrvGIWdZ4U2r47WO8hEdv+wKn65Kh4pmDdiWm7Ibo5pZX+vig==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "2pufIFOgNl/yWTOoIC9XgBnO9VxgfAjdRCnVwpE2+ICfcroGnjuEAGzJ5lTdZeAe0HvA31vMBWXtcmGB7TOq3g==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg==" - }, - "Microsoft.Extensions.Resilience": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "41CCbJJPsDWU6NsmKfANHkfT/+KCBlZZqQ1eBoQhhW0xqGCiWmUlMdi2BoaM/GcwKHX5WiQL/IESROmgk0Owfw==", - "dependencies": { - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.4.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4", - "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0", - "Polly.Extensions": "8.4.2", - "Polly.RateLimiting": "8.4.2" - } - }, - "Microsoft.Extensions.ServiceDiscovery.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "HkBb7cdi27tkQiQw1anQFbXe+A3pjRwDKgVbd/DD9fMAO2X9abK0FEyM/tNVXjW3lwOWl2tF+Xij/DqI6i+JTg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Features": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "AbHleTzdpGPjA6RpOjKVHEYx7SoBRnJ2bwAbbPa3aGB7HiVwBmeTJhBGhtIBiuIW0VpKDS8x+bV5iWqpBRIf4w==", - "dependencies": { - "Microsoft.Extensions.AmbientMetadata.Application": "10.4.0", - "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.4.0", - "Microsoft.Extensions.Logging.Configuration": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0" - } - }, - "Microsoft.Extensions.Telemetry.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "3b2uVa4voJfLLg39BPCKQS0ZgnpEZFkKf7YmnMVlM5FQJYBPOuePIQdnEK1/Oxd+w3GscxGYuE7IMOXDwixZtQ==", - "dependencies": { - "Microsoft.Extensions.Compliance.Abstractions": "10.4.0", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Validation": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "viIKcmaf1akdhx77c+dLkODvhL+HyonHNpBsicndoi47zvbSeHJEWPOk188NnFCh+dCUz7Icld1jPMbS5E2fqw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", - "Microsoft.Extensions.Options": "10.0.0" - } - }, - "Microsoft.IdentityModel.Abstractions": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.16.0" - } - }, - "Microsoft.JSInterop": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "Qfs4KtmfMUQ/BtD4jdt753kiAa7frm1QVms8UHiFArOokoXBkBOOKE2N5APdt3BlAdTSFJNiuDFfgDzVq/3EZg==" - }, - "Microsoft.JSInterop.WebAssembly": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "j0lrmG+7X4OZh75IzOX00nkwAEjXSgt2Ul6kjwmVkAOBzd8JC1Q/yJ/fC4jWcIXe4h22881Qq1o46p/TnSNaRQ==", - "dependencies": { - "Microsoft.JSInterop": "10.0.0" - } - }, - "Microsoft.NET.Test.Sdk": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", - "dependencies": { - "Microsoft.CodeCoverage": "18.0.1", - "Microsoft.TestPlatform.TestHost": "18.0.1" - } - }, - "Microsoft.Testing.Extensions.CodeCoverage": { - "type": "Transitive", - "resolved": "18.4.1", - "contentHash": "l1VZM9dg9s76L5D288ipAT4HRYDJ6Vxh8wX20gfS9VnpueedRfN4/aGNn4oA1g6pwq2WSM3Ci7IoSSGPiqu+WQ==", - "dependencies": { - "Microsoft.DiaSymReader": "2.0.0", - "Microsoft.Extensions.DependencyModel": "8.0.2", - "Microsoft.Testing.Platform": "2.0.2" - } - }, - "Microsoft.Testing.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "5TwgTx2u7k9Al/xbZ18QXq4Hdy2xewkVTI6K3sk+jY2ykqUkIKNuj7rFu3GOV5KnEUkevhw6eZcyZs77STHJIA==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.TrxReport": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "cXmP225WcMLLOSrW8xekaNhfzdBwXX3cbXbE5qSzmLbK0KZe3z8rAObKj70FWiPPPzm2W22x0ZW93gsmAfK6Mg==", - "dependencies": { - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.TrxReport.Abstractions": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "D8xmIJYQFJ6D49Rx5/vPrkZZxb338Jkew+eSqZLBfBiWKw4QZKy3i1BOXiLfz0lOmaNErwDz/YWRojCdNl+B9Q==", - "dependencies": { - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.VSTestBridge": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "bNRIEA2YoGr+Y+7LHdA7i1U80+7BAdf4K4Qh4Kx6eKkoBK/NV7QpoMg+GWPP0/eqAFzuUmUOIPVZ87Oo0Vyxmw==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", - "Microsoft.Testing.Extensions.Telemetry": "2.1.0", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Platform": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "aHkjNTGIA+Zbdw6RJgSFrbDrCjO0CgqpElqYcvkRSeUhBv2bKarnvU3ep786U7UqrPlArT/B7VmImRibJD0Zrg==" - }, - "Microsoft.Testing.Platform.MSBuild": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "UpfPebXQtHGrWz21+YLHmJSm+5zsuPE9U9pfdCtoB+67g75fDmWlNgpkH2ZmdVhSwkjNIed9Icg8Iu63z2ei5Q==", - "dependencies": { - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", - "Newtonsoft.Json": "13.0.3" - } - }, - "Microsoft.Win32.SystemEvents": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A==" - }, - "MSTest.Analyzers": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "4ElL/aqomiUInr090VN4udqz46AuszXLrifHkLrgj0zb7na8eAoyUQt3BwDLTcGd1bSkmk3SfD02rZtKU+ZiqQ==" - }, - "MSTest.TestAdapter": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "bRW1Hftwq0XbcVExcAbj4YAfSZDRAziL0mygDkPBvaUe2nSsWFQIatze5lHVjPFJMvSFgWnItku4pguIy5FowQ==", - "dependencies": { - "MSTest.TestFramework": "4.1.0", - "Microsoft.Testing.Extensions.VSTestBridge": "2.1.0", - "Microsoft.Testing.Platform.MSBuild": "2.1.0" - } - }, - "MSTest.TestFramework": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "BzpvsK+CRbk6khwY62h+7HfYzIxtJXyPv9tOI9T90cy5CVy+WI1JkN4ZaNL4Dobqb6dywSwabLTIbPZKpdrr+A==", - "dependencies": { - "MSTest.Analyzers": "4.1.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "Npgsql": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "10.0.0" - } - }, - "OpenTelemetry": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==", - "dependencies": { - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", - "Microsoft.Extensions.Logging.Configuration": "10.0.0", - "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0" - } - }, - "OpenTelemetry.Api": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w==" - }, - "OpenTelemetry.Api.ProviderBuilderExtensions": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", - "OpenTelemetry.Api": "1.15.0" - } - }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, - "Polly.Extensions": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Polly.Core": "8.4.2" - } - }, - "Polly.RateLimiting": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", - "dependencies": { - "Polly.Core": "8.4.2", - "System.Threading.RateLimiting": "8.0.0" - } - }, - "Serilog": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" - }, - "Serilog.Extensions.Hosting": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", - "Microsoft.Extensions.Logging.Abstractions": "10.0.0", - "Serilog": "4.3.0", - "Serilog.Extensions.Logging": "10.0.0" - } - }, - "Serilog.Extensions.Logging": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", - "dependencies": { - "Microsoft.Extensions.Logging": "10.0.0", - "Serilog": "4.2.0" - } - }, - "Serilog.Formatting.Compact": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "Serilog.Settings.Configuration": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "10.0.0", - "Microsoft.Extensions.DependencyModel": "10.0.0", - "Serilog": "4.3.0" - } - }, - "Serilog.Sinks.Debug": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "SQLitePCLRaw.bundle_e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", - "dependencies": { - "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", - "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" - } - }, - "SQLitePCLRaw.core": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" - }, - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" - }, - "SQLitePCLRaw.provider.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", - "dependencies": { - "SQLitePCLRaw.core": "2.1.11" - } - }, - "System.Drawing.Common": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", - "dependencies": { - "Microsoft.Win32.SystemEvents": "6.0.0" - } - }, - "System.Threading.RateLimiting": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" - }, - "werkr.common": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.34.0, )", - "Microsoft.AspNetCore.Authorization": "[10.0.4, )", - "Microsoft.Extensions.Configuration.Json": "[10.0.4, )", - "Microsoft.IdentityModel.Tokens": "[8.16.0, )", - "Werkr.Common.Configuration": "[1.0.0, )" - } - }, - "werkr.common.configuration": { - "type": "Project" - }, - "werkr.core": { - "type": "Project", - "dependencies": { - "Grpc.Net.Client": "[2.76.0, )", - "Microsoft.Extensions.Hosting.Abstractions": "[10.0.4, )", - "System.Security.Cryptography.ProtectedData": "[10.0.4, )", - "Werkr.Common": "[1.0.0, )", - "Werkr.Data": "[1.0.0, )" - } - }, - "werkr.data": { - "type": "Project", - "dependencies": { - "EFCore.NamingConventions": "[10.0.1, )", - "Microsoft.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )", - "Werkr.Common": "[1.0.0, )" - } - }, - "werkr.data.identity": { - "type": "Project", - "dependencies": { - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.AspNetCore.Identity.UI": "[10.0.4, )", - "Werkr.Common": "[1.0.0, )", - "Werkr.Data": "[1.0.0, )" - } - }, - "werkr.server": { - "type": "Project", - "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "[8.16.0, )", - "QRCoder": "[1.7.0, )", - "Serilog.AspNetCore": "[10.0.0, )", - "Serilog.Sinks.Console": "[6.1.1, )", - "Serilog.Sinks.File": "[7.0.0, )", - "Serilog.Sinks.OpenTelemetry": "[4.2.0, )", - "Werkr.Common": "[1.0.0, )", - "Werkr.Data.Identity": "[1.0.0, )", - "Werkr.ServiceDefaults": "[1.0.0, )" - } - }, - "werkr.servicedefaults": { - "type": "Project", - "dependencies": { - "Microsoft.Extensions.Http.Resilience": "[10.4.0, )", - "Microsoft.Extensions.ServiceDiscovery": "[10.4.0, )", - "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.0, )", - "OpenTelemetry.Extensions.Hosting": "[1.15.0, )" - } - }, - "EFCore.NamingConventions": { - "type": "CentralTransitive", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" - } - }, - "Google.Protobuf": { - "type": "CentralTransitive", - "requested": "[3.34.0, )", - "resolved": "3.34.0", - "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" - }, - "Grpc.Net.Client": { - "type": "CentralTransitive", - "requested": "[2.76.0, )", - "resolved": "2.76.0", - "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==", - "dependencies": { - "Grpc.Net.Common": "2.76.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Microsoft.AspNetCore.Authorization": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==", - "dependencies": { - "Microsoft.AspNetCore.Metadata": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "xcuRo8Ubxwf3xay2z7mD3b3TmEnvMhSLzvfx5cqmk3V10oxTDqgIyZ8C5qav+iAc27In/TXteSJ6kS2iZpah7w==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.Identity.Stores": "10.0.4" - } - }, - "Microsoft.AspNetCore.Identity.UI": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "ZJlXBj2KgSKeR7WWl/1XIOiJLgvxKL7v0/YEkfWoCQQdVqLPz2sEsg3EcvqiQ6w2oR/gjdqDsCa2zkKgHYMnNg==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Embedded": "10.0.4", - "Microsoft.Extensions.Identity.Stores": "10.0.4" - } - }, - "Microsoft.Data.Sqlite.Core": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==", - "dependencies": { - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.EntityFrameworkCore": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "+5mQrqlBhqNUaPyDmFSNM/qiWStvE9LMxZW1MRF0NhEbO781xNeKryXNR9gGDJ0PmYFDAVoMT9ffX+I15LPFTw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Http.Resilience": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "HbkUsPUC7vLy2TaDbdA9aooW64n9yX4sUppRuiJ1cOzzU1FUW+MVEotm6kYVq6AuUI9xwFSBhRFzA03blmk3VA==", - "dependencies": { - "Microsoft.Extensions.Http.Diagnostics": "10.4.0", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Resilience": "10.4.0" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.ServiceDiscovery": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "RznZAH6L4RNvroECT5JpqfFQJjHTn+8N7+ThSgYutbshkuymFeL/uBIZt1CM8LOdpPPhn4//a5fLUah9/k7ayQ==", - "dependencies": { - "Microsoft.Extensions.Http": "10.0.4", - "Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.4.0" - } - }, - "Microsoft.IdentityModel.Tokens": { - "type": "CentralTransitive", - "requested": "[8.16.0, )", - "resolved": "8.16.0", - "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "10.0.0", - "Microsoft.IdentityModel.Logging": "8.16.0" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)", - "Npgsql": "10.0.0" - } - }, - "OpenTelemetry.Exporter.OpenTelemetryProtocol": { - "type": "CentralTransitive", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==", - "dependencies": { - "OpenTelemetry": "1.15.0" - } - }, - "OpenTelemetry.Extensions.Hosting": { - "type": "CentralTransitive", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==", - "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", - "OpenTelemetry": "1.15.0" - } - }, - "QRCoder": { - "type": "CentralTransitive", - "requested": "[1.7.0, )", - "resolved": "1.7.0", - "contentHash": "6R3hQkayihGIDjp3F1nLRDBWG+nqahGyOY2+fH4Rll16Vad67oaUUfHkOiMWKiJFnGh+PIGDfUos+0R9m54O1g==", - "dependencies": { - "System.Drawing.Common": "6.0.0" - } - }, - "Serilog.AspNetCore": { - "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", - "dependencies": { - "Serilog": "4.3.0", - "Serilog.Extensions.Hosting": "10.0.0", - "Serilog.Formatting.Compact": "3.0.0", - "Serilog.Settings.Configuration": "10.0.0", - "Serilog.Sinks.Console": "6.1.1", - "Serilog.Sinks.Debug": "3.0.0", - "Serilog.Sinks.File": "7.0.0" - } - }, - "Serilog.Sinks.Console": { - "type": "CentralTransitive", - "requested": "[6.1.1, )", - "resolved": "6.1.1", - "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "Serilog.Sinks.File": { - "type": "CentralTransitive", - "requested": "[7.0.0, )", - "resolved": "7.0.0", - "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", - "dependencies": { - "Serilog": "4.2.0" - } - }, - "Serilog.Sinks.OpenTelemetry": { - "type": "CentralTransitive", - "requested": "[4.2.0, )", - "resolved": "4.2.0", - "contentHash": "PzMCyE5G19tjr5IZEi5qg+4UU5QrxBEoBEMu/hhYybTrGKXqUDiSGWKZNUDBgelaVKqLADlsmlJVyKce5SyPrg==", - "dependencies": { - "Google.Protobuf": "3.30.1", - "Grpc.Net.Client": "2.70.0", - "Serilog": "4.2.0" - } - }, - "System.Security.Cryptography.ProtectedData": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g==" - } - } - } +{ + "version": 2, + "dependencies": { + "net10.0": { + "bunit": { + "type": "Direct", + "requested": "[2.6.2, )", + "resolved": "2.6.2", + "contentHash": "Xx7EFwuSLurxSP/NqzBQWYFsPWPtBOHChDZZwji3bRYhZSnqVfyI4eXmUTMoj3Wb9Xtm1bPoDZTdDQEzgoQDUg==", + "dependencies": { + "AngleSharp": "1.4.0", + "AngleSharp.Css": "1.0.0-beta.157", + "AngleSharp.Diffing": "1.1.1", + "Microsoft.AspNetCore.Components": "10.0.0", + "Microsoft.AspNetCore.Components.Authorization": "10.0.0", + "Microsoft.AspNetCore.Components.Web": "10.0.0", + "Microsoft.AspNetCore.Components.WebAssembly": "10.0.0", + "Microsoft.AspNetCore.Components.WebAssembly.Authentication": "10.0.0", + "Microsoft.Extensions.Caching.Memory": "10.0.0", + "Microsoft.Extensions.Localization.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "Microsoft.EntityFrameworkCore.InMemory": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "zz4THzlDOrJ7fKU2YUOzQFs2LJ9DgOSr5xFXcDdoD59el73MTQLtQQOAJxQ94F4XDegyL9+2sePSQGdcU25ZxQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Direct", + "requested": "[8.16.0, )", + "resolved": "8.16.0", + "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.16.0" + } + }, + "MSTest": { + "type": "Direct", + "requested": "[4.1.0, )", + "resolved": "4.1.0", + "contentHash": "2bk47yg7HcHRyf6Zf0XgCZicTVTQj4D5lonYTO7lWMxCQB+x66VrQNc2dADBfzthKXfHaA37m8i+VV5h6SbWiA==", + "dependencies": { + "MSTest.TestAdapter": "4.1.0", + "MSTest.TestFramework": "4.1.0", + "Microsoft.NET.Test.Sdk": "18.0.1", + "Microsoft.Testing.Extensions.CodeCoverage": "18.4.1", + "Microsoft.Testing.Extensions.TrxReport": "2.1.0" + } + }, + "AngleSharp": { + "type": "Transitive", + "resolved": "1.4.0", + "contentHash": "6ph8mpaQx0KL0COYRt0kI8MB9gSp1PtKijKMhJU//+aVFgKAJLKDesG/+26JSaVCOrHNgPf12wpfoyRcMYOeXg==" + }, + "AngleSharp.Css": { + "type": "Transitive", + "resolved": "1.0.0-beta.157", + "contentHash": "yptHmuGt2tzNGCeD7Orh6HRs4j7MH3IUhI+FM640OlmSwWRMd1yjKnicVcNG147IdF/tjOvgqtvpTU0pH6wVpA==", + "dependencies": { + "AngleSharp": "[1.0.0, 2.0.0)" + } + }, + "AngleSharp.Diffing": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "dependencies": { + "AngleSharp": "1.1.2", + "AngleSharp.Css": "1.0.0-beta.144" + } + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ==" + }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==", + "dependencies": { + "Grpc.Core.Api": "2.76.0" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "ljLBmYTPkIe/WhOn/y3kpMCJ4A/xg76H/SRm9XkoOBqlM96Ntcbr+WFJqV/0bEqjaUqQYIMITPnJXrVJ242cWQ==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "10.0.0", + "Microsoft.AspNetCore.Components.Analyzers": "10.0.0" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "5j9dQ/U51yLLKo8hKJOU7uTPppPsjLTua0PFzRotaePcmz7M9W7pM69pBXDEgkBfGD1tp2ufH0zHRHkwg1ikDg==" + }, + "Microsoft.AspNetCore.Components.Authorization": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "z/4yuTmCD1A3x++Dr8s2H3yD6nUGglqxvjsPL65BUynJlEO0ZaOaI0TIQ58+OyU8IAw98eprHlezix8UyRMqKg==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "10.0.0", + "Microsoft.AspNetCore.Components": "10.0.0" + } + }, + "Microsoft.AspNetCore.Components.Forms": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "5kMwH8DqYFfUpyMNv+IzIoDHxR8fHTPKeJeFNAyjY5tMxo3yyM7ezmuYNcVt9QJ+6K2soHQjBnCvFs8yNd3UGQ==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.0", + "Microsoft.Extensions.Validation": "10.0.0" + } + }, + "Microsoft.AspNetCore.Components.Web": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "TAv9+cOrU27YPKuHE7AlG2UBWtTWivvcDHmfdN2yngzQFtas+3p2IBDAF6odNfOJiW3Cs1lHwzqLAoNw5Sc59Q==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.0", + "Microsoft.AspNetCore.Components.Forms": "10.0.0", + "Microsoft.Extensions.DependencyInjection": "10.0.0", + "Microsoft.Extensions.Primitives": "10.0.0", + "Microsoft.JSInterop": "10.0.0" + } + }, + "Microsoft.AspNetCore.Components.WebAssembly": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "ens4WMX0PBTHxRDYpS3i//VktXi+24my01qXI0P91q2rDMHcIKRN0/sdE+4kB1ZwJpHec9vYxDmmnw/JOcXCwQ==", + "dependencies": { + "Microsoft.AspNetCore.Components.Web": "10.0.0", + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.Configuration.Json": "10.0.0", + "Microsoft.Extensions.Logging": "10.0.0", + "Microsoft.JSInterop.WebAssembly": "10.0.0" + } + }, + "Microsoft.AspNetCore.Components.WebAssembly.Authentication": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "u4cqo7IdawVPAPHDBlyiTmEodGom6m/OnqXnioz71ASA2l7pAebLed5vaErtsGT9Ud0Z1Nudthy+3FBk0BmHjQ==", + "dependencies": { + "Microsoft.AspNetCore.Components.Authorization": "10.0.0", + "Microsoft.AspNetCore.Components.Web": "10.0.0" + } + }, + "Microsoft.AspNetCore.Connections.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ruD0Lb3Dy8Fn1KJlyILl5WxOwPdhUQzEAYpDEEe9MqMRusC6OqU7PtmwTXP4rqEKOw8yyFq8MjCnOLb1+24T6Q==", + "dependencies": { + "Microsoft.Extensions.Features": "10.0.5" + } + }, + "Microsoft.AspNetCore.Cryptography.Internal": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "kFUpNRYySfqNLuQKGMKZ2mK8b86R1zizlc9QB6R/Ess0rSkrA8pRNCMSFm+DqUnNfm5G3FWjsYIJOKYyhkHeig==" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "NkWUZYkL6wavYY2wMZgnODzsyTOZhcRxP/DJvZlBbWEJViukdyuIqtdTzltODyjsc3MjEvxmbPDDk2KgGv6tMA==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "10.0.5" + } + }, + "Microsoft.AspNetCore.Http.Connections.Client": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "z4Iny9wOi2D3h/YVPELyb2aMakFQIwiAeCHl1T8giCoUHgql+HtvKKldGxQC6/g4niuLBipi7O6zQT34O9/Rcw==", + "dependencies": { + "Microsoft.AspNetCore.Http.Connections.Common": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.AspNetCore.Http.Connections.Common": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "4zUdCDERnuMPhiKnxeu6aWUlXQFzfeswcCy8WEC+YkJkrHi8ZRHESBT50jR5HBCu4cyBJeDbuDjfh2wHTIczUw==", + "dependencies": { + "Microsoft.AspNetCore.Connections.Abstractions": "10.0.5" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A==" + }, + "Microsoft.AspNetCore.SignalR.Client.Core": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "A2+sApRgxWSxzD0Q5eRqElPPzqIqOljYCzt/mtC/KIjlTNLvVWoQn2amNnX3o7BrbaSdI1yeFcyKVal7zVgcSw==", + "dependencies": { + "Microsoft.AspNetCore.SignalR.Common": "10.0.5", + "Microsoft.AspNetCore.SignalR.Protocols.Json": "10.0.5", + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.AspNetCore.SignalR.Common": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "+aUsWJhnJlGzaSs8lipFVH6XuKYJwnJWdK200KO2NvbJFRQSauYynSLQhzhf3mpGGFoQ1U5Z7MVxJiC1ANCOYA==", + "dependencies": { + "Microsoft.AspNetCore.Connections.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.AspNetCore.SignalR.Protocols.Json": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "bFLLtvuZ0ATBhuK5ZqbJW+pG9XWu+u+yh+sR90ra57uMgR2WEQtMAnQPK94/pQOzbQAzm1vNr4bP/JqMs+OftQ==", + "dependencies": { + "Microsoft.AspNetCore.SignalR.Common": "10.0.5" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.AmbientMetadata.Application": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "bovnONzrr/JIc+w343i857rJEb7cQH9UzEjbV5n67agWBEYICGQb8xiqYz5+GoFXp6mKEKLwYCQGttMU1p5yXQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.4", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "4WkknDbVrHNf+S6fwSt1OAXlGJ/G/QrtJlqx4aNzOLmeT3GRyxpGLZn+Q3UV+RMRAF6FfsijEZBg2ZAW8bTAkg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" + }, + "Microsoft.Extensions.DependencyInjection.AutoActivation": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "ksmUG2SFTcXzYdyoLOdeSM/qYLRGN6qbbSzYVkwMK9xsctfR1hYkUayeOpFCMd7L+QSlYX72mK9wxwdgQxyS4g==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.4" + } + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "1/hQmONMWxRTKXuN0pQShQN9QsqIRTS1G4fdmKW0O9phuVZjyzIROQD9Fbfwyn2t+yvP8SzjatGAPX4jDRfgHg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + } + }, + "Microsoft.Extensions.Features": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "yvfER/YMzYyQHbnLW+zGM3CNqG+4RpM6SIJzaHei0v+xPxMhigckqY4kvucmUBeOfs49HCZu+WxrgfXSbsDkgg==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileProviders.Embedded": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "DQzkbLFNfwmjxErAnWyZTTyBd4cMo6vmGteM4Ayedhk5Pccm2VuKoeKcOZjJG1T+dYK6lMCNk2L7Ftl7dLhgqg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" + }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "QRbs+A+WfiGTnV9KFNfWlF+My5euQNZnsvdVMulwRN6C/tEPaF+ZlQfedHoNvFHKLwjQMmqwm4z+TSO9eLvRQw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.Diagnostics": "10.0.4", + "Microsoft.Extensions.Logging": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.Extensions.Http.Diagnostics": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "ybx2QcCWROCnUCbSj/IyHXn1c58brjjHzTTbueKgBl/qHsWk69mu25mjQ3oaMsO1I0+EcS6AhVuhIopL2q3IDw==", + "dependencies": { + "Microsoft.Extensions.Http": "10.0.4", + "Microsoft.Extensions.Telemetry": "10.4.0" + } + }, + "Microsoft.Extensions.Identity.Core": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ew2Xob+HFvJvM7BelIrUIbGeVMO2q1A6gsZEsI8N/v0ddSv7qbvvY68mH16XzvlsqydqD3ct5ioQHsiEUDSNkg==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Identity.Stores": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "St8g+4xGLUhfSzTlHSLtCv7kh/tppvFab5x0kFIOsWryf1ffK2Ux+JIg01v5Yf27g2iQLCFEmW5hG5DDZL1HLA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.Identity.Core": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "+gopxHC5+vWE61Qq2i+aFblxjSiS4UyjUG5kpfXodWbeBVwXPeP66s8uAi0LPAwnhS5jInmYJNRFYUMn+sdtKg==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "XPXoOpUnWEh0pV7Vl2DK2wj47y73Krhrve5OkPrvGIWdZ4U2r47WO8hEdv+wKn65Kh4pmDdiWm7Ibo5pZX+vig==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", + "Microsoft.Extensions.Configuration.Binder": "10.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.Logging": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "2pufIFOgNl/yWTOoIC9XgBnO9VxgfAjdRCnVwpE2+ICfcroGnjuEAGzJ5lTdZeAe0HvA31vMBWXtcmGB7TOq3g==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.Extensions.Resilience": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "41CCbJJPsDWU6NsmKfANHkfT/+KCBlZZqQ1eBoQhhW0xqGCiWmUlMdi2BoaM/GcwKHX5WiQL/IESROmgk0Owfw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics": "10.0.4", + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.4.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4", + "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0", + "Polly.Extensions": "8.4.2", + "Polly.RateLimiting": "8.4.2" + } + }, + "Microsoft.Extensions.ServiceDiscovery.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "HkBb7cdi27tkQiQw1anQFbXe+A3pjRwDKgVbd/DD9fMAO2X9abK0FEyM/tNVXjW3lwOWl2tF+Xij/DqI6i+JTg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", + "Microsoft.Extensions.Configuration.Binder": "10.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.Features": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4", + "Microsoft.Extensions.Primitives": "10.0.4" + } + }, + "Microsoft.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "AbHleTzdpGPjA6RpOjKVHEYx7SoBRnJ2bwAbbPa3aGB7HiVwBmeTJhBGhtIBiuIW0VpKDS8x+bV5iWqpBRIf4w==", + "dependencies": { + "Microsoft.Extensions.AmbientMetadata.Application": "10.4.0", + "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.4.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0" + } + }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "3b2uVa4voJfLLg39BPCKQS0ZgnpEZFkKf7YmnMVlM5FQJYBPOuePIQdnEK1/Oxd+w3GscxGYuE7IMOXDwixZtQ==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "10.4.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.Extensions.Validation": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "viIKcmaf1akdhx77c+dLkODvhL+HyonHNpBsicndoi47zvbSeHJEWPOk188NnFCh+dCUz7Icld1jPMbS5E2fqw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.16.0" + } + }, + "Microsoft.JSInterop": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "Qfs4KtmfMUQ/BtD4jdt753kiAa7frm1QVms8UHiFArOokoXBkBOOKE2N5APdt3BlAdTSFJNiuDFfgDzVq/3EZg==" + }, + "Microsoft.JSInterop.WebAssembly": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "j0lrmG+7X4OZh75IzOX00nkwAEjXSgt2Ul6kjwmVkAOBzd8JC1Q/yJ/fC4jWcIXe4h22881Qq1o46p/TnSNaRQ==", + "dependencies": { + "Microsoft.JSInterop": "10.0.0" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", + "dependencies": { + "Microsoft.CodeCoverage": "18.0.1", + "Microsoft.TestPlatform.TestHost": "18.0.1" + } + }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Transitive", + "resolved": "18.4.1", + "contentHash": "l1VZM9dg9s76L5D288ipAT4HRYDJ6Vxh8wX20gfS9VnpueedRfN4/aGNn4oA1g6pwq2WSM3Ci7IoSSGPiqu+WQ==", + "dependencies": { + "Microsoft.DiaSymReader": "2.0.0", + "Microsoft.Extensions.DependencyModel": "8.0.2", + "Microsoft.Testing.Platform": "2.0.2" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "5TwgTx2u7k9Al/xbZ18QXq4Hdy2xewkVTI6K3sk+jY2ykqUkIKNuj7rFu3GOV5KnEUkevhw6eZcyZs77STHJIA==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.TrxReport": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "cXmP225WcMLLOSrW8xekaNhfzdBwXX3cbXbE5qSzmLbK0KZe3z8rAObKj70FWiPPPzm2W22x0ZW93gsmAfK6Mg==", + "dependencies": { + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "D8xmIJYQFJ6D49Rx5/vPrkZZxb338Jkew+eSqZLBfBiWKw4QZKy3i1BOXiLfz0lOmaNErwDz/YWRojCdNl+B9Q==", + "dependencies": { + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.VSTestBridge": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "bNRIEA2YoGr+Y+7LHdA7i1U80+7BAdf4K4Qh4Kx6eKkoBK/NV7QpoMg+GWPP0/eqAFzuUmUOIPVZ87Oo0Vyxmw==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Microsoft.Testing.Extensions.Telemetry": "2.1.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "aHkjNTGIA+Zbdw6RJgSFrbDrCjO0CgqpElqYcvkRSeUhBv2bKarnvU3ep786U7UqrPlArT/B7VmImRibJD0Zrg==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "UpfPebXQtHGrWz21+YLHmJSm+5zsuPE9U9pfdCtoB+67g75fDmWlNgpkH2ZmdVhSwkjNIed9Icg8Iu63z2ei5Q==", + "dependencies": { + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.Win32.SystemEvents": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A==" + }, + "MSTest.Analyzers": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "4ElL/aqomiUInr090VN4udqz46AuszXLrifHkLrgj0zb7na8eAoyUQt3BwDLTcGd1bSkmk3SfD02rZtKU+ZiqQ==" + }, + "MSTest.TestAdapter": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "bRW1Hftwq0XbcVExcAbj4YAfSZDRAziL0mygDkPBvaUe2nSsWFQIatze5lHVjPFJMvSFgWnItku4pguIy5FowQ==", + "dependencies": { + "MSTest.TestFramework": "4.1.0", + "Microsoft.Testing.Extensions.VSTestBridge": "2.1.0", + "Microsoft.Testing.Platform.MSBuild": "2.1.0" + } + }, + "MSTest.TestFramework": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "BzpvsK+CRbk6khwY62h+7HfYzIxtJXyPv9tOI9T90cy5CVy+WI1JkN4ZaNL4Dobqb6dywSwabLTIbPZKpdrr+A==", + "dependencies": { + "MSTest.Analyzers": "4.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.15.0" + } + }, + "Polly.Core": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" + }, + "Polly.Extensions": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Polly.Core": "8.4.2" + } + }, + "Polly.RateLimiting": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", + "dependencies": { + "Polly.Core": "8.4.2", + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "System.Drawing.Common": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", + "dependencies": { + "Microsoft.Win32.SystemEvents": "6.0.0" + } + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "werkr.common": { + "type": "Project", + "dependencies": { + "Google.Protobuf": "[3.34.0, )", + "Microsoft.AspNetCore.Authorization": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Json": "[10.0.5, )", + "Microsoft.IdentityModel.Tokens": "[8.16.0, )", + "Werkr.Common.Configuration": "[1.0.0, )" + } + }, + "werkr.common.configuration": { + "type": "Project" + }, + "werkr.core": { + "type": "Project", + "dependencies": { + "Grpc.Net.Client": "[2.76.0, )", + "Microsoft.Extensions.Hosting.Abstractions": "[10.0.5, )", + "System.Security.Cryptography.ProtectedData": "[10.0.5, )", + "Werkr.Common": "[1.0.0, )", + "Werkr.Data": "[1.0.0, )" + } + }, + "werkr.data": { + "type": "Project", + "dependencies": { + "EFCore.NamingConventions": "[10.0.1, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "Werkr.Common": "[1.0.0, )" + } + }, + "werkr.data.identity": { + "type": "Project", + "dependencies": { + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.AspNetCore.Identity.UI": "[10.0.5, )", + "Werkr.Common": "[1.0.0, )", + "Werkr.Data": "[1.0.0, )" + } + }, + "werkr.server": { + "type": "Project", + "dependencies": { + "Microsoft.AspNetCore.SignalR.Client": "[10.0.5, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.16.0, )", + "QRCoder": "[1.7.0, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "Serilog.Sinks.File": "[7.0.0, )", + "Serilog.Sinks.OpenTelemetry": "[4.2.0, )", + "Werkr.Common": "[1.0.0, )", + "Werkr.Data.Identity": "[1.0.0, )", + "Werkr.ServiceDefaults": "[1.0.0, )" + } + }, + "werkr.servicedefaults": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Http.Resilience": "[10.4.0, )", + "Microsoft.Extensions.ServiceDiscovery": "[10.4.0, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.0, )", + "OpenTelemetry.Extensions.Hosting": "[1.15.0, )" + } + }, + "EFCore.NamingConventions": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + } + }, + "Google.Protobuf": { + "type": "CentralTransitive", + "requested": "[3.34.0, )", + "resolved": "3.34.0", + "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" + }, + "Grpc.Net.Client": { + "type": "CentralTransitive", + "requested": "[2.76.0, )", + "resolved": "2.76.0", + "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==", + "dependencies": { + "Grpc.Net.Common": "2.76.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "oo1uauTwgcnhYgituZ2nI3wJ8XN9z76ggu2zkOJu1BCfOOsqr+g08Kr4MOiMuXEhwySkpIV+MVoC25hC1288NA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Identity.Stores": "10.0.5" + } + }, + "Microsoft.AspNetCore.Identity.UI": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "qzYhpJ4Uxng18hmuKqwqydZaPzItrv9WOwNULJ2ka952TZKlOQkERTSkVO8G/19WiRtoznZatrcRyOvppYRGFA==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Embedded": "10.0.5", + "Microsoft.Extensions.Identity.Stores": "10.0.5" + } + }, + "Microsoft.AspNetCore.SignalR.Client": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "2v+Qi63UGm8hu9RAAfWeim3H9ovnxDiPx2MyLQI8nsWrgP+76Z7oiKj+kpVCR1N5QU9bF94RV0lZ/7OJH1LXUA==", + "dependencies": { + "Microsoft.AspNetCore.Http.Connections.Client": "10.0.5", + "Microsoft.AspNetCore.SignalR.Client.Core": "10.0.5" + } + }, + "Microsoft.Data.Sqlite.Core": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Http.Resilience": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "HbkUsPUC7vLy2TaDbdA9aooW64n9yX4sUppRuiJ1cOzzU1FUW+MVEotm6kYVq6AuUI9xwFSBhRFzA03blmk3VA==", + "dependencies": { + "Microsoft.Extensions.Http.Diagnostics": "10.4.0", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Resilience": "10.4.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.ServiceDiscovery": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "RznZAH6L4RNvroECT5JpqfFQJjHTn+8N7+ThSgYutbshkuymFeL/uBIZt1CM8LOdpPPhn4//a5fLUah9/k7ayQ==", + "dependencies": { + "Microsoft.Extensions.Http": "10.0.4", + "Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.4.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "CentralTransitive", + "requested": "[8.16.0, )", + "resolved": "8.16.0", + "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.IdentityModel.Logging": "8.16.0" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==", + "dependencies": { + "OpenTelemetry": "1.15.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.15.0" + } + }, + "QRCoder": { + "type": "CentralTransitive", + "requested": "[1.7.0, )", + "resolved": "1.7.0", + "contentHash": "6R3hQkayihGIDjp3F1nLRDBWG+nqahGyOY2+fH4Rll16Vad67oaUUfHkOiMWKiJFnGh+PIGDfUos+0R9m54O1g==", + "dependencies": { + "System.Drawing.Common": "6.0.0" + } + }, + "Serilog.AspNetCore": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "Serilog.Sinks.OpenTelemetry": { + "type": "CentralTransitive", + "requested": "[4.2.0, )", + "resolved": "4.2.0", + "contentHash": "PzMCyE5G19tjr5IZEi5qg+4UU5QrxBEoBEMu/hhYybTrGKXqUDiSGWKZNUDBgelaVKqLADlsmlJVyKce5SyPrg==", + "dependencies": { + "Google.Protobuf": "3.30.1", + "Grpc.Net.Client": "2.70.0", + "Serilog": "4.2.0" + } + }, + "System.Security.Cryptography.ProtectedData": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg==" + } + } + } } \ No newline at end of file diff --git a/src/Test/Werkr.Tests/packages.lock.json b/src/Test/Werkr.Tests/packages.lock.json index 5e7b883..00a8649 100644 --- a/src/Test/Werkr.Tests/packages.lock.json +++ b/src/Test/Werkr.Tests/packages.lock.json @@ -1,1268 +1,1268 @@ -{ - "version": 2, - "dependencies": { - "net10.0": { - "Microsoft.AspNetCore.Mvc.Testing": { - "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "ZkgmjUX4Q1ffkpomVKFZWvBXiJsLWvEETme+QDJme7NST7dc3DaVEdgL6xs5QfglDke9tX5kLrs253ZFMMGB3g==", - "dependencies": { - "Microsoft.AspNetCore.TestHost": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Hosting": "10.0.4" - } - }, - "Microsoft.IdentityModel.JsonWebTokens": { - "type": "Direct", - "requested": "[8.16.0, )", - "resolved": "8.16.0", - "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.16.0" - } - }, - "MSTest": { - "type": "Direct", - "requested": "[4.1.0, )", - "resolved": "4.1.0", - "contentHash": "2bk47yg7HcHRyf6Zf0XgCZicTVTQj4D5lonYTO7lWMxCQB+x66VrQNc2dADBfzthKXfHaA37m8i+VV5h6SbWiA==", - "dependencies": { - "MSTest.TestAdapter": "4.1.0", - "MSTest.TestFramework": "4.1.0", - "Microsoft.NET.Test.Sdk": "18.0.1", - "Microsoft.Testing.Extensions.CodeCoverage": "18.4.1", - "Microsoft.Testing.Extensions.TrxReport": "2.1.0" - } - }, - "Testcontainers.PostgreSql": { - "type": "Direct", - "requested": "[4.10.0, )", - "resolved": "4.10.0", - "contentHash": "TP7j3N014O9MONT21lqZPzlVuP1LJYhkRKYZPbRdHl3VN+4RPk5Jt799WvLfxDsOFLVNibNO3B7tP1vcYQmXHA==", - "dependencies": { - "Testcontainers": "4.10.0" - } - }, - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.6.2", - "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w==" - }, - "Docker.DotNet.Enhanced": { - "type": "Transitive", - "resolved": "3.131.1", - "contentHash": "hGLHCNUsQbT2Ab/HUznRnNqYZQs40zInXa3eLwYjeNyfUYbw1pqqDGqcOLl5uGepS8IuigEYakEdAcVT/2ezYg==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.3" - } - }, - "Docker.DotNet.Enhanced.X509": { - "type": "Transitive", - "resolved": "3.131.1", - "contentHash": "8FU7zmttFQzp0xb0EPupxQ0nGtC2cTpukgh3jMxMT8luj5TSDyzIKTnroDpXCjpg9P2fV+6JIvC+IetsMEfyBA==", - "dependencies": { - "Docker.DotNet.Enhanced": "3.131.1" - } - }, - "Grpc.AspNetCore.Server": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "diSC/ZeNdSdxHdYSOpYwuSBBDYpuNVtJQFJfiBB0WrYOQ4lVMmdxuUZJcViahQyo8pCvS3Mueo5lqFxwwMF/iw==", - "dependencies": { - "Grpc.Net.Common": "2.76.0" - } - }, - "Grpc.AspNetCore.Server.ClientFactory": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "y5KGO1GO0N2L/hCCMR05mmoK8j+v8rKvZ+9nothAxKx2Tf2CwV8f4TM5K0GkKfDsp4vrc4lm90MU6E+DeN7YIw==", - "dependencies": { - "Grpc.AspNetCore.Server": "2.76.0", - "Grpc.Net.ClientFactory": "2.76.0" - } - }, - "Grpc.Core.Api": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ==" - }, - "Grpc.Net.Common": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==", - "dependencies": { - "Grpc.Core.Api": "2.76.0" - } - }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.23.0", - "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" - }, - "Microsoft.AspNetCore.Cryptography.Internal": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "63eZHvKVbBnjX3QDbac2S+05SKbaX/2yiQYZI0VR10EegeZGMhT1LPgi0ax76UfLkFOgap7UGpybPbjgzUrlag==" - }, - "Microsoft.AspNetCore.Cryptography.KeyDerivation": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "+Bi0aTctq7Z+j2lpSfQ/CdrA8x7JcsmJDd8KC6IC93rvNokRbi3wJ4H0gJqlNysTGp764D/KGsL93fsqddlLEw==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.Internal": "10.0.4" - } - }, - "Microsoft.AspNetCore.Metadata": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w==" - }, - "Microsoft.AspNetCore.TestHost": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "+kBqWRfgforSlqlW+s8VotYy1OxdCbg20ZUVHcxjRcMHEtoMq7IJMbtc7jtacgWSMy+Ct154SLOYHCp7Azpp+w==" - }, - "Microsoft.CodeCoverage": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" - }, - "Microsoft.DiaSymReader": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite.Core": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==", - "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.4", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.Extensions.AmbientMetadata.Application": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "bovnONzrr/JIc+w343i857rJEb7cQH9UzEjbV5n67agWBEYICGQb8xiqYz5+GoFXp6mKEKLwYCQGttMU1p5yXQ==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "uDRooaV6N3WZ0kdlNPMB68/MdGn/in1Fs7Db7DnIm85RBTPy4P321WO+daAImiYpH5dekjNggDqy1N44WaIlMA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Compliance.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "4WkknDbVrHNf+S6fwSt1OAXlGJ/G/QrtJlqx4aNzOLmeT3GRyxpGLZn+Q3UV+RMRAF6FfsijEZBg2ZAW8bTAkg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.CommandLine": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DPl2tZZGtqxrkOq/Exo43hRuTA8KI7UXWnf6y0SkRBbqSkqFR1a+7rIZPFMP1KLE2UYiIvfbVR7ynRPJ/HeMDw==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.EnvironmentVariables": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "iapetemB9HxFe5YzEOD1faKC3pYKKaK0CUi85e5iZU5s541x1UVloswQB5zaN1TZg8t/7ONWaf8RYKuTDjveOw==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.FileExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Physical": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.UserSecrets": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "+lRKEecmj+pSZh3we5sxiVS90YmfdJye4qps6vF9GFRbdm563kU0YyFnwCsm2PDTqQbOe3M8kE2vaV4zleeE7Q==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Json": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Physical": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "NkvJ8aSr3AG30yabjv7ZWwTG/wq5OElNTlNq39Ok2HSEF3TIwAc1f1xnTJlR/GuoJmEgkfT7WBO9YbSXRk41+g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ==" - }, - "Microsoft.Extensions.DependencyInjection.AutoActivation": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "ksmUG2SFTcXzYdyoLOdeSM/qYLRGN6qbbSzYVkwMK9xsctfR1hYkUayeOpFCMd7L+QSlYX72mK9wxwdgQxyS4g==", - "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg==" - }, - "Microsoft.Extensions.Diagnostics": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "1/hQmONMWxRTKXuN0pQShQN9QsqIRTS1G4fdmKW0O9phuVZjyzIROQD9Fbfwyn2t+yvP8SzjatGAPX4jDRfgHg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Features": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "7to+nkZO+g/GiGQOBzAcrr8HcG8dXETI/hg58fJju0jPO9p/GvNLAis8kMPTBdsjfeTfslBrgFX9Yx1KRnKDww==" - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileProviders.Embedded": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "yM9BTzWdqLDk3v08UcmRJqUAbFiiP/cYFkkoHRphDv+YdMFS7cIc6YL2cbfaFlLfBomBA4Zx8+8lB6lRxvDFxA==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.FileProviders.Physical": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileSystemGlobbing": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileSystemGlobbing": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg==" - }, - "Microsoft.Extensions.Hosting": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DqWjjar9YgGXswSmp81jW6kSsOXVqsBkJS9nRI0HEi7NK9c7uSjCfZ0pXLEYlKP3+t3uLWrDdyLIlkKQYxUwmA==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.Configuration.CommandLine": "10.0.4", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.4", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4", - "Microsoft.Extensions.Configuration.Json": "10.0.4", - "Microsoft.Extensions.Configuration.UserSecrets": "10.0.4", - "Microsoft.Extensions.DependencyInjection": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Physical": "10.0.4", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Configuration": "10.0.4", - "Microsoft.Extensions.Logging.Console": "10.0.4", - "Microsoft.Extensions.Logging.Debug": "10.0.4", - "Microsoft.Extensions.Logging.EventLog": "10.0.4", - "Microsoft.Extensions.Logging.EventSource": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Http": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "QRbs+A+WfiGTnV9KFNfWlF+My5euQNZnsvdVMulwRN6C/tEPaF+ZlQfedHoNvFHKLwjQMmqwm4z+TSO9eLvRQw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Http.Diagnostics": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "ybx2QcCWROCnUCbSj/IyHXn1c58brjjHzTTbueKgBl/qHsWk69mu25mjQ3oaMsO1I0+EcS6AhVuhIopL2q3IDw==", - "dependencies": { - "Microsoft.Extensions.Http": "10.0.4", - "Microsoft.Extensions.Telemetry": "10.4.0" - } - }, - "Microsoft.Extensions.Identity.Core": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "n2MIwxPu+5+voKM94XFzTTUPRKFc26fhDH4vYqU1BjpNXQIYA9N3Dg811q7K6gOr9YgukaxZqyimkKKj9qUqEA==", - "dependencies": { - "Microsoft.AspNetCore.Cryptography.KeyDerivation": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Identity.Stores": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "PY0WbLDw8DiQgj59faevWLCTVo0T6+0Qhed1rghLo909xsZC9mRiBnF8O6U6KpFHbpS8CpPDvh+3JfasWYV4tw==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.4", - "Microsoft.Extensions.Identity.Core": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "S8+6fCuMOhJZGk8sGFtOy3VsF9mk9x4UOL59GM91REiA/fmCDjunKKIw4RmStG87qyXPfxelDJf2pXIbTuaBdw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Logging.Configuration": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "XPXoOpUnWEh0pV7Vl2DK2wj47y73Krhrve5OkPrvGIWdZ4U2r47WO8hEdv+wKn65Kh4pmDdiWm7Ibo5pZX+vig==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.Logging.Console": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "70fnP/xbQmH3cRtj4jjXOuqRWrYIMRz897KngW5Egu6to60pikT/09Y9ZxCljT/PWQDZ+qLqyeaAwNJe/L6FbA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Configuration": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Logging.EventLog": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "P/iPKwMNvtcfX5tb48KbcAs8ZMfv5tfDUilFNtTW9hwiWrLDFBJ7WE1QZuVMb8Ro1MvXHaqmc+p2rRPVEqZ2SA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "System.Diagnostics.EventLog": "10.0.4" - } - }, - "Microsoft.Extensions.Logging.EventSource": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "se693u6nFSHxBaAL6CVshlPhrWiGi6NjgkbmTxACLmuJfcldaGsDdzc0e8Izcz+OFMr6hOtNKMBTbv4U8XaFxw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "2pufIFOgNl/yWTOoIC9XgBnO9VxgfAjdRCnVwpE2+ICfcroGnjuEAGzJ5lTdZeAe0HvA31vMBWXtcmGB7TOq3g==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg==" - }, - "Microsoft.Extensions.Resilience": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "41CCbJJPsDWU6NsmKfANHkfT/+KCBlZZqQ1eBoQhhW0xqGCiWmUlMdi2BoaM/GcwKHX5WiQL/IESROmgk0Owfw==", - "dependencies": { - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.4.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4", - "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0", - "Polly.Extensions": "8.4.2", - "Polly.RateLimiting": "8.4.2" - } - }, - "Microsoft.Extensions.ServiceDiscovery.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "HkBb7cdi27tkQiQw1anQFbXe+A3pjRwDKgVbd/DD9fMAO2X9abK0FEyM/tNVXjW3lwOWl2tF+Xij/DqI6i+JTg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Features": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "AbHleTzdpGPjA6RpOjKVHEYx7SoBRnJ2bwAbbPa3aGB7HiVwBmeTJhBGhtIBiuIW0VpKDS8x+bV5iWqpBRIf4w==", - "dependencies": { - "Microsoft.Extensions.AmbientMetadata.Application": "10.4.0", - "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.4.0", - "Microsoft.Extensions.Logging.Configuration": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0" - } - }, - "Microsoft.Extensions.Telemetry.Abstractions": { - "type": "Transitive", - "resolved": "10.4.0", - "contentHash": "3b2uVa4voJfLLg39BPCKQS0ZgnpEZFkKf7YmnMVlM5FQJYBPOuePIQdnEK1/Oxd+w3GscxGYuE7IMOXDwixZtQ==", - "dependencies": { - "Microsoft.Extensions.Compliance.Abstractions": "10.4.0", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.IdentityModel.Abstractions": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.16.0" - } - }, - "Microsoft.IdentityModel.Protocols": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", - "dependencies": { - "Microsoft.IdentityModel.Tokens": "8.0.1" - } - }, - "Microsoft.IdentityModel.Protocols.OpenIdConnect": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", - "dependencies": { - "Microsoft.IdentityModel.Protocols": "8.0.1", - "System.IdentityModel.Tokens.Jwt": "8.0.1" - } - }, - "Microsoft.NET.Test.Sdk": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", - "dependencies": { - "Microsoft.CodeCoverage": "18.0.1", - "Microsoft.TestPlatform.TestHost": "18.0.1" - } - }, - "Microsoft.OpenApi": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "GGYLfzV/G/ct80OZ45JxnWP7NvMX1BCugn/lX7TH5o0lcVaviavsLMTxmFV2AybXWjbi3h6FF1vgZiTK6PXndw==" - }, - "Microsoft.Testing.Extensions.CodeCoverage": { - "type": "Transitive", - "resolved": "18.4.1", - "contentHash": "l1VZM9dg9s76L5D288ipAT4HRYDJ6Vxh8wX20gfS9VnpueedRfN4/aGNn4oA1g6pwq2WSM3Ci7IoSSGPiqu+WQ==", - "dependencies": { - "Microsoft.DiaSymReader": "2.0.0", - "Microsoft.Extensions.DependencyModel": "8.0.2", - "Microsoft.Testing.Platform": "2.0.2" - } - }, - "Microsoft.Testing.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "5TwgTx2u7k9Al/xbZ18QXq4Hdy2xewkVTI6K3sk+jY2ykqUkIKNuj7rFu3GOV5KnEUkevhw6eZcyZs77STHJIA==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.TrxReport": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "cXmP225WcMLLOSrW8xekaNhfzdBwXX3cbXbE5qSzmLbK0KZe3z8rAObKj70FWiPPPzm2W22x0ZW93gsmAfK6Mg==", - "dependencies": { - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.TrxReport.Abstractions": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "D8xmIJYQFJ6D49Rx5/vPrkZZxb338Jkew+eSqZLBfBiWKw4QZKy3i1BOXiLfz0lOmaNErwDz/YWRojCdNl+B9Q==", - "dependencies": { - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Extensions.VSTestBridge": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "bNRIEA2YoGr+Y+7LHdA7i1U80+7BAdf4K4Qh4Kx6eKkoBK/NV7QpoMg+GWPP0/eqAFzuUmUOIPVZ87Oo0Vyxmw==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", - "Microsoft.Testing.Extensions.Telemetry": "2.1.0", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.Testing.Platform": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "aHkjNTGIA+Zbdw6RJgSFrbDrCjO0CgqpElqYcvkRSeUhBv2bKarnvU3ep786U7UqrPlArT/B7VmImRibJD0Zrg==" - }, - "Microsoft.Testing.Platform.MSBuild": { - "type": "Transitive", - "resolved": "2.1.0", - "contentHash": "UpfPebXQtHGrWz21+YLHmJSm+5zsuPE9U9pfdCtoB+67g75fDmWlNgpkH2ZmdVhSwkjNIed9Icg8Iu63z2ei5Q==", - "dependencies": { - "Microsoft.Testing.Platform": "2.1.0" - } - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" - }, - "Microsoft.TestPlatform.TestHost": { - "type": "Transitive", - "resolved": "18.0.1", - "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.0.1", - "Newtonsoft.Json": "13.0.3" - } - }, - "MSTest.Analyzers": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "4ElL/aqomiUInr090VN4udqz46AuszXLrifHkLrgj0zb7na8eAoyUQt3BwDLTcGd1bSkmk3SfD02rZtKU+ZiqQ==" - }, - "MSTest.TestAdapter": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "bRW1Hftwq0XbcVExcAbj4YAfSZDRAziL0mygDkPBvaUe2nSsWFQIatze5lHVjPFJMvSFgWnItku4pguIy5FowQ==", - "dependencies": { - "MSTest.TestFramework": "4.1.0", - "Microsoft.Testing.Extensions.VSTestBridge": "2.1.0", - "Microsoft.Testing.Platform.MSBuild": "2.1.0" - } - }, - "MSTest.TestFramework": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "BzpvsK+CRbk6khwY62h+7HfYzIxtJXyPv9tOI9T90cy5CVy+WI1JkN4ZaNL4Dobqb6dywSwabLTIbPZKpdrr+A==", - "dependencies": { - "MSTest.Analyzers": "4.1.0" - } - }, - "Newtonsoft.Json": { - "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "Npgsql": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "10.0.0" - } - }, - "OpenTelemetry": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==", - "dependencies": { - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", - "Microsoft.Extensions.Logging.Configuration": "10.0.0", - "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0" - } - }, - "OpenTelemetry.Api": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w==" - }, - "OpenTelemetry.Api.ProviderBuilderExtensions": { - "type": "Transitive", - "resolved": "1.15.0", - "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", - "OpenTelemetry.Api": "1.15.0" - } - }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, - "Polly.Extensions": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Polly.Core": "8.4.2" - } - }, - "Polly.RateLimiting": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", - "dependencies": { - "Polly.Core": "8.4.2", - "System.Threading.RateLimiting": "8.0.0" - } - }, - "Serilog": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" - }, - "Serilog.Extensions.Hosting": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", - "Microsoft.Extensions.Logging.Abstractions": "10.0.0", - "Serilog": "4.3.0", - "Serilog.Extensions.Logging": "10.0.0" - } - }, - "Serilog.Extensions.Logging": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", - "dependencies": { - "Microsoft.Extensions.Logging": "10.0.0", - "Serilog": "4.2.0" - } - }, - "Serilog.Formatting.Compact": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "Serilog.Settings.Configuration": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "10.0.0", - "Microsoft.Extensions.DependencyModel": "10.0.0", - "Serilog": "4.3.0" - } - }, - "Serilog.Sinks.Debug": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "SharpZipLib": { - "type": "Transitive", - "resolved": "1.4.2", - "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" - }, - "SQLitePCLRaw.bundle_e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", - "dependencies": { - "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", - "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" - } - }, - "SQLitePCLRaw.core": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" - }, - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" - }, - "SQLitePCLRaw.provider.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", - "dependencies": { - "SQLitePCLRaw.core": "2.1.11" - } - }, - "SSH.NET": { - "type": "Transitive", - "resolved": "2025.1.0", - "contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==", - "dependencies": { - "BouncyCastle.Cryptography": "2.6.2", - "Microsoft.Extensions.Logging.Abstractions": "8.0.3" - } - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "xXo5NcIPeMUMfpo9Efl/3Rgomjb/Jfa3Aa0BgSfqYctN4YpwjPzvIrnD9JTwoKqe3pFtLF9vmtMl892+wwZ6EA==" - }, - "System.Threading.RateLimiting": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" - }, - "Testcontainers": { - "type": "Transitive", - "resolved": "4.10.0", - "contentHash": "a7tH+s9IRME6QEeMRgl/mTqQyudgtGNJmJRPn1+LwW8w/2L11cJzRJd7Io0QoSrP+i6lAOETX2SRY7cLbElcdQ==", - "dependencies": { - "Docker.DotNet.Enhanced": "3.131.1", - "Docker.DotNet.Enhanced.X509": "3.131.1", - "Microsoft.Extensions.Logging.Abstractions": "8.0.3", - "SSH.NET": "2025.1.0", - "SharpZipLib": "1.4.2" - } - }, - "werkr.api": { - "type": "Project", - "dependencies": { - "Grpc.AspNetCore": "[2.76.0, )", - "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.4, )", - "Microsoft.AspNetCore.OpenApi": "[10.0.4, )", - "Microsoft.IdentityModel.JsonWebTokens": "[8.16.0, )", - "Serilog.AspNetCore": "[10.0.0, )", - "Serilog.Sinks.Console": "[6.1.1, )", - "Serilog.Sinks.File": "[7.0.0, )", - "Serilog.Sinks.OpenTelemetry": "[4.2.0, )", - "Werkr.Common": "[1.0.0, )", - "Werkr.Core": "[1.0.0, )", - "Werkr.Data": "[1.0.0, )", - "Werkr.ServiceDefaults": "[1.0.0, )" - } - }, - "werkr.common": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.34.0, )", - "Microsoft.AspNetCore.Authorization": "[10.0.4, )", - "Microsoft.Extensions.Configuration.Json": "[10.0.4, )", - "Microsoft.IdentityModel.Tokens": "[8.16.0, )", - "Werkr.Common.Configuration": "[1.0.0, )" - } - }, - "werkr.common.configuration": { - "type": "Project" - }, - "werkr.core": { - "type": "Project", - "dependencies": { - "Grpc.Net.Client": "[2.76.0, )", - "Microsoft.Extensions.Hosting.Abstractions": "[10.0.4, )", - "System.Security.Cryptography.ProtectedData": "[10.0.4, )", - "Werkr.Common": "[1.0.0, )", - "Werkr.Data": "[1.0.0, )" - } - }, - "werkr.data": { - "type": "Project", - "dependencies": { - "EFCore.NamingConventions": "[10.0.1, )", - "Microsoft.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )", - "Werkr.Common": "[1.0.0, )" - } - }, - "werkr.data.identity": { - "type": "Project", - "dependencies": { - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.AspNetCore.Identity.UI": "[10.0.4, )", - "Werkr.Common": "[1.0.0, )", - "Werkr.Data": "[1.0.0, )" - } - }, - "werkr.servicedefaults": { - "type": "Project", - "dependencies": { - "Microsoft.Extensions.Http.Resilience": "[10.4.0, )", - "Microsoft.Extensions.ServiceDiscovery": "[10.4.0, )", - "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.0, )", - "OpenTelemetry.Extensions.Hosting": "[1.15.0, )" - } - }, - "EFCore.NamingConventions": { - "type": "CentralTransitive", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" - } - }, - "Google.Protobuf": { - "type": "CentralTransitive", - "requested": "[3.34.0, )", - "resolved": "3.34.0", - "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" - }, - "Grpc.AspNetCore": { - "type": "CentralTransitive", - "requested": "[2.76.0, )", - "resolved": "2.76.0", - "contentHash": "LyXMmpN2Ba0TE35SOLSKbGqIYtJuhc1UgiaGfoW1X8KJERV70QI5KGW+ckEY7MrXoFWN/uWo4B70siVhbDmCgQ==", - "dependencies": { - "Google.Protobuf": "3.31.1", - "Grpc.AspNetCore.Server.ClientFactory": "2.76.0", - "Grpc.Tools": "2.76.0" - } - }, - "Grpc.Net.Client": { - "type": "CentralTransitive", - "requested": "[2.76.0, )", - "resolved": "2.76.0", - "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==", - "dependencies": { - "Grpc.Net.Common": "2.76.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Grpc.Net.ClientFactory": { - "type": "CentralTransitive", - "requested": "[2.76.0, )", - "resolved": "2.76.0", - "contentHash": "XI+kO69L9AV8B9N0UQOmH911r6MOEp9huHiavEsY56DJYuzJ9KAxNGy37dpV6CLbgCaN2uKmpOsZ9Pao6bmpVQ==", - "dependencies": { - "Grpc.Net.Client": "2.76.0", - "Microsoft.Extensions.Http": "8.0.0" - } - }, - "Microsoft.AspNetCore.Authentication.JwtBearer": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1mUwGyeGrGETdhlU/ZzNZeN2J6ugqf9EztZyG+WQIZwkaH5+lFNza15+cNCXhXNA0MKo1iohr5vj60eqR2J44Q==", - "dependencies": { - "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" - } - }, - "Microsoft.AspNetCore.Authorization": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==", - "dependencies": { - "Microsoft.AspNetCore.Metadata": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "xcuRo8Ubxwf3xay2z7mD3b3TmEnvMhSLzvfx5cqmk3V10oxTDqgIyZ8C5qav+iAc27In/TXteSJ6kS2iZpah7w==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.Identity.Stores": "10.0.4" - } - }, - "Microsoft.AspNetCore.Identity.UI": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "ZJlXBj2KgSKeR7WWl/1XIOiJLgvxKL7v0/YEkfWoCQQdVqLPz2sEsg3EcvqiQ6w2oR/gjdqDsCa2zkKgHYMnNg==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Embedded": "10.0.4", - "Microsoft.Extensions.Identity.Stores": "10.0.4" - } - }, - "Microsoft.AspNetCore.OpenApi": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "OsEhbmT4Xenukau5YCR867gr/HmuAJ9DqMBPQGTcmdNU/TqBqdcnB+yLNwD/mTdkHzLBB+XG7cI4H1L5B1jx+Q==", - "dependencies": { - "Microsoft.OpenApi": "2.0.0" - } - }, - "Microsoft.Data.Sqlite.Core": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==", - "dependencies": { - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.EntityFrameworkCore": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "+5mQrqlBhqNUaPyDmFSNM/qiWStvE9LMxZW1MRF0NhEbO781xNeKryXNR9gGDJ0PmYFDAVoMT9ffX+I15LPFTw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Http.Resilience": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "HbkUsPUC7vLy2TaDbdA9aooW64n9yX4sUppRuiJ1cOzzU1FUW+MVEotm6kYVq6AuUI9xwFSBhRFzA03blmk3VA==", - "dependencies": { - "Microsoft.Extensions.Http.Diagnostics": "10.4.0", - "Microsoft.Extensions.ObjectPool": "10.0.4", - "Microsoft.Extensions.Resilience": "10.4.0" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Logging.Debug": { - "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.4", - "contentHash": "vd1bEO+hoJsQZ+cyhtLT7l5/5rtCgc4G5wJdPlYtWSSXe4hv00CDYTATbpyPjCehZL3pNv+aC1RwRlU5oWA6xQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.ServiceDiscovery": { - "type": "CentralTransitive", - "requested": "[10.4.0, )", - "resolved": "10.4.0", - "contentHash": "RznZAH6L4RNvroECT5JpqfFQJjHTn+8N7+ThSgYutbshkuymFeL/uBIZt1CM8LOdpPPhn4//a5fLUah9/k7ayQ==", - "dependencies": { - "Microsoft.Extensions.Http": "10.0.4", - "Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.4.0" - } - }, - "Microsoft.IdentityModel.Tokens": { - "type": "CentralTransitive", - "requested": "[8.16.0, )", - "resolved": "8.16.0", - "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "10.0.0", - "Microsoft.IdentityModel.Logging": "8.16.0" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)", - "Npgsql": "10.0.0" - } - }, - "OpenTelemetry.Exporter.OpenTelemetryProtocol": { - "type": "CentralTransitive", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==", - "dependencies": { - "OpenTelemetry": "1.15.0" - } - }, - "OpenTelemetry.Extensions.Hosting": { - "type": "CentralTransitive", - "requested": "[1.15.0, )", - "resolved": "1.15.0", - "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==", - "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", - "OpenTelemetry": "1.15.0" - } - }, - "Serilog.AspNetCore": { - "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", - "dependencies": { - "Serilog": "4.3.0", - "Serilog.Extensions.Hosting": "10.0.0", - "Serilog.Formatting.Compact": "3.0.0", - "Serilog.Settings.Configuration": "10.0.0", - "Serilog.Sinks.Console": "6.1.1", - "Serilog.Sinks.Debug": "3.0.0", - "Serilog.Sinks.File": "7.0.0" - } - }, - "Serilog.Sinks.Console": { - "type": "CentralTransitive", - "requested": "[6.1.1, )", - "resolved": "6.1.1", - "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "Serilog.Sinks.File": { - "type": "CentralTransitive", - "requested": "[7.0.0, )", - "resolved": "7.0.0", - "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", - "dependencies": { - "Serilog": "4.2.0" - } - }, - "Serilog.Sinks.OpenTelemetry": { - "type": "CentralTransitive", - "requested": "[4.2.0, )", - "resolved": "4.2.0", - "contentHash": "PzMCyE5G19tjr5IZEi5qg+4UU5QrxBEoBEMu/hhYybTrGKXqUDiSGWKZNUDBgelaVKqLADlsmlJVyKce5SyPrg==", - "dependencies": { - "Google.Protobuf": "3.30.1", - "Grpc.Net.Client": "2.70.0", - "Serilog": "4.2.0" - } - }, - "System.IdentityModel.Tokens.Jwt": { - "type": "CentralTransitive", - "requested": "[8.16.0, )", - "resolved": "8.0.1", - "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", - "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", - "Microsoft.IdentityModel.Tokens": "8.0.1" - } - }, - "System.Security.Cryptography.ProtectedData": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g==" - } - } - } +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.AspNetCore.Mvc.Testing": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "MfacYQ7jNzj6073YobyoFfXpNmGqrV1UCywTM339DOcYpfalcM4K4heFjV5k3dDkKkWOGWO/DV3hdmVRqFkIxA==", + "dependencies": { + "Microsoft.AspNetCore.TestHost": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Hosting": "10.0.5" + } + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Direct", + "requested": "[8.16.0, )", + "resolved": "8.16.0", + "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.16.0" + } + }, + "MSTest": { + "type": "Direct", + "requested": "[4.1.0, )", + "resolved": "4.1.0", + "contentHash": "2bk47yg7HcHRyf6Zf0XgCZicTVTQj4D5lonYTO7lWMxCQB+x66VrQNc2dADBfzthKXfHaA37m8i+VV5h6SbWiA==", + "dependencies": { + "MSTest.TestAdapter": "4.1.0", + "MSTest.TestFramework": "4.1.0", + "Microsoft.NET.Test.Sdk": "18.0.1", + "Microsoft.Testing.Extensions.CodeCoverage": "18.4.1", + "Microsoft.Testing.Extensions.TrxReport": "2.1.0" + } + }, + "Testcontainers.PostgreSql": { + "type": "Direct", + "requested": "[4.11.0, )", + "resolved": "4.11.0", + "contentHash": "OWGi0Og+qFpr2OPDignA74aJSfUd0nvZOaXNGWXwMJR1BvpdzhCNHQB2h7yLSTb0a8JVmmQQ4mUpHAC9q27NLw==", + "dependencies": { + "Testcontainers": "4.11.0" + } + }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.6.2", + "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w==" + }, + "Docker.DotNet.Enhanced": { + "type": "Transitive", + "resolved": "3.131.1", + "contentHash": "hGLHCNUsQbT2Ab/HUznRnNqYZQs40zInXa3eLwYjeNyfUYbw1pqqDGqcOLl5uGepS8IuigEYakEdAcVT/2ezYg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.3" + } + }, + "Docker.DotNet.Enhanced.X509": { + "type": "Transitive", + "resolved": "3.131.1", + "contentHash": "8FU7zmttFQzp0xb0EPupxQ0nGtC2cTpukgh3jMxMT8luj5TSDyzIKTnroDpXCjpg9P2fV+6JIvC+IetsMEfyBA==", + "dependencies": { + "Docker.DotNet.Enhanced": "3.131.1" + } + }, + "Grpc.AspNetCore.Server": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "diSC/ZeNdSdxHdYSOpYwuSBBDYpuNVtJQFJfiBB0WrYOQ4lVMmdxuUZJcViahQyo8pCvS3Mueo5lqFxwwMF/iw==", + "dependencies": { + "Grpc.Net.Common": "2.76.0" + } + }, + "Grpc.AspNetCore.Server.ClientFactory": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "y5KGO1GO0N2L/hCCMR05mmoK8j+v8rKvZ+9nothAxKx2Tf2CwV8f4TM5K0GkKfDsp4vrc4lm90MU6E+DeN7YIw==", + "dependencies": { + "Grpc.AspNetCore.Server": "2.76.0", + "Grpc.Net.ClientFactory": "2.76.0" + } + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ==" + }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==", + "dependencies": { + "Grpc.Core.Api": "2.76.0" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.AspNetCore.Cryptography.Internal": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "kFUpNRYySfqNLuQKGMKZ2mK8b86R1zizlc9QB6R/Ess0rSkrA8pRNCMSFm+DqUnNfm5G3FWjsYIJOKYyhkHeig==" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "NkWUZYkL6wavYY2wMZgnODzsyTOZhcRxP/DJvZlBbWEJViukdyuIqtdTzltODyjsc3MjEvxmbPDDk2KgGv6tMA==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "10.0.5" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A==" + }, + "Microsoft.AspNetCore.TestHost": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "PJEdrZnnhvxIEXzDdvdZ38GvpdaiUfKkZ99kudS8riJwhowFb/Qh26Wjk9smrCWcYdMFQmpN5epGiL4o1s8LYA==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "O+utSr97NAJowIQT/OVp3Lh9QgW/wALVTP4RG1m2AfFP4IyJmJz0ZBmFJUsRQiAPgq6IRC0t8AAzsiPIsaUDEA==" + }, + "Microsoft.DiaSymReader": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "QcZrCETsBJqy/vQpFtJc+jSXQ0K5sucQ6NUFbTNVHD4vfZZOwjZ/3sBzczkC4DityhD3AVO/+K/+9ioLs1AgRA==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.AmbientMetadata.Application": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "bovnONzrr/JIc+w343i857rJEb7cQH9UzEjbV5n67agWBEYICGQb8xiqYz5+GoFXp6mKEKLwYCQGttMU1p5yXQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.4", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "4WkknDbVrHNf+S6fwSt1OAXlGJ/G/QrtJlqx4aNzOLmeT3GRyxpGLZn+Q3UV+RMRAF6FfsijEZBg2ZAW8bTAkg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "or9fOLopMUTJOQVJ3bou4aD6PwvsiKf4kZC4EE5sRRKSkmh+wfk/LekJXRjAX88X+1JA9zHjDo+5fiQ7z3MY/A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "fhdG6UV9lIp70QhNkVyaHciUVq25IPFkczheVJL9bIFvmnJ+Zghaie6dWkDbbVmxZlHl9gj3zTDxMxJs5zNhIA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Json": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" + }, + "Microsoft.Extensions.DependencyInjection.AutoActivation": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "ksmUG2SFTcXzYdyoLOdeSM/qYLRGN6qbbSzYVkwMK9xsctfR1hYkUayeOpFCMd7L+QSlYX72mK9wxwdgQxyS4g==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.4" + } + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "1/hQmONMWxRTKXuN0pQShQN9QsqIRTS1G4fdmKW0O9phuVZjyzIROQD9Fbfwyn2t+yvP8SzjatGAPX4jDRfgHg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + } + }, + "Microsoft.Extensions.Features": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "7to+nkZO+g/GiGQOBzAcrr8HcG8dXETI/hg58fJju0jPO9p/GvNLAis8kMPTBdsjfeTfslBrgFX9Yx1KRnKDww==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileProviders.Embedded": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "DQzkbLFNfwmjxErAnWyZTTyBd4cMo6vmGteM4Ayedhk5Pccm2VuKoeKcOZjJG1T+dYK6lMCNk2L7Ftl7dLhgqg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" + }, + "Microsoft.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8i7e5IBdiKLNqt/+ciWrS8U95Rv5DClaaj7ulkZbimnCi4uREWd+lXzkp3joofFuIPOlAzV4AckxLTIELv2jdg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.5", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.Configuration.Json": "10.0.5", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.5", + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Configuration": "10.0.5", + "Microsoft.Extensions.Logging.Console": "10.0.5", + "Microsoft.Extensions.Logging.Debug": "10.0.5", + "Microsoft.Extensions.Logging.EventLog": "10.0.5", + "Microsoft.Extensions.Logging.EventSource": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "QRbs+A+WfiGTnV9KFNfWlF+My5euQNZnsvdVMulwRN6C/tEPaF+ZlQfedHoNvFHKLwjQMmqwm4z+TSO9eLvRQw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.Diagnostics": "10.0.4", + "Microsoft.Extensions.Logging": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.Extensions.Http.Diagnostics": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "ybx2QcCWROCnUCbSj/IyHXn1c58brjjHzTTbueKgBl/qHsWk69mu25mjQ3oaMsO1I0+EcS6AhVuhIopL2q3IDw==", + "dependencies": { + "Microsoft.Extensions.Http": "10.0.4", + "Microsoft.Extensions.Telemetry": "10.4.0" + } + }, + "Microsoft.Extensions.Identity.Core": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ew2Xob+HFvJvM7BelIrUIbGeVMO2q1A6gsZEsI8N/v0ddSv7qbvvY68mH16XzvlsqydqD3ct5ioQHsiEUDSNkg==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Identity.Stores": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "St8g+4xGLUhfSzTlHSLtCv7kh/tppvFab5x0kFIOsWryf1ffK2Ux+JIg01v5Yf27g2iQLCFEmW5hG5DDZL1HLA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.Identity.Core": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Configuration": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "0ezhWYJS4/6KrqQel9JL+Tr4n+4EX2TF5EYiaysBWNNEM2c3Gtj1moD39esfgk8OHblSX+UFjtZ3z0c4i9tRvw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "System.Diagnostics.EventLog": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "vN+aq1hBFXyYvY5Ow9WyeR66drKQxRZmas4lAjh6QWfryPkjTn1uLtX5AFIxyDaZj78v5TG2sELUyvrXpAPQQw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "2pufIFOgNl/yWTOoIC9XgBnO9VxgfAjdRCnVwpE2+ICfcroGnjuEAGzJ5lTdZeAe0HvA31vMBWXtcmGB7TOq3g==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.Extensions.Resilience": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "41CCbJJPsDWU6NsmKfANHkfT/+KCBlZZqQ1eBoQhhW0xqGCiWmUlMdi2BoaM/GcwKHX5WiQL/IESROmgk0Owfw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics": "10.0.4", + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.4.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4", + "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0", + "Polly.Extensions": "8.4.2", + "Polly.RateLimiting": "8.4.2" + } + }, + "Microsoft.Extensions.ServiceDiscovery.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "HkBb7cdi27tkQiQw1anQFbXe+A3pjRwDKgVbd/DD9fMAO2X9abK0FEyM/tNVXjW3lwOWl2tF+Xij/DqI6i+JTg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", + "Microsoft.Extensions.Configuration.Binder": "10.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.Features": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4", + "Microsoft.Extensions.Primitives": "10.0.4" + } + }, + "Microsoft.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "AbHleTzdpGPjA6RpOjKVHEYx7SoBRnJ2bwAbbPa3aGB7HiVwBmeTJhBGhtIBiuIW0VpKDS8x+bV5iWqpBRIf4w==", + "dependencies": { + "Microsoft.Extensions.AmbientMetadata.Application": "10.4.0", + "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.4.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0" + } + }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "3b2uVa4voJfLLg39BPCKQS0ZgnpEZFkKf7YmnMVlM5FQJYBPOuePIQdnEK1/Oxd+w3GscxGYuE7IMOXDwixZtQ==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "10.4.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.16.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "WNpu6vI2rA0pXY4r7NKxCN16XRWl5uHu6qjuyVLoDo6oYEggIQefrMjkRuibQHm/NslIUNCcKftvoWAN80MSAg==", + "dependencies": { + "Microsoft.CodeCoverage": "18.0.1", + "Microsoft.TestPlatform.TestHost": "18.0.1" + } + }, + "Microsoft.OpenApi": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "GGYLfzV/G/ct80OZ45JxnWP7NvMX1BCugn/lX7TH5o0lcVaviavsLMTxmFV2AybXWjbi3h6FF1vgZiTK6PXndw==" + }, + "Microsoft.Testing.Extensions.CodeCoverage": { + "type": "Transitive", + "resolved": "18.4.1", + "contentHash": "l1VZM9dg9s76L5D288ipAT4HRYDJ6Vxh8wX20gfS9VnpueedRfN4/aGNn4oA1g6pwq2WSM3Ci7IoSSGPiqu+WQ==", + "dependencies": { + "Microsoft.DiaSymReader": "2.0.0", + "Microsoft.Extensions.DependencyModel": "8.0.2", + "Microsoft.Testing.Platform": "2.0.2" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "5TwgTx2u7k9Al/xbZ18QXq4Hdy2xewkVTI6K3sk+jY2ykqUkIKNuj7rFu3GOV5KnEUkevhw6eZcyZs77STHJIA==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.TrxReport": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "cXmP225WcMLLOSrW8xekaNhfzdBwXX3cbXbE5qSzmLbK0KZe3z8rAObKj70FWiPPPzm2W22x0ZW93gsmAfK6Mg==", + "dependencies": { + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "D8xmIJYQFJ6D49Rx5/vPrkZZxb338Jkew+eSqZLBfBiWKw4QZKy3i1BOXiLfz0lOmaNErwDz/YWRojCdNl+B9Q==", + "dependencies": { + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Extensions.VSTestBridge": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "bNRIEA2YoGr+Y+7LHdA7i1U80+7BAdf4K4Qh4Kx6eKkoBK/NV7QpoMg+GWPP0/eqAFzuUmUOIPVZ87Oo0Vyxmw==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Microsoft.Testing.Extensions.Telemetry": "2.1.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.1.0", + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "aHkjNTGIA+Zbdw6RJgSFrbDrCjO0CgqpElqYcvkRSeUhBv2bKarnvU3ep786U7UqrPlArT/B7VmImRibJD0Zrg==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "UpfPebXQtHGrWz21+YLHmJSm+5zsuPE9U9pfdCtoB+67g75fDmWlNgpkH2ZmdVhSwkjNIed9Icg8Iu63z2ei5Q==", + "dependencies": { + "Microsoft.Testing.Platform": "2.1.0" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.0.1", + "contentHash": "uDJKAEjFTaa2wHdWlfo6ektyoh+WD4/Eesrwb4FpBFKsLGehhACVnwwTI4qD3FrIlIEPlxdXg3SyrYRIcO+RRQ==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.0.1", + "Newtonsoft.Json": "13.0.3" + } + }, + "MSTest.Analyzers": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "4ElL/aqomiUInr090VN4udqz46AuszXLrifHkLrgj0zb7na8eAoyUQt3BwDLTcGd1bSkmk3SfD02rZtKU+ZiqQ==" + }, + "MSTest.TestAdapter": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "bRW1Hftwq0XbcVExcAbj4YAfSZDRAziL0mygDkPBvaUe2nSsWFQIatze5lHVjPFJMvSFgWnItku4pguIy5FowQ==", + "dependencies": { + "MSTest.TestFramework": "4.1.0", + "Microsoft.Testing.Extensions.VSTestBridge": "2.1.0", + "Microsoft.Testing.Platform.MSBuild": "2.1.0" + } + }, + "MSTest.TestFramework": { + "type": "Transitive", + "resolved": "4.1.0", + "contentHash": "BzpvsK+CRbk6khwY62h+7HfYzIxtJXyPv9tOI9T90cy5CVy+WI1JkN4ZaNL4Dobqb6dywSwabLTIbPZKpdrr+A==", + "dependencies": { + "MSTest.Analyzers": "4.1.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "7mS/oZFF8S6xyqGQfMU1btp0nXJQUPWV535Vp/XMLYwRAUv36xQN+U4vufWBF1+z4HnRTOwuFHtUSGnHbyN6FQ==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.0" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "vk5OGdf6K9kQScCWo3bRjhDWCv6Pqw92IpX4dlARZ8B1WL7/2NGTDtCkkw42eQf7UdwyoHKzVvMH/PtL8d6z7w==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.15.0", + "contentHash": "OnuSUlRpGvowkOzGFQfy+KZFu0cITfKfh2IYJJiZskxVJiOuexwOOuvfDAgpJdmTzVWAHjYdz2shcHZaJ06UjQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.15.0" + } + }, + "Polly.Core": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" + }, + "Polly.Extensions": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Polly.Core": "8.4.2" + } + }, + "Polly.RateLimiting": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", + "dependencies": { + "Polly.Core": "8.4.2", + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Serilog": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "SharpZipLib": { + "type": "Transitive", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "SSH.NET": { + "type": "Transitive", + "resolved": "2025.1.0", + "contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==", + "dependencies": { + "BouncyCastle.Cryptography": "2.6.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.3" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "wugvy+pBVzjQEnRs9wMTWwoaeNFX3hsaHeVHFDIvJSWXp7wfmNWu3mxAwBIE6pyW+g6+rHa1Of5fTzb0QVqUTA==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "Testcontainers": { + "type": "Transitive", + "resolved": "4.11.0", + "contentHash": "9pBNaK9Ra3GVnr5h6gaDJOBH0txA5G3Juho5WANPuyu38l5xyr2lCvf11oA5/uSd+Lh4Wtng34nKp3nMiea02g==", + "dependencies": { + "Docker.DotNet.Enhanced": "3.131.1", + "Docker.DotNet.Enhanced.X509": "3.131.1", + "Microsoft.Extensions.Logging.Abstractions": "8.0.3", + "SSH.NET": "2025.1.0", + "SharpZipLib": "1.4.2" + } + }, + "werkr.api": { + "type": "Project", + "dependencies": { + "Grpc.AspNetCore": "[2.76.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.5, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.5, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.16.0, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "Serilog.Sinks.File": "[7.0.0, )", + "Serilog.Sinks.OpenTelemetry": "[4.2.0, )", + "Werkr.Common": "[1.0.0, )", + "Werkr.Core": "[1.0.0, )", + "Werkr.Data": "[1.0.0, )", + "Werkr.ServiceDefaults": "[1.0.0, )" + } + }, + "werkr.common": { + "type": "Project", + "dependencies": { + "Google.Protobuf": "[3.34.0, )", + "Microsoft.AspNetCore.Authorization": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Json": "[10.0.5, )", + "Microsoft.IdentityModel.Tokens": "[8.16.0, )", + "Werkr.Common.Configuration": "[1.0.0, )" + } + }, + "werkr.common.configuration": { + "type": "Project" + }, + "werkr.core": { + "type": "Project", + "dependencies": { + "Grpc.Net.Client": "[2.76.0, )", + "Microsoft.Extensions.Hosting.Abstractions": "[10.0.5, )", + "System.Security.Cryptography.ProtectedData": "[10.0.5, )", + "Werkr.Common": "[1.0.0, )", + "Werkr.Data": "[1.0.0, )" + } + }, + "werkr.data": { + "type": "Project", + "dependencies": { + "EFCore.NamingConventions": "[10.0.1, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "Werkr.Common": "[1.0.0, )" + } + }, + "werkr.data.identity": { + "type": "Project", + "dependencies": { + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.AspNetCore.Identity.UI": "[10.0.5, )", + "Werkr.Common": "[1.0.0, )", + "Werkr.Data": "[1.0.0, )" + } + }, + "werkr.servicedefaults": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Http.Resilience": "[10.4.0, )", + "Microsoft.Extensions.ServiceDiscovery": "[10.4.0, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.0, )", + "OpenTelemetry.Extensions.Hosting": "[1.15.0, )" + } + }, + "EFCore.NamingConventions": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + } + }, + "Google.Protobuf": { + "type": "CentralTransitive", + "requested": "[3.34.0, )", + "resolved": "3.34.0", + "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" + }, + "Grpc.AspNetCore": { + "type": "CentralTransitive", + "requested": "[2.76.0, )", + "resolved": "2.76.0", + "contentHash": "LyXMmpN2Ba0TE35SOLSKbGqIYtJuhc1UgiaGfoW1X8KJERV70QI5KGW+ckEY7MrXoFWN/uWo4B70siVhbDmCgQ==", + "dependencies": { + "Google.Protobuf": "3.31.1", + "Grpc.AspNetCore.Server.ClientFactory": "2.76.0", + "Grpc.Tools": "2.76.0" + } + }, + "Grpc.Net.Client": { + "type": "CentralTransitive", + "requested": "[2.76.0, )", + "resolved": "2.76.0", + "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==", + "dependencies": { + "Grpc.Net.Common": "2.76.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "Grpc.Net.ClientFactory": { + "type": "CentralTransitive", + "requested": "[2.76.0, )", + "resolved": "2.76.0", + "contentHash": "XI+kO69L9AV8B9N0UQOmH911r6MOEp9huHiavEsY56DJYuzJ9KAxNGy37dpV6CLbgCaN2uKmpOsZ9Pao6bmpVQ==", + "dependencies": { + "Grpc.Net.Client": "2.76.0", + "Microsoft.Extensions.Http": "8.0.0" + } + }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "oo1uauTwgcnhYgituZ2nI3wJ8XN9z76ggu2zkOJu1BCfOOsqr+g08Kr4MOiMuXEhwySkpIV+MVoC25hC1288NA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Identity.Stores": "10.0.5" + } + }, + "Microsoft.AspNetCore.Identity.UI": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "qzYhpJ4Uxng18hmuKqwqydZaPzItrv9WOwNULJ2ka952TZKlOQkERTSkVO8G/19WiRtoznZatrcRyOvppYRGFA==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Embedded": "10.0.5", + "Microsoft.Extensions.Identity.Stores": "10.0.5" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==", + "dependencies": { + "Microsoft.OpenApi": "2.0.0" + } + }, + "Microsoft.Data.Sqlite.Core": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Http.Resilience": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "HbkUsPUC7vLy2TaDbdA9aooW64n9yX4sUppRuiJ1cOzzU1FUW+MVEotm6kYVq6AuUI9xwFSBhRFzA03blmk3VA==", + "dependencies": { + "Microsoft.Extensions.Http.Diagnostics": "10.4.0", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Resilience": "10.4.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.5", + "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.ServiceDiscovery": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "RznZAH6L4RNvroECT5JpqfFQJjHTn+8N7+ThSgYutbshkuymFeL/uBIZt1CM8LOdpPPhn4//a5fLUah9/k7ayQ==", + "dependencies": { + "Microsoft.Extensions.Http": "10.0.4", + "Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.4.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "CentralTransitive", + "requested": "[8.16.0, )", + "resolved": "8.16.0", + "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.IdentityModel.Logging": "8.16.0" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "VH8ANc/js9IRvfYt0Q2UaAxNCOWm+IU+vWrtoH7pfx4oWPVdISUt+9uWfBCFMWZg5WzQip5dhslyDjeyZXXfSQ==", + "dependencies": { + "OpenTelemetry": "1.15.0" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "RixjKyB1pbYGhWdvPto4KJs+exdQknJsnjUO9WszdLles5Vcd0EYzxPNJdwmLjYfP+Jfbr4B5nktM4ZgeHSWtg==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.15.0" + } + }, + "Serilog.AspNetCore": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "Serilog.Sinks.OpenTelemetry": { + "type": "CentralTransitive", + "requested": "[4.2.0, )", + "resolved": "4.2.0", + "contentHash": "PzMCyE5G19tjr5IZEi5qg+4UU5QrxBEoBEMu/hhYybTrGKXqUDiSGWKZNUDBgelaVKqLADlsmlJVyKce5SyPrg==", + "dependencies": { + "Google.Protobuf": "3.30.1", + "Grpc.Net.Client": "2.70.0", + "Serilog": "4.2.0" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "CentralTransitive", + "requested": "[8.16.0, )", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "System.Security.Cryptography.ProtectedData": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg==" + } + } + } } \ No newline at end of file diff --git a/src/Werkr.Agent/Communication/AgentGrpcClientFactory.cs b/src/Werkr.Agent/Communication/AgentGrpcClientFactory.cs index 651631f..b270e9e 100644 --- a/src/Werkr.Agent/Communication/AgentGrpcClientFactory.cs +++ b/src/Werkr.Agent/Communication/AgentGrpcClientFactory.cs @@ -16,9 +16,11 @@ namespace Werkr.Agent.Communication; /// Provides typed client accessors and with bearer token authentication. ///
/// Factory for creating DI scopes to resolve . +/// Application configuration for optional URL overrides. /// Logger instance. public sealed partial class AgentGrpcClientFactory( IServiceScopeFactory scopeFactory, + IConfiguration configuration, ILogger logger ) : IDisposable { /// @@ -114,7 +116,11 @@ public CallOptions CreateCallOptions( }; TimeSpan effectiveTimeout = timeout ?? TimeSpan.FromMinutes( 5 ); - DateTime deadline = DateTime.UtcNow + effectiveTimeout; + + // Infinite or non-positive timeouts mean no deadline (used by output streaming). + DateTime? deadline = effectiveTimeout <= TimeSpan.Zero + ? null + : DateTime.UtcNow + effectiveTimeout; return new CallOptions( headers: metadata, @@ -202,15 +208,30 @@ _connection is not null && _connection = await ResolveConnectionAsync( ct ); + // Prefer explicit API URL from config/environment (e.g. Aspire-injected + // Werkr__ApiUrl) over the stored registration URL, which may be stale after + // port changes or Aspire restarts. + string targetUrl = _connection.RemoteUrl; + string? apiUrlOverride = configuration["Werkr:ApiUrl"]; + if (!string.IsNullOrWhiteSpace( apiUrlOverride )) { + if (logger.IsEnabled( LogLevel.Information ) + && !string.Equals( apiUrlOverride, targetUrl, StringComparison.OrdinalIgnoreCase )) { + logger.LogInformation( + "Overriding stored RemoteUrl {StoredUrl} with configured ApiUrl {OverrideUrl}.", + targetUrl, apiUrlOverride ); + } + targetUrl = apiUrlOverride; + } + _channel?.Dispose( ); - _channel = GrpcChannel.ForAddress( _connection.RemoteUrl, new GrpcChannelOptions { + _channel = GrpcChannel.ForAddress( targetUrl, new GrpcChannelOptions { HttpHandler = CreateHttpHandler( ) } ); if (logger.IsEnabled( LogLevel.Information )) { logger.LogInformation( "Created gRPC channel to Server at {Url} (Connection: {ConnectionId}).", - _connection.RemoteUrl, _connection.Id.ToString( ) ); + targetUrl, _connection.Id.ToString( ) ); } } finally { _ = _initLock.Release( ); diff --git a/src/Werkr.Agent/Operators/SystemShellOperator.cs b/src/Werkr.Agent/Operators/SystemShellOperator.cs index 816b515..8703999 100644 --- a/src/Werkr.Agent/Operators/SystemShellOperator.cs +++ b/src/Werkr.Agent/Operators/SystemShellOperator.cs @@ -150,6 +150,11 @@ CancellationToken cancellationToken await process.WaitForExitAsync( cancellationToken ); + // The synchronous WaitForExit() ensures all redirected stdout/stderr + // events have been fully processed before we read ExitCode or + // complete the channel writer. + process.WaitForExit( ); + int exitCode = process.ExitCode; if (exitCode != 0) { diff --git a/src/Werkr.Agent/Scheduling/WorkflowExecutionService.cs b/src/Werkr.Agent/Scheduling/WorkflowExecutionService.cs index c0ae0ea..e596ba5 100644 --- a/src/Werkr.Agent/Scheduling/WorkflowExecutionService.cs +++ b/src/Werkr.Agent/Scheduling/WorkflowExecutionService.cs @@ -29,6 +29,7 @@ namespace Werkr.Agent.Scheduling; /// Built-in action operator. /// Factory for creating outbound gRPC clients to the Server. /// Client for workflow variable get/set/create operations. +/// Manages real-time output streaming to the server. /// Factory for creating DI scopes to resolve scoped services (e.g. WerkrDbContext). /// Logger. public sealed partial class WorkflowExecutionService( @@ -40,6 +41,7 @@ public sealed partial class WorkflowExecutionService( IActionOperator actionOperator, AgentGrpcClientFactory clientFactory, VariableClient variableClient, + Werkr.Agent.Services.OutputStreamingService outputStreamingService, IServiceScopeFactory serviceScopeFactory, ILogger logger ) { @@ -132,6 +134,7 @@ CancellationToken ct Dictionary branchTaken = []; bool workflowFailed = false; + long? failedStepId = null; try { foreach (IReadOnlyList level in levels) { @@ -166,6 +169,7 @@ CancellationToken ct "Workflow run {RunId} failed at step {StepId}: {Error}.", workflowRunId, result.StepId, result.ErrorMessage ); workflowFailed = true; + failedStepId ??= result.StepId; } } } @@ -190,6 +194,7 @@ CancellationToken ct "Workflow run {RunId} failed at step {StepId}: {Error}.", workflowRunId, result.StepId, result.ErrorMessage ); workflowFailed = true; + failedStepId ??= result.StepId; break; } } @@ -208,7 +213,11 @@ CancellationToken ct throw; // Propagate shutdown } catch (Exception ex) { logger.LogError( ex, "Workflow run {RunId} failed with unexpected error.", workflowRunId ); + workflowFailed = true; } + + // Report workflow run completion/failure to the server + await CompleteWorkflowRunAsync( workflowRunId, !workflowFailed, failedStepId, ct ); } // ── Step Execution ─────────────────────────────────────────────────────────── @@ -248,9 +257,16 @@ CancellationToken ct logger.LogDebug( "Step {StepId} '{StepLabel}' skipped by control flow ({ControlStatement}).", step.StepId, stepLabel, (ControlStatement)step.ControlStatement ); } + + string skipReason = $"Control flow: {(ControlStatement) step.ControlStatement}"; + await ReportStepSkippedAsync( workflowRunId, step.StepId, taskDef.Name, skipReason, ct ); + return StepExecutionResult.Skipped( step.StepId ); } + // Report step started to the server + await ReportStepStartedAsync( workflowRunId, step.StepId, taskDef.Name, taskDef.TaskId, ct ); + // Resolve input variable from cache string? inputVariableValue = null; if (!string.IsNullOrWhiteSpace( step.InputVariableName ) @@ -270,7 +286,7 @@ CancellationToken ct // Execute the step's task locally StepJobResult job = await ExecuteStepTaskAsync( - taskDef, workflowRunId, scheduleId, inputVariableValue, outputVariableName, ct); + taskDef, workflowRunId, step.StepId, scheduleId, inputVariableValue, outputVariableName, ct); // Push output variable to server and local cache if (outputVariableName is not null && job.OutputVariableValue is not null) { @@ -339,7 +355,7 @@ CancellationToken ct } lastJob = await ExecuteStepTaskAsync( - taskDef, workflowRunId, scheduleId, inputVariableValue, outputVariableName, ct ); + taskDef, workflowRunId, step.StepId, scheduleId, inputVariableValue, outputVariableName, ct ); iterations++; // Push output variable after each iteration @@ -372,6 +388,7 @@ await variableClient.PushVariableAsync( workflowRunId, outputVariableName, private async Task ExecuteStepTaskAsync( ScheduledTaskDefinition taskDef, Guid workflowRunId, + long stepId, Guid? scheduleId, string? inputVariableValue, string? outputVariableName, @@ -428,6 +445,21 @@ or TaskActionType.ShellCommand await foreach (OperatorOutput output in execution.Output.WithCancellation( timeoutCts.Token )) { await outputWriter.WriteLineAsync( jobId, output, timeoutCts.Token ); collectedOutput.Add( output ); + + // Publish to output streaming service with workflow context + string scheduleIdStr = scheduleId?.ToString( ) ?? ""; + outputStreamingService.Publish( new OutputMessage { + TaskId = taskDef.TaskId, + ScheduleId = scheduleIdStr, + JobId = jobId.ToString( ), + Line = new OutputLine { + Text = output.Message, + LogLevel = output.LogLevel, + Timestamp = output.Timestamp, + }, + WorkflowRunId = workflowRunId.ToString( ), + StepId = stepId, + } ); } IOperatorResult result = await execution.Result; @@ -489,11 +521,26 @@ or TaskActionType.ShellCommand await PersistJobLocallyAsync( jobId, taskDef.TaskId, taskDef.Content, startTime, endTime, success, exitCode, errorCategory, tailPreview, workflowRunId.ToString( ), scheduleId, ct ); - // Report result to server with workflowRunId, schedule ID, and output variable info + // Report result to server with workflowRunId, schedule ID, step ID, and output variable info await ReportJobResultAsync( jobId, taskDef, startTime, endTime, success, exitCode, errorCategory, workflowRunId.ToString( ), tailPreview, scheduleId, - outputVariableName, outputVariableValue, ct ); + stepId, outputVariableName, outputVariableValue, ct ); + + // Publish completion to output streaming service with workflow context + string completeScheduleIdStr = scheduleId?.ToString( ) ?? ""; + outputStreamingService.Publish( new OutputMessage { + TaskId = taskDef.TaskId, + ScheduleId = completeScheduleIdStr, + JobId = jobId.ToString( ), + Complete = new OutputComplete { + ExitCode = exitCode, + Success = success, + ErrorMessage = executionException?.Message ?? "", + }, + WorkflowRunId = workflowRunId.ToString( ), + StepId = stepId, + } ); return new StepJobResult( jobId, success, exitCode, startTime, endTime, errorCategory, tailPreview, outputVariableValue ); } @@ -786,6 +833,7 @@ private async Task ReportJobResultAsync( string? workflowRunId, string? outputPreview, Guid? scheduleId, + long stepId, string? outputVariableName, string? outputVariableValue, CancellationToken ct @@ -805,6 +853,7 @@ CancellationToken ct ErrorCategory = (int) errorCategory, OutputPath = AgentJobOutputWriter.GetRelativeOutputPath( jobId ), JobId = jobId.ToString( ), + StepId = stepId, }; if (!string.IsNullOrWhiteSpace( workflowRunId )) { innerRequest.WorkflowRunId = workflowRunId; @@ -853,4 +902,121 @@ CancellationToken ct } } } + + // ── Step Lifecycle Reporting ────────────────────────────────────────────────── + + /// + /// Reports that a workflow step has started execution. + /// + private async Task ReportStepStartedAsync( + Guid workflowRunId, long stepId, string stepName, long taskId, CancellationToken ct + ) { + try { + JobReporting.JobReportingClient client = await clientFactory.CreateJobReportingClientAsync( ct ); + + StepStartedRequest inner = new( ) { + ConnectionId = ( await clientFactory.GetConnectionAsync( ct ) ).Id.ToString( ), + WorkflowRunId = workflowRunId.ToString( ), + StepId = stepId, + StepName = stepName, + TaskId = taskId, + StartTime = DateTime.UtcNow.ToString( "o" ), + }; + + EncryptedEnvelope envelope = PayloadEncryptor.EncryptToEnvelope( + inner, clientFactory.GetSharedKey( ), clientFactory.GetKeyId( ) ); + + Grpc.Core.CallOptions callOptions = clientFactory.CreateCallOptions( cancellationToken: ct ); + _ = await client.ReportStepStartedAsync( envelope, callOptions ); + } catch (Exception ex) { + logger.LogWarning( ex, "Failed to report step started for step {StepId} in run {RunId}.", + stepId, workflowRunId ); + } + } + + /// + /// Reports that a workflow step was skipped by control flow evaluation. + /// + private async Task ReportStepSkippedAsync( + Guid workflowRunId, long stepId, string stepName, string reason, CancellationToken ct + ) { + try { + JobReporting.JobReportingClient client = await clientFactory.CreateJobReportingClientAsync( ct ); + + StepSkippedRequest inner = new( ) { + ConnectionId = ( await clientFactory.GetConnectionAsync( ct ) ).Id.ToString( ), + WorkflowRunId = workflowRunId.ToString( ), + StepId = stepId, + StepName = stepName, + Reason = reason, + }; + + EncryptedEnvelope envelope = PayloadEncryptor.EncryptToEnvelope( + inner, clientFactory.GetSharedKey( ), clientFactory.GetKeyId( ) ); + + Grpc.Core.CallOptions callOptions = clientFactory.CreateCallOptions( cancellationToken: ct ); + _ = await client.ReportStepSkippedAsync( envelope, callOptions ); + } catch (Exception ex) { + logger.LogWarning( ex, "Failed to report step skipped for step {StepId} in run {RunId}.", + stepId, workflowRunId ); + } + } + + /// + /// Reports workflow run completion or failure to the server. + /// Retries up to 3 times with exponential backoff on transient failure. + /// + private async Task CompleteWorkflowRunAsync( + Guid workflowRunId, bool success, long? failedStepId, CancellationToken ct + ) { + TimeSpan delay = s_reportRetryBaseDelay; + for (int attempt = 1; attempt <= ReportMaxRetries; attempt++) { + try { + VariableService.VariableServiceClient client = + await clientFactory.CreateVariableServiceClientAsync( ct ); + Data.Entities.Registration.RegisteredConnection connection = + await clientFactory.GetConnectionAsync( ct ); + + CompleteWorkflowRunRequest inner = new( ) { + ConnectionId = connection.Id.ToString( ), + WorkflowRunId = workflowRunId.ToString( ), + Success = success, + EndTime = DateTime.UtcNow.ToString( "o" ), + }; + + if (failedStepId.HasValue) { + inner.FailedStepId = failedStepId.Value; + } + + EncryptedEnvelope envelope = PayloadEncryptor.EncryptToEnvelope( + inner, clientFactory.GetSharedKey( ), clientFactory.GetKeyId( ) ); + + Grpc.Core.CallOptions callOptions = clientFactory.CreateCallOptions( cancellationToken: ct ); + EncryptedEnvelope responseEnvelope = await client.CompleteWorkflowRunAsync( envelope, callOptions ); + CompleteWorkflowRunResponse response = PayloadEncryptor.DecryptFromEnvelope( + responseEnvelope, clientFactory.GetSharedKey( ) ); + + if (response.Accepted) { + if (logger.IsEnabled( LogLevel.Information )) { + logger.LogInformation( + "Workflow run {RunId} completion reported (success={Success}).", + workflowRunId, success ); + } + } else { + logger.LogWarning( "Server rejected CompleteWorkflowRun for run {RunId}.", workflowRunId ); + } + return; + } catch (Exception ex) when (attempt < ReportMaxRetries && !ct.IsCancellationRequested) { + logger.LogWarning( ex, + "Failed to report workflow run completion for {RunId} (attempt {Attempt}/{MaxRetries}). Retrying in {Delay}.", + workflowRunId, attempt, ReportMaxRetries, delay ); + await Task.Delay( delay, ct ); + delay *= 2; + } catch (Exception ex) { + logger.LogError( ex, + "Failed to report workflow run completion for {RunId} after {MaxRetries} attempts.", + workflowRunId, ReportMaxRetries ); + } + } + } } diff --git a/src/Werkr.Agent/Services/OutputStreamingService.cs b/src/Werkr.Agent/Services/OutputStreamingService.cs index 8ee13ac..3cdb58c 100644 --- a/src/Werkr.Agent/Services/OutputStreamingService.cs +++ b/src/Werkr.Agent/Services/OutputStreamingService.cs @@ -106,25 +106,50 @@ private async Task MaintainStreamAsync( CancellationToken ct ) { try { Common.Protos.OutputStreamingService.OutputStreamingServiceClient client = await clientFactory.CreateOutputStreamingClientAsync( ct ); - CallOptions callOptions = clientFactory.CreateCallOptions( cancellationToken: ct ); + CallOptions callOptions = clientFactory.CreateCallOptions( + timeout: Timeout.InfiniteTimeSpan, cancellationToken: ct ); using AsyncDuplexStreamingCall call = client.StreamOutput( callOptions ); - // Reset backoff on successful connect + // Use a per-stream token so we can cancel the writer when the + // reader detects a disconnect (and vice-versa). + using CancellationTokenSource streamCts = + CancellationTokenSource.CreateLinkedTokenSource( ct ); + + Task readTask = ReadSubscriptionsAsync( call.ResponseStream, streamCts.Token ); + Task writeTask = WriteOutputAsync( call.RequestStream, streamCts.Token ); + + // Give the reader a moment to fail on immediate connection errors + // (e.g. "Connection refused") before declaring success. + Task settled = await Task.WhenAny( readTask, Task.Delay( 1000, streamCts.Token ) ); + if (settled == readTask && readTask.IsFaulted) { + await readTask; // propagate the connection error + } + + // Connection confirmed — reset backoff. delay = TimeSpan.FromSeconds( 2 ); if (logger.IsEnabled( LogLevel.Information )) { logger.LogInformation( "Output streaming connected to server." ); } - // Read subscriptions in the background - Task readTask = ReadSubscriptionsAsync( call.ResponseStream, ct ); + // Wait for either side to complete or fail. Without WhenAny a + // reader failure goes undetected while the writer blocks on an + // empty outbound channel, leaving the stream in a zombie state. + Task completed = await Task.WhenAny( readTask, writeTask ); + await streamCts.CancelAsync( ); - // Write outbound messages - await WriteOutputAsync( call.RequestStream, ct ); + // Drain the peer task so it doesn't leak as unobserved. + Task peer = completed == readTask ? writeTask : readTask; + try { await peer; } catch (Exception peerEx) { + logger.LogDebug( peerEx, "Peer stream task ended during reconnection." ); + } - await readTask; + // Re-throw the original failure for the reconnection catch block. + // If the read side completed normally (server closed the stream), + // this falls through and the outer loop reconnects. + await completed; } catch (OperationCanceledException) when (ct.IsCancellationRequested) { break; } catch (RpcException ex) when (ct.IsCancellationRequested) { @@ -133,6 +158,11 @@ private async Task MaintainStreamAsync( CancellationToken ct ) { } catch (Exception ex) { logger.LogWarning( ex, "Output stream disconnected. Reconnecting in {Delay}s.", delay.TotalSeconds ); _subscriptions.Clear( ); + + // Force the gRPC channel to be recreated on the next attempt so + // stale URLs (from old ports or transient failures) are discarded. + clientFactory.Reset( ); + try { await Task.Delay( delay, ct ); } catch (OperationCanceledException) { diff --git a/src/Werkr.Agent/packages.lock.json b/src/Werkr.Agent/packages.lock.json index b563b9c..eeef698 100644 --- a/src/Werkr.Agent/packages.lock.json +++ b/src/Werkr.Agent/packages.lock.json @@ -221,30 +221,30 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw==" + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA==" + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4" + "Microsoft.EntityFrameworkCore": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.4", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", "SQLitePCLRaw.core": "2.1.11" } }, @@ -265,8 +265,8 @@ }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg==" + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" }, "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { "type": "Transitive", @@ -502,8 +502,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==" + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==" }, "OpenTelemetry": { "type": "Transitive", @@ -978,7 +978,7 @@ "type": "Project", "dependencies": { "Grpc.Net.Client": "[2.76.0, )", - "System.Security.Cryptography.ProtectedData": "[10.0.4, )", + "System.Security.Cryptography.ProtectedData": "[10.0.5, )", "Werkr.Common": "[1.0.0, )", "Werkr.Data": "[1.0.0, )" } @@ -987,9 +987,9 @@ "type": "Project", "dependencies": { "EFCore.NamingConventions": "[10.0.1, )", - "Microsoft.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", "Werkr.Common": "[1.0.0, )" } }, @@ -1039,30 +1039,30 @@ "Microsoft.Data.Sqlite.Core": { "type": "CentralTransitive", "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", "dependencies": { "SQLitePCLRaw.core": "2.1.11" } }, "Microsoft.EntityFrameworkCore": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4" + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", "SQLitePCLRaw.core": "2.1.11" } @@ -1097,13 +1097,13 @@ }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)", - "Npgsql": "10.0.0" + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" } }, "OpenTelemetry.Exporter.OpenTelemetryProtocol": { @@ -1126,9 +1126,9 @@ }, "System.Security.Cryptography.ProtectedData": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg==" } } } diff --git a/src/Werkr.Api/Endpoints/EventEndpoints.cs b/src/Werkr.Api/Endpoints/EventEndpoints.cs index dc717f9..a966c18 100644 --- a/src/Werkr.Api/Endpoints/EventEndpoints.cs +++ b/src/Werkr.Api/Endpoints/EventEndpoints.cs @@ -5,9 +5,7 @@ namespace Werkr.Api.Endpoints; /// -/// Maps Server-Sent Events (SSE) endpoints for real-time job event streaming. -/// Clients (e.g. the Blazor Server) subscribe to /api/events/jobs to receive -/// live notifications when agents report job completions. +/// Maps Server-Sent Events (SSE) endpoints for real-time job and workflow event streaming. /// internal static class EventEndpoints { @@ -15,7 +13,7 @@ internal static class EventEndpoints { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; - /// Maps the /api/events/jobs SSE endpoint. + /// Maps all SSE endpoints. public static WebApplication MapEventEndpoints( this WebApplication app ) { _ = app.MapGet( "/api/events/jobs", async ( JobEventBroadcaster broadcaster, @@ -47,6 +45,77 @@ CancellationToken ct .RequireAuthorization( Policies.CanRead ) .ExcludeFromDescription( ); + _ = app.MapGet( "/api/events/workflow-runs", async ( + WorkflowEventBroadcaster broadcaster, + HttpContext httpContext, + CancellationToken ct + ) => { + httpContext.Response.ContentType = "text/event-stream"; + httpContext.Response.Headers.CacheControl = "no-cache"; + httpContext.Response.Headers.Connection = "keep-alive"; + + using WorkflowEventSubscription subscription = broadcaster.Subscribe( ); + + try { + await foreach (WorkflowEvent workflowEvent in subscription.Reader.ReadAllAsync( ct )) { + string eventType = GetEventType( workflowEvent ); + string json = JsonSerializer.Serialize( workflowEvent, workflowEvent.GetType( ), s_jsonOptions ); + + await httpContext.Response.WriteAsync( $"event: {eventType}\n", ct ); + await httpContext.Response.WriteAsync( $"data: {json}\n\n", ct ); + await httpContext.Response.Body.FlushAsync( ct ); + } + } catch (OperationCanceledException) { + // Client disconnected — normal SSE lifecycle. + } + } ) + .WithName( "StreamWorkflowEvents" ) + .RequireAuthorization( Policies.CanRead ) + .ExcludeFromDescription( ); + + _ = app.MapGet( "/api/workflows/runs/{runId:guid}/events", async ( + Guid runId, + WorkflowEventBroadcaster broadcaster, + HttpContext httpContext, + CancellationToken ct + ) => { + httpContext.Response.ContentType = "text/event-stream"; + httpContext.Response.Headers.CacheControl = "no-cache"; + httpContext.Response.Headers.Connection = "keep-alive"; + + using WorkflowEventSubscription subscription = broadcaster.Subscribe( ); + + try { + await foreach (WorkflowEvent workflowEvent in subscription.Reader.ReadAllAsync( ct )) { + if (workflowEvent.WorkflowRunId != runId) { + continue; + } + + string eventType = GetEventType( workflowEvent ); + string json = JsonSerializer.Serialize( workflowEvent, workflowEvent.GetType( ), s_jsonOptions ); + + await httpContext.Response.WriteAsync( $"event: {eventType}\n", ct ); + await httpContext.Response.WriteAsync( $"data: {json}\n\n", ct ); + await httpContext.Response.Body.FlushAsync( ct ); + } + } catch (OperationCanceledException) { + // Client disconnected — normal SSE lifecycle. + } + } ) + .WithName( "StreamWorkflowRunEvents" ) + .RequireAuthorization( Policies.CanRead ) + .ExcludeFromDescription( ); + return app; } + + private static string GetEventType( WorkflowEvent workflowEvent ) => workflowEvent switch { + StepStartedEvent => "step-started", + StepCompletedEvent => "step-completed", + StepFailedEvent => "step-failed", + StepSkippedEvent => "step-skipped", + RunCompletedEvent => "run-completed", + LogAppendedEvent => "log-appended", + _ => "unknown", + }; } diff --git a/src/Werkr.Api/Endpoints/FilterEndpoints.cs b/src/Werkr.Api/Endpoints/FilterEndpoints.cs new file mode 100644 index 0000000..514a50a --- /dev/null +++ b/src/Werkr.Api/Endpoints/FilterEndpoints.cs @@ -0,0 +1,246 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Werkr.Common.Auth; +using Werkr.Data; +using Werkr.Data.Entities.Settings; + +namespace Werkr.Api.Endpoints; + +/// Maps CRUD endpoints for server-synced saved filters. +internal static class FilterEndpoints { + + private static readonly HashSet s_validPageKeys = ["runs", "workflows", "jobs", "agents", "schedules", "tasks"]; + + /// Maps the saved-filter endpoints at /api/filters/{pageKey}. + public static WebApplication MapFilterEndpoints( this WebApplication app ) { + + // GET /api/filters/{pageKey} — list own + shared filters + _ = app.MapGet( "/api/filters/{pageKey}", async ( + string pageKey, + HttpContext httpContext, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + if (!s_validPageKeys.Contains( pageKey )) { + return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); + } + + string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; + if (string.IsNullOrWhiteSpace( userId )) { + return Results.Unauthorized( ); + } + + List filters = await dbContext.SavedFilters + .AsNoTracking( ) + .Where( f => f.PageKey == pageKey && ( f.OwnerId == userId || f.IsShared ) ) + .OrderBy( f => f.Name ) + .ToListAsync( ct ); + + List dtos = [.. filters.Select( f => new { + f.Id, + f.Name, + f.PageKey, + f.CriteriaJson, + f.IsShared, + IsOwner = f.OwnerId == userId + } )]; + + return Results.Ok( dtos ); + } ) + .RequireAuthorization( Policies.CanRead ); + + // POST /api/filters/{pageKey} — create a filter + _ = app.MapPost( "/api/filters/{pageKey}", async ( + string pageKey, + CreateFilterRequest request, + HttpContext httpContext, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + if (!s_validPageKeys.Contains( pageKey )) { + return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); + } + + string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; + if (string.IsNullOrWhiteSpace( userId )) { + return Results.Unauthorized( ); + } + + if (string.IsNullOrWhiteSpace( request.Name )) { + return Results.BadRequest( new { message = "Name is required." } ); + } + + if (!IsValidJson( request.CriteriaJson )) { + return Results.BadRequest( new { message = "CriteriaJson must be valid JSON." } ); + } + + if (request.CriteriaJson.Length > 4096) { + return Results.BadRequest( new { message = "CriteriaJson must not exceed 4 KB." } ); + } + + DateTime now = DateTime.UtcNow; + SavedFilter entity = new( ) { + OwnerId = userId, + PageKey = pageKey, + Name = request.Name.Trim( ), + CriteriaJson = request.CriteriaJson, + IsShared = false, + Created = now, + LastUpdated = now, + Version = 1, + }; + + _ = dbContext.SavedFilters.Add( entity ); + _ = await dbContext.SaveChangesAsync( ct ); + + return Results.Created( $"/api/filters/{pageKey}/{entity.Id}", new { + entity.Id, + entity.Name, + entity.PageKey, + entity.CriteriaJson, + entity.IsShared, + IsOwner = true + } ); + } ) + .RequireAuthorization( Policies.CanRead ); + + // PUT /api/filters/{pageKey}/{id} — update own filter + _ = app.MapPut( "/api/filters/{pageKey}/{id}", async ( + string pageKey, + long id, + UpdateFilterRequest request, + HttpContext httpContext, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + if (!s_validPageKeys.Contains( pageKey )) { + return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); + } + + string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; + if (string.IsNullOrWhiteSpace( userId )) { + return Results.Unauthorized( ); + } + + SavedFilter? entity = await dbContext.SavedFilters + .FirstOrDefaultAsync( f => f.Id == id && f.PageKey == pageKey, ct ); + + if (entity is null) { + return Results.NotFound( ); + } + + if (entity.OwnerId != userId) { + return Results.Forbid( ); + } + + if (!string.IsNullOrWhiteSpace( request.Name )) { + entity.Name = request.Name.Trim( ); + } + + if (request.CriteriaJson is not null) { + if (!IsValidJson( request.CriteriaJson )) { + return Results.BadRequest( new { message = "CriteriaJson must be valid JSON." } ); + } + if (request.CriteriaJson.Length > 4096) { + return Results.BadRequest( new { message = "CriteriaJson must not exceed 4 KB." } ); + } + entity.CriteriaJson = request.CriteriaJson; + } + + entity.LastUpdated = DateTime.UtcNow; + entity.Version++; + _ = await dbContext.SaveChangesAsync( ct ); + + return Results.Ok( new { + entity.Id, + entity.Name, + entity.PageKey, + entity.CriteriaJson, + entity.IsShared, + IsOwner = true + } ); + } ) + .RequireAuthorization( Policies.CanRead ); + + // DELETE /api/filters/{pageKey}/{id} — delete own filter + _ = app.MapDelete( "/api/filters/{pageKey}/{id}", async ( + string pageKey, + long id, + HttpContext httpContext, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + if (!s_validPageKeys.Contains( pageKey )) { + return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); + } + + string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; + if (string.IsNullOrWhiteSpace( userId )) { + return Results.Unauthorized( ); + } + + SavedFilter? entity = await dbContext.SavedFilters + .FirstOrDefaultAsync( f => f.Id == id && f.PageKey == pageKey, ct ); + + if (entity is null) { + return Results.NotFound( ); + } + + if (entity.OwnerId != userId) { + return Results.Forbid( ); + } + + _ = dbContext.SavedFilters.Remove( entity ); + _ = await dbContext.SaveChangesAsync( ct ); + + return Results.NoContent( ); + } ) + .RequireAuthorization( Policies.CanRead ); + + // PUT /api/filters/{pageKey}/{id}/share — toggle shared visibility (admin only) + _ = app.MapPut( "/api/filters/{pageKey}/{id}/share", async ( + string pageKey, + long id, + HttpContext httpContext, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + if (!s_validPageKeys.Contains( pageKey )) { + return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); + } + + SavedFilter? entity = await dbContext.SavedFilters + .FirstOrDefaultAsync( f => f.Id == id && f.PageKey == pageKey, ct ); + + if (entity is null) { + return Results.NotFound( ); + } + + entity.IsShared = !entity.IsShared; + entity.LastUpdated = DateTime.UtcNow; + entity.Version++; + _ = await dbContext.SaveChangesAsync( ct ); + + return Results.Ok( new { entity.Id, entity.IsShared } ); + } ) + .RequireAuthorization( Policies.IsAdmin ); + + return app; + } + + private static bool IsValidJson( string json ) { + try { + using JsonDocument doc = JsonDocument.Parse( json ); + return true; + } catch (JsonException) { + return false; + } + } +} + +/// Request body for creating a saved filter. +internal sealed record CreateFilterRequest( string Name, string CriteriaJson ); + +/// Request body for updating a saved filter. +internal sealed record UpdateFilterRequest( string? Name, string? CriteriaJson ); diff --git a/src/Werkr.Api/Endpoints/ShellEndpoints.cs b/src/Werkr.Api/Endpoints/ShellEndpoints.cs index 0b4732a..5bef7ed 100644 --- a/src/Werkr.Api/Endpoints/ShellEndpoints.cs +++ b/src/Werkr.Api/Endpoints/ShellEndpoints.cs @@ -51,14 +51,24 @@ CancellationToken ct (long taskId, Guid scheduleId) = await runNowService.CreateEphemeralTaskAsync( request.Command, actionType, agentTags, ct ); - // Push invalidation so the agent picks it up immediately - await invalidationDispatcher.InvalidateAsync( scheduleId, ct ); - - // Subscribe to the output stream from the agent + // Subscribe to the output stream BEFORE pushing invalidation so the + // agent's subscription is registered before execution can begin. string scheduleIdStr = scheduleId.ToString( ); Channel? channel = await outputStreaming.SubscribeAsync( taskId, scheduleIdStr ); + // If no agent stream is available, wait briefly for a reconnecting + // agent before falling back to the non-streaming 202 response. + if (channel is null) { + for (int retry = 0; retry < 3 && channel is null; retry++) { + await Task.Delay( 1000, ct ); + channel = await outputStreaming.SubscribeAsync( taskId, scheduleIdStr ); + } + } + + // Push invalidation so the agent picks it up immediately + await invalidationDispatcher.InvalidateAsync( scheduleId, ct ); + if (channel is null) { // No agent stream available yet — return 202 with IDs so client can poll httpContext.Response.StatusCode = StatusCodes.Status202Accepted; diff --git a/src/Werkr.Api/Endpoints/WorkflowEndpoints.cs b/src/Werkr.Api/Endpoints/WorkflowEndpoints.cs index 3a08260..7f3e95c 100644 --- a/src/Werkr.Api/Endpoints/WorkflowEndpoints.cs +++ b/src/Werkr.Api/Endpoints/WorkflowEndpoints.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; +using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore; using Werkr.Api.Models; using Werkr.Api.Services; @@ -17,7 +18,7 @@ namespace Werkr.Api.Endpoints; /// /// Extension methods for mapping workflow REST endpoints to the application. /// -internal static class WorkflowEndpoints { +internal static partial class WorkflowEndpoints { /// Maps all workflow-related REST endpoints. public static WebApplication MapWorkflowEndpoints( this WebApplication app ) { @@ -26,6 +27,7 @@ public static WebApplication MapWorkflowEndpoints( this WebApplication app ) { MapStepDependencies( app ); MapWorkflowSchedules( app ); MapWorkflowExecution( app ); + MapWorkflowDashboard( app ); return app; } @@ -84,6 +86,45 @@ CancellationToken ct CancellationToken ct ) => { try { + // Validate annotations + if (request.Annotations is { Count: > 50 }) { + return Results.BadRequest( new { message = "Maximum 50 annotations allowed." } ); + } + + if (request.Annotations is not null) { + string serialized = JsonSerializer.Serialize( request.Annotations ); + if (serialized.Length > 65536) { + return Results.BadRequest( new { message = "Annotations JSON exceeds 64 KB limit." } ); + } + + // Strip HTML tags from annotation text for XSS safety + List sanitized = []; + foreach (AnnotationDto ann in request.Annotations) { + string cleanText = StripHtmlTags( ann.Text ); + sanitized.Add( ann with { Text = cleanText } ); + } + // Validate annotation fields explicitly (DataAnnotations are not auto-enforced in Minimal APIs) + foreach (AnnotationDto ann in sanitized) { + if (ann.Text.Length > 500) { + return Results.BadRequest( new { message = $"Annotation text exceeds 500 characters (id: {ann.Id})." } ); + } + + if (ann.X < -10000 || ann.X > 50000 || ann.Y < -10000 || ann.Y > 50000) { + return Results.BadRequest( new { message = $"Annotation position out of bounds (id: {ann.Id})." } ); + } + + if (ann.Width < 80 || ann.Width > 800 || ann.Height < 40 || ann.Height > 600) { + return Results.BadRequest( new { message = $"Annotation dimensions out of bounds (id: {ann.Id})." } ); + } + + if (!HexColorRegex( ).IsMatch( ann.Color )) { + return Results.BadRequest( new { message = $"Annotation color must be a hex color (id: {ann.Id})." } ); + } + } + + request = request with { Annotations = sanitized }; + } + Workflow entity = WorkflowMapper.ToEntity( id, request ); Workflow updated = await workflowService.UpdateAsync( entity, ct ); return Results.Ok( WorkflowMapper.ToDto( updated ) ); @@ -205,6 +246,81 @@ CancellationToken ct } ) .WithName( "RemoveWorkflowStep" ) .RequireAuthorization( Policies.CanDelete ); + + _ = app.MapPost( "/api/workflows/{workflowId}/steps/batch", async ( + long workflowId, + WorkflowStepBatchRequest request, + WorkflowService workflowService, + Microsoft.AspNetCore.Http.HttpContext httpContext, + CancellationToken ct + ) => { + // Dynamic per-operation-type authorization + Microsoft.AspNetCore.Authorization.IAuthorizationService authService = + httpContext.RequestServices.GetRequiredService(); + + bool needsCreate = request.Operations.Any( o => + string.Equals( o.OperationType, "Add", StringComparison.OrdinalIgnoreCase ) ); + bool needsUpdate = request.Operations.Any( o => + string.Equals( o.OperationType, "Update", StringComparison.OrdinalIgnoreCase ) ); + bool needsDelete = request.Operations.Any( o => + string.Equals( o.OperationType, "Delete", StringComparison.OrdinalIgnoreCase ) ); + + // Check dependency changes too + foreach (StepBatchOperation op in request.Operations) { + if (op.DependencyChanges is null) { + continue; + } + + foreach (DependencyBatchItem dep in op.DependencyChanges) { + if (string.Equals( dep.OperationType, "Add", StringComparison.OrdinalIgnoreCase )) { + needsCreate = true; + } else if (string.Equals( dep.OperationType, "Delete", StringComparison.OrdinalIgnoreCase )) { + needsDelete = true; + } + } + } + + List missingPolicies = []; + if (needsCreate) { + Microsoft.AspNetCore.Authorization.AuthorizationResult result = + await authService.AuthorizeAsync( httpContext.User, null, Policies.CanCreate ); + if (!result.Succeeded) { + missingPolicies.Add( Policies.CanCreate ); + } + } + if (needsUpdate) { + Microsoft.AspNetCore.Authorization.AuthorizationResult result = + await authService.AuthorizeAsync( httpContext.User, null, Policies.CanUpdate ); + if (!result.Succeeded) { + missingPolicies.Add( Policies.CanUpdate ); + } + } + if (needsDelete) { + Microsoft.AspNetCore.Authorization.AuthorizationResult result = + await authService.AuthorizeAsync( httpContext.User, null, Policies.CanDelete ); + if (!result.Succeeded) { + missingPolicies.Add( Policies.CanDelete ); + } + } + + if (missingPolicies.Count > 0) { + return Results.Forbid( ); + } + + try { + WorkflowStepBatchResponse response = + await workflowService.BatchUpdateStepsAsync( workflowId, request, ct ); + return response.Success + ? Results.Ok( response ) + : Results.BadRequest( response ); + } catch (KeyNotFoundException) { + return Results.NotFound( ); + } catch (Exception ex) when (ex is FormatException or ArgumentException) { + return Results.BadRequest( new { message = ex.Message } ); + } + } ) + .WithName( "BatchUpdateWorkflowSteps" ) + .RequireAuthorization( Policies.CanRead ); } // ── Step Dependencies ── @@ -410,6 +526,29 @@ CancellationToken ct .WithName( "GetWorkflowRuns" ) .RequireAuthorization( Policies.CanRead ); + // Global workflow runs across all workflows (for the top-level Workflow Runs page) + _ = app.MapGet( "/api/workflows/runs", async ( + int? limit, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + List dtos = await dbContext.WorkflowRuns.AsNoTracking( ) + .Include( r => r.Workflow ) + .OrderByDescending( r => r.StartTime ) + .Take( limit ?? 50 ) + .Select( r => new WorkflowRunDto( + r.Id, + r.WorkflowId, + r.StartTime, + r.EndTime, + r.Status.ToString( ), + r.Workflow != null ? r.Workflow.Name : null ) ) + .ToListAsync( ct ); + return Results.Ok( dtos ); + } ) + .WithName( "GetAllWorkflowRuns" ) + .RequireAuthorization( Policies.CanRead ); + _ = app.MapGet( "/api/workflows/runs/{runId}", async ( Guid runId, WerkrDbContext dbContext, @@ -417,6 +556,7 @@ CancellationToken ct ) => { WorkflowRun? run = await dbContext.WorkflowRuns.AsNoTracking( ) .Include( r => r.Jobs ) + .Include( r => r.StepExecutions ) .FirstOrDefaultAsync( r => r.Id == runId, ct ); return run is null ? Results.NotFound( ) @@ -469,5 +609,298 @@ CancellationToken ct .WithName( "StreamWorkflowRunUpdates" ) .RequireAuthorization( Policies.CanRead ) .ExcludeFromDescription( ); + + // Step execution history for a run (all attempts) + _ = app.MapGet( "/api/workflows/runs/{runId:guid}/step-executions", async ( + Guid runId, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + List executions = await dbContext.WorkflowStepExecutions + .AsNoTracking( ) + .Where( e => e.WorkflowRunId == runId ) + .OrderBy( e => e.StepId ) + .ThenBy( e => e.Attempt ) + .ToListAsync( ct ); + return Results.Ok( executions.Select( WorkflowMapper.ToStepExecutionDto ) ); + } ) + .WithName( "GetStepExecutions" ) + .RequireAuthorization( Policies.CanRead ); + + _ = app.MapPost( "/api/workflows/{id}/runs/{runId:guid}/retry-from/{stepId:long}", async ( + long id, + Guid runId, + long stepId, + RetryFromFailedRequest? request, + RetryFromFailedService retryService, + ScheduleInvalidationDispatcher invalidationDispatcher, + CancellationToken ct + ) => { + try { + RetryFromFailedService.RetryResult result = await retryService.RetryAsync( + id, runId, stepId, request?.VariableOverrides, ct ); + + await invalidationDispatcher.InvalidateAsync( result.ScheduleId, ct ); + + return Results.Accepted( $"/api/workflows/runs/{runId}", + new { result.RunId, result.RetryFromStepId, result.ResetStepCount } ); + } catch (InvalidOperationException ex) { + return Results.Conflict( new { message = ex.Message } ); + } catch (KeyNotFoundException ex) { + return Results.NotFound( new { message = ex.Message } ); + } + } ) + .WithName( "RetryFromFailed" ) + .RequireAuthorization( Policies.CanExecute ); + } + + // ── Workflow Dashboard ── + + /// + /// Registers aggregated dashboard and latest-run-status endpoints. + /// + private static void MapWorkflowDashboard( WebApplication app ) { + + _ = app.MapGet( "/api/workflows/dashboard", async ( + int? page, + int? pageSize, + string? search, + string? tag, + string? status, + bool? enabled, + int? recentRunCount, + WerkrDbContext dbContext, + ScheduleService scheduleService, + ILogger logger, + CancellationToken ct + ) => { + int pageVal = Math.Max( 1, page ?? 1 ); + int pageSizeVal = Math.Clamp( pageSize ?? 25, 1, 100 ); + int sparklineCount = Math.Clamp( recentRunCount ?? 10, 1, 50 ); + + // Query 1 — Paginated workflow summaries + IQueryable query = dbContext.Workflows.AsNoTracking( ); + + if (!string.IsNullOrWhiteSpace( search )) { + query = query.Where( w => w.Name.Contains( search ) ); + } + + if (enabled.HasValue) { + query = query.Where( w => w.Enabled == enabled.Value ); + } + + if (!string.IsNullOrWhiteSpace( tag )) { + query = query.Where( w => w.TargetTags != null && w.TargetTags.Contains( tag ) ); + } + + bool hasStatusFilter = !string.IsNullOrWhiteSpace( status ); + int totalCount = await query.CountAsync( ct ); + + IQueryable orderedQuery = query.OrderBy( w => w.Name ); + + // When status filter is active, fetch all rows so we can filter + // in memory before paginating; otherwise paginate at DB level. + if (!hasStatusFilter) { + orderedQuery = orderedQuery + .Skip( (pageVal - 1) * pageSizeVal ) + .Take( pageSizeVal ); + } + + var summaries = await orderedQuery + .Select( w => new { + w.Id, + w.Name, + w.Description, + w.Enabled, + w.TargetTags, + StepCount = w.Steps.Count, + RunCount = w.Runs.Count, + LastRun = w.Runs + .OrderByDescending( r => r.StartTime ) + .Select( r => new { r.Id, r.Status, r.StartTime, r.EndTime } ) + .FirstOrDefault( ) + } ) + .ToListAsync( ct ); + + // Apply last-run status filter in memory (status is an enum stored as int) + if (hasStatusFilter) { + summaries = [.. summaries + .Where( s => s.LastRun is not null + && s.LastRun.Status.ToString( ).Equals( status, StringComparison.OrdinalIgnoreCase ) )]; + totalCount = summaries.Count; + summaries = [.. summaries + .Skip( (pageVal - 1) * pageSizeVal ) + .Take( pageSizeVal )]; + } + + List workflowIds = [.. summaries.Select( w => w.Id )]; + + // Query 2 — Sparkline data (recent runs for the page of workflow IDs) + List recentRuns; + try { + recentRuns = await dbContext.WorkflowRuns.AsNoTracking( ) + .Where( r => workflowIds.Contains( r.WorkflowId ) ) + .GroupBy( r => r.WorkflowId ) + .SelectMany( g => g.OrderByDescending( r => r.StartTime ).Take( sparklineCount ) ) + .ToListAsync( ct ); + } catch (Exception ex) { + // Fallback: EF provider may not support GroupBy+SelectMany+Take + logger.LogWarning( ex, "Sparkline GroupBy query failed; falling back to in-memory grouping" ); + recentRuns = await dbContext.WorkflowRuns.AsNoTracking( ) + .Where( r => workflowIds.Contains( r.WorkflowId ) ) + .OrderByDescending( r => r.StartTime ) + .ToListAsync( ct ); + recentRuns = [.. recentRuns + .GroupBy( r => r.WorkflowId ) + .SelectMany( g => g.Take( sparklineCount ) )]; + } + + ILookup sparklineByWorkflow = recentRuns + .OrderBy( r => r.StartTime ) + .ToLookup( + r => r.WorkflowId, + r => new RunSparklineDto( + RunId: r.Id, + Status: r.Status.ToString( ), + DurationSeconds: r.EndTime.HasValue + ? ( r.EndTime.Value - r.StartTime ).TotalSeconds + : null ) ); + + // Schedule computation — next scheduled run per workflow + List scheduleLinks = await dbContext.WorkflowSchedules.AsNoTracking( ) + .Where( ws => workflowIds.Contains( ws.WorkflowId ) ) + .ToListAsync( ct ); + + Dictionary nextScheduledByWorkflow = []; + DateTime utcNow = DateTime.UtcNow; + DateTime windowEnd = utcNow.AddDays( 30 ); + + ILookup scheduleIdsByWorkflow = scheduleLinks + .ToLookup( ws => ws.WorkflowId, ws => ws.ScheduleId ); + + foreach (long wfId in workflowIds) { + DateTime? earliest = null; + foreach (Guid scheduleId in scheduleIdsByWorkflow[wfId]) { + Schedule? schedule = await scheduleService.GetByIdAsync( scheduleId, ct ); + if (schedule is null) { + continue; + } + + try { + IReadOnlyList occurrences = ScheduleCalculator.CalculateOccurrences( + schedule, windowEnd ); + DateTime? next = occurrences.FirstOrDefault( o => o > utcNow ); + if (next.HasValue && next.Value != default + && (!earliest.HasValue || next.Value < earliest.Value)) { + earliest = next; + } + } catch { + // Schedule may be misconfigured — skip it + } + } + nextScheduledByWorkflow[wfId] = earliest; + } + + // Assemble response + List items = [.. summaries.Select( s => new WorkflowDashboardDto( + Id: s.Id, + Name: s.Name, + Description: s.Description, + Enabled: s.Enabled, + TargetTags: s.TargetTags, + StepCount: s.StepCount, + RunCount: s.RunCount, + LastRun: s.LastRun is not null + ? new WorkflowRunSummaryDto( + Id: s.LastRun.Id, + Status: s.LastRun.Status.ToString( ), + StartTime: s.LastRun.StartTime, + EndTime: s.LastRun.EndTime ) + : null, + RecentRuns: [.. sparklineByWorkflow[s.Id]], + NextScheduledRun: nextScheduledByWorkflow.GetValueOrDefault( s.Id ) + ) )]; + + return Results.Ok( new WorkflowDashboardPageDto( + Items: items, + TotalCount: totalCount, + Page: pageVal, + PageSize: pageSizeVal ) ); + } ) + .WithName( "GetWorkflowDashboard" ) + .RequireAuthorization( Policies.CanRead ); + + _ = app.MapGet( "/api/workflows/{id}/latest-run-status", async ( + long id, + WerkrDbContext dbContext, + ILogger logger, + CancellationToken ct + ) => { + var latestRun = await dbContext.WorkflowRuns.AsNoTracking( ) + .Where( r => r.WorkflowId == id ) + .OrderByDescending( r => r.StartTime ) + .Select( r => new { r.Id, r.Status, r.StartTime, r.EndTime } ) + .FirstOrDefaultAsync( ct ); + + if (latestRun is null) { + return Results.Ok( new WorkflowLatestRunStatusDto( + RunId: null, + RunStatus: null, + RunStartTime: null, + RunEndTime: null, + Steps: [] ) ); + } + + List steps; + try { + steps = await dbContext.WorkflowStepExecutions.AsNoTracking( ) + .Where( e => e.WorkflowRunId == latestRun.Id ) + .GroupBy( e => e.StepId ) + .Select( g => g.OrderByDescending( e => e.Attempt ).First( ) ) + .Select( e => new StepStatusSummaryDto( + StepId: e.StepId, + Status: e.Status.ToString( ), + StartTime: e.StartTime, + EndTime: e.EndTime, + ExitCode: e.Job != null ? e.Job.ExitCode : null ) ) + .ToListAsync( ct ); + } catch (Exception ex) { + // Fallback: EF provider (e.g. SQLite) may not support GroupBy+First + logger.LogWarning( ex, "Latest-run-status GroupBy query failed; falling back to in-memory grouping" ); + List allExecs = await dbContext.WorkflowStepExecutions + .AsNoTracking( ) + .Include( e => e.Job ) + .Where( e => e.WorkflowRunId == latestRun.Id ) + .OrderByDescending( e => e.Attempt ) + .ToListAsync( ct ); + steps = [.. allExecs + .GroupBy( e => e.StepId ) + .Select( g => g.First( ) ) + .Select( e => new StepStatusSummaryDto( + StepId: e.StepId, + Status: e.Status.ToString( ), + StartTime: e.StartTime, + EndTime: e.EndTime, + ExitCode: e.Job?.ExitCode ) )]; + } + + return Results.Ok( new WorkflowLatestRunStatusDto( + RunId: latestRun.Id, + RunStatus: latestRun.Status.ToString( ), + RunStartTime: latestRun.StartTime, + RunEndTime: latestRun.EndTime, + Steps: steps ) ); + } ) + .WithName( "GetLatestRunStatus" ) + .RequireAuthorization( Policies.CanRead ); } + + [GeneratedRegex( @"<[^>]+>" )] + private static partial Regex HtmlTagRegex( ); + + [GeneratedRegex( @"^#[0-9a-fA-F]{6}$" )] + private static partial Regex HexColorRegex( ); + + private static string StripHtmlTags( string input ) => + string.IsNullOrEmpty( input ) ? input : HtmlTagRegex( ).Replace( input, string.Empty ); } diff --git a/src/Werkr.Api/Models/WorkflowMapper.cs b/src/Werkr.Api/Models/WorkflowMapper.cs index d1696a6..4b725d1 100644 --- a/src/Werkr.Api/Models/WorkflowMapper.cs +++ b/src/Werkr.Api/Models/WorkflowMapper.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Werkr.Common.Models; using Werkr.Data.Entities.Workflows; @@ -25,6 +26,9 @@ public static Workflow ToEntity( long id, WorkflowUpdateRequest request ) => Description = request.Description ?? string.Empty, Enabled = request.Enabled, TargetTags = request.TargetTags, + Annotations = request.Annotations is { Count: > 0 } + ? JsonSerializer.Serialize( request.Annotations ) + : null, }; /// Maps a entity to a . @@ -35,7 +39,8 @@ public static WorkflowDto ToDto( Workflow workflow ) => Description: workflow.Description, Enabled: workflow.Enabled, Steps: [.. workflow.Steps.Select( ToStepDto )], - TargetTags: workflow.TargetTags ); + TargetTags: workflow.TargetTags, + Annotations: DeserializeAnnotations( workflow.Annotations ) ); /// Maps a entity to a . public static WorkflowStepDto ToStepDto( WorkflowStep step ) => @@ -51,7 +56,8 @@ public static WorkflowStepDto ToStepDto( WorkflowStep step ) => DependencyMode: step.DependencyMode.ToString( ), Dependencies: [.. step.Dependencies.Select( ToDepDto )], InputVariableName: step.InputVariableName, - OutputVariableName: step.OutputVariableName ); + OutputVariableName: step.OutputVariableName, + TaskName: step.Task?.Name ); /// Maps a to a . public static StepDependencyDto ToDepDto( WorkflowStepDependency dep ) => @@ -81,7 +87,7 @@ public static WorkflowRunDto ToRunDto( WorkflowRun run ) => EndTime: run.EndTime, Status: run.Status.ToString( ) ); - /// Maps a entity (with Jobs) to a . + /// Maps a entity (with Jobs and StepExecutions) to a . public static WorkflowRunDetailDto ToRunDetailDto( WorkflowRun run ) => new( Id: run.Id, @@ -89,7 +95,24 @@ public static WorkflowRunDetailDto ToRunDetailDto( WorkflowRun run ) => StartTime: run.StartTime, EndTime: run.EndTime, Status: run.Status.ToString( ), - Jobs: [.. run.Jobs.Select( TaskMapper.ToJobDto )] ); + Jobs: [.. run.Jobs.Select( TaskMapper.ToJobDto )], + StepExecutions: run.StepExecutions is not null + ? [.. run.StepExecutions.Select( ToStepExecutionDto )] + : [] ); + + /// Maps a entity to a . + public static StepExecutionDto ToStepExecutionDto( WorkflowStepExecution execution ) => + new( + Id: execution.Id, + WorkflowRunId: execution.WorkflowRunId, + StepId: execution.StepId, + Attempt: execution.Attempt, + Status: execution.Status.ToString( ), + StartTime: execution.StartTime, + EndTime: execution.EndTime, + JobId: execution.JobId, + ErrorMessage: execution.ErrorMessage, + SkipReason: execution.SkipReason ); /// Maps a entity to a . public static WorkflowVariableDto ToVariableDto( WorkflowVariable variable ) => @@ -120,4 +143,16 @@ public static RunVariableVersionDto ToRunVariableVersionDto( WorkflowRunVariable ProducedByJobId: variable.ProducedByJobId, Source: variable.Source.ToString( ), Created: variable.Created ); + + private static List? DeserializeAnnotations( string? json ) { + if (string.IsNullOrWhiteSpace( json )) { + return null; + } + + try { + return JsonSerializer.Deserialize>( json ); + } catch (JsonException) { + return null; + } + } } diff --git a/src/Werkr.Api/Program.cs b/src/Werkr.Api/Program.cs index 3c96154..b06305d 100644 --- a/src/Werkr.Api/Program.cs +++ b/src/Werkr.Api/Program.cs @@ -148,6 +148,9 @@ public static async Task Main( string[] args ) { // Job event broadcaster (Singleton — SSE fan-out for real-time push) _ = builder.Services.AddSingleton( ); + // Workflow event broadcaster (Singleton — SSE fan-out for step lifecycle events) + _ = builder.Services.AddSingleton( ); + // Output streaming gRPC service (Singleton — receives agent output streams) _ = builder.Services.AddSingleton( ); @@ -163,6 +166,7 @@ public static async Task Main( string[] args ) { // Schedule service (Scoped — one per request) _ = builder.Services.AddScoped( ); _ = builder.Services.AddScoped( ); + _ = builder.Services.AddScoped( ); // Task & Job services (Scoped — one per request) _ = builder.Services.Configure( @@ -245,6 +249,7 @@ public static async Task Main( string[] args ) { _ = app.MapHolidayCalendarEndpoints( ); _ = app.MapEventEndpoints( ); _ = app.MapShellEndpoints( ); + _ = app.MapFilterEndpoints( ); _ = app.MapDefaultEndpoints( ); diff --git a/src/Werkr.Api/Services/JobReportingGrpcService.cs b/src/Werkr.Api/Services/JobReportingGrpcService.cs index f23881c..d2e3327 100644 --- a/src/Werkr.Api/Services/JobReportingGrpcService.cs +++ b/src/Werkr.Api/Services/JobReportingGrpcService.cs @@ -1,10 +1,12 @@ using Grpc.Core; using Microsoft.EntityFrameworkCore; +using Werkr.Common.Models; using Werkr.Common.Protos; using Werkr.Core.Communication; using Werkr.Data; using Werkr.Data.Entities.Registration; using Werkr.Data.Entities.Tasks; +using Werkr.Data.Entities.Workflows; namespace Werkr.Api.Services; @@ -14,11 +16,13 @@ namespace Werkr.Api.Services; /// All RPCs use . /// /// Database context. -/// Singleton broadcaster for SSE push notifications. +/// Singleton broadcaster for SSE push notifications. +/// Singleton broadcaster for workflow SSE events. /// Logger instance. public sealed partial class JobReportingGrpcService( WerkrDbContext dbContext, - JobEventBroadcaster broadcaster, + JobEventBroadcaster jobBroadcaster, + WorkflowEventBroadcaster workflowBroadcaster, ILogger logger ) : JobReporting.JobReportingBase { @@ -79,6 +83,9 @@ ServerCallContext context ? parsedScheduleId : null; + // Parse step ID (if provided, for workflow step linkage) + long? stepId = inner.StepId > 0 ? inner.StepId : null; + // Upsert: if agent provided a job_id, check for existing job WerkrJob? job = agentJobId.HasValue ? await dbContext.Jobs.FirstOrDefaultAsync( j => j.Id == agentJobId.Value, context.CancellationToken ) @@ -99,6 +106,7 @@ ServerCallContext context job.OutputPath = inner.OutputPath; job.WorkflowRunId = workflowRunId; job.ScheduleId = scheduleId; + job.StepId = stepId; } else { // Create new job job = new( ) { @@ -115,6 +123,7 @@ ServerCallContext context OutputPath = inner.OutputPath, WorkflowRunId = workflowRunId, ScheduleId = scheduleId, + StepId = stepId, }; if (agentJobId.HasValue) { job.Id = agentJobId.Value; @@ -124,7 +133,7 @@ ServerCallContext context _ = await dbContext.SaveChangesAsync( context.CancellationToken ); // Broadcast to SSE subscribers after the job is safely persisted. - broadcaster.Publish( new JobEvent( + jobBroadcaster.Publish( new JobEvent( JobId: job.Id, TaskId: job.TaskId, WorkflowRunId: job.WorkflowRunId, @@ -135,6 +144,56 @@ ServerCallContext context Timestamp: DateTime.UtcNow ) ); + // Publish workflow step event and update step execution if this is a workflow job + if (workflowRunId.HasValue && stepId.HasValue) { + // Update WorkflowStepExecution to Completed/Failed + WorkflowStepExecution? stepExecution = await dbContext.WorkflowStepExecutions + .Where( se => se.WorkflowRunId == workflowRunId.Value && se.StepId == stepId.Value ) + .OrderByDescending( se => se.Attempt ) + .FirstOrDefaultAsync( context.CancellationToken ); + + if (stepExecution is not null) { + stepExecution.Status = inner.Success ? StepExecutionStatus.Completed : StepExecutionStatus.Failed; + stepExecution.EndTime = endTime ?? DateTime.UtcNow; + stepExecution.JobId = job.Id; + if (!inner.Success) { + stepExecution.ErrorMessage = inner.OutputPreview?[..Math.Min( inner.OutputPreview.Length, 4000 )]; + } + _ = await dbContext.SaveChangesAsync( context.CancellationToken ); + } + + // Resolve step name for event + string stepName = stepExecution?.Step?.Task?.Name ?? $"Step {stepId.Value}"; + if (stepExecution?.Step is null) { + WorkflowStep? step = await dbContext.WorkflowSteps + .Include( s => s.Task ) + .FirstOrDefaultAsync( s => s.Id == stepId.Value, context.CancellationToken ); + stepName = step?.Task?.Name ?? $"Step {stepId.Value}"; + } + + if (inner.Success) { + workflowBroadcaster.Publish( new StepCompletedEvent( + WorkflowRunId: workflowRunId.Value, + StepId: stepId.Value, + StepName: stepName, + JobId: job.Id, + ExitCode: inner.ExitCode, + RuntimeSeconds: inner.RuntimeSeconds, + Timestamp: DateTime.UtcNow + ) ); + } else { + workflowBroadcaster.Publish( new StepFailedEvent( + WorkflowRunId: workflowRunId.Value, + StepId: stepId.Value, + StepName: stepName, + JobId: job.Id, + ExitCode: inner.ExitCode, + ErrorMessage: inner.OutputPreview, + Timestamp: DateTime.UtcNow + ) ); + } + } + if (logger.IsEnabled( LogLevel.Information )) { logger.LogInformation( "Persisted job {JobId} from agent {AgentId}: Task={TaskId}, Success={Success}, Runtime={Runtime:F1}s.", @@ -157,4 +216,97 @@ private static RegisteredConnection GetConnection( ServerCallContext context ) { ? connection : throw new RpcException( new Status( StatusCode.Internal, "Connection not resolved by interceptor." ) ); } + + /// + /// Receives a step-started notification from an agent and creates a record. + /// + public override async Task ReportStepStarted( + EncryptedEnvelope request, + ServerCallContext context + ) { + RegisteredConnection connection = GetConnection( context ); + string keyId = connection.ActiveKeyId ?? connection.Id.ToString( ); + + StepStartedRequest inner = PayloadEncryptor.DecryptFromEnvelope( + request, connection.SharedKey ); + + if (!Guid.TryParse( inner.WorkflowRunId, out Guid runId )) { + throw new RpcException( new Status( StatusCode.InvalidArgument, "Invalid workflow_run_id." ) ); + } + + DateTime startTime = DateTime.TryParse( inner.StartTime, out DateTime st ) + ? DateTime.SpecifyKind( st, DateTimeKind.Utc ) + : DateTime.UtcNow; + + // Determine attempt number (previous max + 1 for retries) + int maxAttempt = await dbContext.WorkflowStepExecutions + .Where( se => se.WorkflowRunId == runId && se.StepId == inner.StepId ) + .MaxAsync( se => (int?) se.Attempt, context.CancellationToken ) ?? 0; + + int attempt = maxAttempt + 1; + + WorkflowStepExecution execution = new( ) { + WorkflowRunId = runId, + StepId = inner.StepId, + Attempt = attempt, + Status = StepExecutionStatus.Running, + StartTime = startTime, + }; + _ = dbContext.WorkflowStepExecutions.Add( execution ); + _ = await dbContext.SaveChangesAsync( context.CancellationToken ); + + workflowBroadcaster.Publish( new StepStartedEvent( + WorkflowRunId: runId, + StepId: inner.StepId, + StepName: inner.StepName, + TaskId: inner.TaskId, + Timestamp: DateTime.UtcNow + ) ); + + StepEventResponse response = new( ) { Accepted = true }; + return PayloadEncryptor.EncryptToEnvelope( response, connection.SharedKey, keyId ); + } + + /// + /// Receives a step-skipped notification from an agent and creates a record. + /// + public override async Task ReportStepSkipped( + EncryptedEnvelope request, + ServerCallContext context + ) { + RegisteredConnection connection = GetConnection( context ); + string keyId = connection.ActiveKeyId ?? connection.Id.ToString( ); + + StepSkippedRequest inner = PayloadEncryptor.DecryptFromEnvelope( + request, connection.SharedKey ); + + if (!Guid.TryParse( inner.WorkflowRunId, out Guid runId )) { + throw new RpcException( new Status( StatusCode.InvalidArgument, "Invalid workflow_run_id." ) ); + } + + int maxAttempt = await dbContext.WorkflowStepExecutions + .Where( se => se.WorkflowRunId == runId && se.StepId == inner.StepId ) + .MaxAsync( se => (int?) se.Attempt, context.CancellationToken ) ?? 0; + + WorkflowStepExecution execution = new( ) { + WorkflowRunId = runId, + StepId = inner.StepId, + Attempt = maxAttempt + 1, + Status = StepExecutionStatus.Skipped, + SkipReason = inner.Reason, + }; + _ = dbContext.WorkflowStepExecutions.Add( execution ); + _ = await dbContext.SaveChangesAsync( context.CancellationToken ); + + workflowBroadcaster.Publish( new StepSkippedEvent( + WorkflowRunId: runId, + StepId: inner.StepId, + StepName: inner.StepName, + Reason: inner.Reason, + Timestamp: DateTime.UtcNow + ) ); + + StepEventResponse response = new( ) { Accepted = true }; + return PayloadEncryptor.EncryptToEnvelope( response, connection.SharedKey, keyId ); + } } diff --git a/src/Werkr.Api/Services/OutputStreamingGrpcService.cs b/src/Werkr.Api/Services/OutputStreamingGrpcService.cs index 0e19549..37346ad 100644 --- a/src/Werkr.Api/Services/OutputStreamingGrpcService.cs +++ b/src/Werkr.Api/Services/OutputStreamingGrpcService.cs @@ -2,6 +2,7 @@ using System.Threading.Channels; using Grpc.Core; using Werkr.Common.Protos; +using Werkr.Core.Communication; namespace Werkr.Api.Services; @@ -15,8 +16,10 @@ namespace Werkr.Api.Services; /// that wraps the duplex call. /// /// +/// Workflow event broadcaster for log events. /// Logger. public sealed partial class OutputStreamingGrpcService( + WorkflowEventBroadcaster workflowBroadcaster, ILogger logger ) : Werkr.Common.Protos.OutputStreamingService.OutputStreamingServiceBase { @@ -34,6 +37,43 @@ private sealed class AgentStream { /// All currently connected agent streams keyed by peer address. private readonly ConcurrentDictionary _agents = new( ); + /// + /// Per-run rate limiter for publishing. + /// Tracks last publish timestamp and dropped-line count per workflow run ID. + /// Max 50 events/second per run to prevent flooding the SSE→SignalR pipeline. + /// + private readonly ConcurrentDictionary _logRateState = new( ); + + /// Mutable rate-limit state for a single workflow run's log events. + private sealed class RunLogRateState { + private long _lastPublishTicks; + private int _droppedCount; + + /// Maximum log events published per second per run. + private const int MaxEventsPerSecond = 50; + private static readonly long s_tickInterval = TimeSpan.TicksPerSecond / MaxEventsPerSecond; + + /// + /// Attempts to acquire a publish permit. Returns true if within rate limit. + /// On true after drops, returns the count of dropped lines for batching notice. + /// + public bool TryAcquire( out int droppedSinceLastPublish ) { + _ = Environment.TickCount64 * TimeSpan.TicksPerMillisecond / TimeSpan.TicksPerMillisecond; + long nowTicks = DateTime.UtcNow.Ticks; + long last = Interlocked.Read( ref _lastPublishTicks ); + + if (nowTicks - last >= s_tickInterval) { + _ = Interlocked.Exchange( ref _lastPublishTicks, nowTicks ); + droppedSinceLastPublish = Interlocked.Exchange( ref _droppedCount, 0 ); + return true; + } + + _ = Interlocked.Increment( ref _droppedCount ); + droppedSinceLastPublish = 0; + return false; + } + } + // ── gRPC Entry Point ───────────────────────────────────────────────────────── /// @@ -65,6 +105,33 @@ ServerCallContext context _ = kvp.Value.Writer.TryWrite( message ); } } + + // Publish LogAppendedEvent for workflow step output (rate-limited) + if (message.PayloadCase == OutputMessage.PayloadOneofCase.Line + && !string.IsNullOrEmpty( message.WorkflowRunId ) + && Guid.TryParse( message.WorkflowRunId, out Guid workflowRunId ) + && Guid.TryParse( message.JobId, out Guid jobId )) { + + RunLogRateState rateState = _logRateState.GetOrAdd( workflowRunId, _ => new RunLogRateState( ) ); + if (rateState.TryAcquire( out int dropped )) { + if (dropped > 0) { + workflowBroadcaster.Publish( new LogAppendedEvent( + WorkflowRunId: workflowRunId, + StepId: message.StepId, + JobId: jobId, + Line: $"... {dropped} lines batched ...", + Timestamp: DateTime.UtcNow + ) ); + } + workflowBroadcaster.Publish( new LogAppendedEvent( + WorkflowRunId: workflowRunId, + StepId: message.StepId, + JobId: jobId, + Line: message.Line.Text, + Timestamp: DateTime.UtcNow + ) ); + } + } } } catch (OperationCanceledException) { // Agent disconnected or server shutting down @@ -140,6 +207,17 @@ public async Task UnsubscribeAsync( long taskId, string scheduleId ) { } } + // ── Run Lifecycle ────────────────────────────────────────────────────────── + + /// + /// Removes rate-limit state for a completed workflow run, preventing unbounded + /// growth of . + /// + /// The workflow run that has completed. + public void CleanupRun( Guid runId ) { + _ = _logRateState.TryRemove( runId, out _ ); + } + // ── Helpers ────────────────────────────────────────────────────────────────── private static string BuildConsumerKey( long taskId, string scheduleId ) diff --git a/src/Werkr.Api/Services/ScheduleInvalidationDispatcher.cs b/src/Werkr.Api/Services/ScheduleInvalidationDispatcher.cs index bda3d1c..a701439 100644 --- a/src/Werkr.Api/Services/ScheduleInvalidationDispatcher.cs +++ b/src/Werkr.Api/Services/ScheduleInvalidationDispatcher.cs @@ -7,6 +7,7 @@ using Werkr.Data; using Werkr.Data.Entities.Registration; using Werkr.Data.Entities.Tasks; +using Werkr.Data.Entities.Workflows; namespace Werkr.Api.Services; @@ -45,14 +46,21 @@ ILogger logger .Select( ts => ts.Task! ) .ToListAsync( ct ); - if (affectedTasks.Count == 0) { + // Find workflows referencing this schedule to get their TargetTags + List affectedWorkflows = await db.WorkflowSchedules + .AsNoTracking( ) + .Where( ws => ws.ScheduleId == scheduleId ) + .Select( ws => ws.Workflow! ) + .ToListAsync( ct ); + + if (affectedTasks.Count == 0 && affectedWorkflows.Count == 0) { if (logger.IsEnabled( LogLevel.Debug )) { - logger.LogDebug( "No tasks reference schedule {ScheduleId}. Skipping invalidation.", scheduleId ); + logger.LogDebug( "No tasks or workflows reference schedule {ScheduleId}. Skipping invalidation.", scheduleId ); } return; } - // Collect all unique target tags from affected tasks + // Collect all unique target tags from affected tasks and workflows HashSet allTargetTags = new( StringComparer.OrdinalIgnoreCase ); foreach (WerkrTask task in affectedTasks) { if (task.TargetTags is { Length: > 0 }) { @@ -61,6 +69,13 @@ ILogger logger } } } + foreach (Workflow workflow in affectedWorkflows) { + if (workflow.TargetTags is { Length: > 0 }) { + foreach (string tag in workflow.TargetTags) { + _ = allTargetTags.Add( tag ); + } + } + } // Find all connected agents List agents = await db.RegisteredConnections diff --git a/src/Werkr.Api/Services/ScheduleSyncGrpcService.cs b/src/Werkr.Api/Services/ScheduleSyncGrpcService.cs index dfc3779..f4485f1 100644 --- a/src/Werkr.Api/Services/ScheduleSyncGrpcService.cs +++ b/src/Werkr.Api/Services/ScheduleSyncGrpcService.cs @@ -104,14 +104,11 @@ ServerCallContext context foreach (WorkflowSchedule ws in workflowSchedules) { Workflow workflow = ws.Workflow!; // If workflow has TargetTags, use those for agent matching; otherwise fall back to per-task tags - bool anyMatch; - if (workflow.TargetTags is { Length: > 0 }) { - anyMatch = workflow.TargetTags.Any( tag => agentTags.Contains( tag.Trim( ) ) ); - } else { - anyMatch = workflow.Steps.Any( step => + bool anyMatch = workflow.TargetTags is { Length: > 0 } + ? workflow.TargetTags.Any( tag => agentTags.Contains( tag.Trim( ) ) ) + : workflow.Steps.Any( step => step.Task is not null && step.Task.TargetTags.Any( tag => agentTags.Contains( tag.Trim( ) ) ) ); - } if (!anyMatch) { continue; } diff --git a/src/Werkr.Api/Services/VariableGrpcService.cs b/src/Werkr.Api/Services/VariableGrpcService.cs index a1f57f8..adc6791 100644 --- a/src/Werkr.Api/Services/VariableGrpcService.cs +++ b/src/Werkr.Api/Services/VariableGrpcService.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Werkr.Common.Configuration; +using Werkr.Common.Models; using Werkr.Common.Protos; using Werkr.Core.Communication; using Werkr.Data; @@ -16,10 +17,14 @@ namespace Werkr.Api.Services; /// /// Database context. /// Variable size configuration. +/// Workflow event broadcaster for run lifecycle events. +/// Output streaming service for rate-limit state cleanup. /// Logger instance. public sealed partial class VariableGrpcService( WerkrDbContext dbContext, IOptions variableOptions, + WorkflowEventBroadcaster workflowBroadcaster, + OutputStreamingGrpcService outputStreaming, ILogger logger ) : VariableService.VariableServiceBase { @@ -237,6 +242,104 @@ private static RegisteredConnection GetConnection( ServerCallContext context ) { : throw new RpcException( new Status( StatusCode.Internal, "Connection not resolved by interceptor." ) ); } + /// + /// Sets WorkflowRun.Status to Completed or Failed and publishes a . + /// + public override async Task CompleteWorkflowRun( + EncryptedEnvelope request, + ServerCallContext context + ) { + RegisteredConnection connection = GetConnection( context ); + string keyId = connection.ActiveKeyId ?? connection.Id.ToString( ); + + CompleteWorkflowRunRequest inner = PayloadEncryptor.DecryptFromEnvelope( + request, connection.SharedKey ); + + if (!Guid.TryParse( inner.WorkflowRunId, out Guid runId )) { + throw new RpcException( new Status( StatusCode.InvalidArgument, "Invalid workflow_run_id." ) ); + } + + DateTime endTime = DateTime.TryParse( inner.EndTime, out DateTime et ) + ? DateTime.SpecifyKind( et, DateTimeKind.Utc ) + : DateTime.UtcNow; + + WorkflowRun? run = await dbContext.WorkflowRuns + .FirstOrDefaultAsync( r => r.Id == runId, context.CancellationToken ) ?? throw new RpcException( new Status( StatusCode.NotFound, $"WorkflowRun {runId} not found." ) ); + run.Status = inner.Success ? WorkflowRunStatus.Completed : WorkflowRunStatus.Failed; + run.EndTime = endTime; + _ = await dbContext.SaveChangesAsync( context.CancellationToken ); + + // Count step results for the event + int completedSteps = await dbContext.WorkflowStepExecutions + .CountAsync( se => se.WorkflowRunId == runId && se.Status == StepExecutionStatus.Completed, + context.CancellationToken ); + int failedSteps = await dbContext.WorkflowStepExecutions + .CountAsync( se => se.WorkflowRunId == runId && se.Status == StepExecutionStatus.Failed, + context.CancellationToken ); + + long? failedStepId = inner.FailedStepId > 0 ? inner.FailedStepId : null; + + workflowBroadcaster.Publish( new RunCompletedEvent( + WorkflowRunId: runId, + Success: inner.Success, + FailedStepId: failedStepId, + CompletedSteps: completedSteps, + FailedSteps: failedSteps, + Timestamp: DateTime.UtcNow + ) ); + + outputStreaming.CleanupRun( runId ); + + if (logger.IsEnabled( LogLevel.Information )) { + logger.LogInformation( + "Workflow run {RunId} finalized: Success={Success}, Completed={Completed}, Failed={Failed}.", + runId.ToString( ), inner.Success.ToString( ), + completedSteps.ToString( ), failedSteps.ToString( ) ); + } + + CompleteWorkflowRunResponse response = new( ) { Accepted = true }; + return PayloadEncryptor.EncryptToEnvelope( response, connection.SharedKey, keyId ); + } + + /// + /// Returns step execution records for a workflow run (used by the agent for retry skip logic). + /// + public override async Task GetStepExecutions( + EncryptedEnvelope request, + ServerCallContext context + ) { + RegisteredConnection connection = GetConnection( context ); + string keyId = connection.ActiveKeyId ?? connection.Id.ToString( ); + + GetStepExecutionsRequest inner = PayloadEncryptor.DecryptFromEnvelope( + request, connection.SharedKey ); + + if (!Guid.TryParse( inner.WorkflowRunId, out Guid runId )) { + throw new RpcException( new Status( StatusCode.InvalidArgument, "Invalid workflow_run_id." ) ); + } + + // Return the latest attempt per step + List executions = await dbContext.WorkflowStepExecutions + .Where( se => se.WorkflowRunId == runId ) + .OrderByDescending( se => se.Attempt ) + .ToListAsync( context.CancellationToken ); + + GetStepExecutionsResponse response = new( ); + + HashSet seen = []; + foreach (WorkflowStepExecution exec in executions) { + if (seen.Add( exec.StepId )) { + response.Entries.Add( new StepExecutionEntry { + StepId = exec.StepId, + Attempt = exec.Attempt, + Status = exec.Status.ToString( ), + } ); + } + } + + return PayloadEncryptor.EncryptToEnvelope( response, connection.SharedKey, keyId ); + } + /// /// Validates that a string is valid JSON. /// diff --git a/src/Werkr.Api/Werkr.Api.csproj.user b/src/Werkr.Api/Werkr.Api.csproj.user new file mode 100644 index 0000000..9ff5820 --- /dev/null +++ b/src/Werkr.Api/Werkr.Api.csproj.user @@ -0,0 +1,6 @@ + + + + https + + \ No newline at end of file diff --git a/src/Werkr.Api/packages.lock.json b/src/Werkr.Api/packages.lock.json index b74fe8e..f9c0c3a 100644 --- a/src/Werkr.Api/packages.lock.json +++ b/src/Werkr.Api/packages.lock.json @@ -21,35 +21,35 @@ }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "1mUwGyeGrGETdhlU/ZzNZeN2J6ugqf9EztZyG+WQIZwkaH5+lFNza15+cNCXhXNA0MKo1iohr5vj60eqR2J44Q==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } }, "Microsoft.AspNetCore.OpenApi": { "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "OsEhbmT4Xenukau5YCR867gr/HmuAJ9DqMBPQGTcmdNU/TqBqdcnB+yLNwD/mTdkHzLBB+XG7cI4H1L5B1jx+Q==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==", "dependencies": { "Microsoft.OpenApi": "2.0.0" } }, "Microsoft.EntityFrameworkCore.Design": { "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "FmiUU5xdu1chVxnmsu/mEpCKVQ5+lvIxdP0194lE7HfoU1jO4z/9qnWZpd0kSkVve4gOnRm1lE20kkhlMqJJIg==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "gm6f0cC2w/2tcd4GeZJqEMruTercpIJfO5sSAFLtqTqblDBHgAFk70xwshUIUVX4I6sZwdEUSd1YxoKFk1AL0w==", "dependencies": { "Humanizer.Core": "2.14.1", "Microsoft.Build.Framework": "18.0.2", "Microsoft.CodeAnalysis.CSharp": "5.0.0", "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", "Mono.TextTemplating": "3.0.0", "Newtonsoft.Json": "13.0.3" } @@ -209,30 +209,30 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw==" + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA==" + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4" + "Microsoft.EntityFrameworkCore": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.4", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", "SQLitePCLRaw.core": "2.1.11" } }, @@ -253,8 +253,8 @@ }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg==" + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" }, "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { "type": "Transitive", @@ -358,8 +358,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==" + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==" }, "OpenTelemetry": { "type": "Transitive", @@ -545,7 +545,7 @@ "type": "Project", "dependencies": { "Grpc.Net.Client": "[2.76.0, )", - "System.Security.Cryptography.ProtectedData": "[10.0.4, )", + "System.Security.Cryptography.ProtectedData": "[10.0.5, )", "Werkr.Common": "[1.0.0, )", "Werkr.Data": "[1.0.0, )" } @@ -554,9 +554,9 @@ "type": "Project", "dependencies": { "EFCore.NamingConventions": "[10.0.1, )", - "Microsoft.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", "Werkr.Common": "[1.0.0, )" } }, @@ -606,30 +606,30 @@ "Microsoft.Data.Sqlite.Core": { "type": "CentralTransitive", "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", "dependencies": { "SQLitePCLRaw.core": "2.1.11" } }, "Microsoft.EntityFrameworkCore": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4" + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", "SQLitePCLRaw.core": "2.1.11" } @@ -664,13 +664,13 @@ }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)", - "Npgsql": "10.0.0" + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" } }, "OpenTelemetry.Exporter.OpenTelemetryProtocol": { @@ -703,9 +703,9 @@ }, "System.Security.Cryptography.ProtectedData": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg==" } } } diff --git a/src/Werkr.AppHost/AppHost.cs b/src/Werkr.AppHost/AppHost.cs index d3db58f..431a2e0 100644 --- a/src/Werkr.AppHost/AppHost.cs +++ b/src/Werkr.AppHost/AppHost.cs @@ -14,19 +14,32 @@ public static void Main( string[] args ) { IResourceBuilder werkrDb = postgres.AddDatabase( "werkrdb" ); IResourceBuilder werkrIdentityDb = postgres.AddDatabase( "werkridentitydb" ); - // Agent - _ = builder.AddProject( "agent" ) + // API Service — owns the application database. + // Pin ports so the agent's stored gRPC RemoteUrl remains valid across restarts. + IResourceBuilder apiService = builder.AddProject( "api" ) + .WithEndpoint( "https", e => { e.Port = 7349; e.IsProxied = false; } ) + .WithEndpoint( "http", e => { e.Port = 5560; e.IsProxied = false; } ) .WithHttpHealthCheck( "/health" ) + .WithReference( werkrDb ) .WaitFor( werkrDb ); - // API Service — owns the application database - IResourceBuilder apiService = builder.AddProject( "api" ) + // Agent — pin ports so the registered RemoteUrl survives Aspire restarts. + // IsProxied = false makes the app bind directly on the port (no DCP proxy) + // so gRPC works without HTTP/2 proxy issues. + // Werkr__ApiUrl overrides the stored gRPC URL so the agent always reaches the + // correct API endpoint, even if the DB record is stale from a prior registration. + _ = builder.AddProject( "agent" ) + .WithEndpoint( "https", e => { e.Port = 7100; e.IsProxied = false; } ) + .WithEndpoint( "http", e => { e.Port = 5100; e.IsProxied = false; } ) + .WithEnvironment( "Werkr__AgentUrl", "https://localhost:7100" ) + .WithEnvironment( "Werkr__ApiUrl", apiService.GetEndpoint( "https" ) ) .WithHttpHealthCheck( "/health" ) - .WithReference( werkrDb ) .WaitFor( werkrDb ); // Server (Blazor UI) — owns the identity database _ = builder.AddProject( "server" ) + .WithEndpoint( "https", e => { e.Port = 7041; e.IsProxied = false; } ) + .WithEndpoint( "http", e => { e.Port = 5005; e.IsProxied = false; } ) .WithExternalHttpEndpoints( ) .WithHttpHealthCheck( "/health" ) .WithReference( apiService ) diff --git a/src/Werkr.AppHost/packages.lock.json b/src/Werkr.AppHost/packages.lock.json index 2add083..b0a468d 100644 --- a/src/Werkr.AppHost/packages.lock.json +++ b/src/Werkr.AppHost/packages.lock.json @@ -608,7 +608,7 @@ }, "Microsoft.Extensions.Configuration.Json": { "type": "CentralTransitive", - "requested": "[10.0.4, )", + "requested": "[10.0.5, )", "resolved": "10.0.1", "contentHash": "0zW3eYBJlRctmgqk5s0kFIi5o5y2g80mvGCD8bkYxREPQlKUnr0ndU/Sop+UDIhyWN0fIi4RW63vo7BKTi7ncA==", "dependencies": { @@ -620,7 +620,7 @@ }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "CentralTransitive", - "requested": "[10.0.4, )", + "requested": "[10.0.5, )", "resolved": "10.0.1", "contentHash": "qmoQkVZcbm4/gFpted3W3Y+1kTATZTcUhV3mRkbtpfBXlxWCHwh/2oMffVcCruaGOfJuEnyAsGyaSUouSdECOw==", "dependencies": { diff --git a/src/Werkr.Common.Configuration/packages.lock.json b/src/Werkr.Common.Configuration/packages.lock.json index 7f5b5f6..5224309 100644 --- a/src/Werkr.Common.Configuration/packages.lock.json +++ b/src/Werkr.Common.Configuration/packages.lock.json @@ -1,21 +1,21 @@ -{ - "version": 2, - "dependencies": { - ".NETStandard,Version=v2.0": { - "NETStandard.Library": { - "type": "Direct", - "requested": "[2.0.3, )", - "resolved": "2.0.3", - "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0" - } - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" - } - } - } +{ + "version": 2, + "dependencies": { + ".NETStandard,Version=v2.0": { + "NETStandard.Library": { + "type": "Direct", + "requested": "[2.0.3, )", + "resolved": "2.0.3", + "contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + } + } + } } \ No newline at end of file diff --git a/src/Werkr.Common/Models/Actions/ActionRegistry.cs b/src/Werkr.Common/Models/Actions/ActionRegistry.cs index cb3cc9a..0984071 100644 --- a/src/Werkr.Common/Models/Actions/ActionRegistry.cs +++ b/src/Werkr.Common/Models/Actions/ActionRegistry.cs @@ -323,6 +323,50 @@ public static class ActionRegistry { HelpText: "The JSON value to set or merge. Required for Set and Merge operations." ), ] ), ] ), + + // ── Shell operations ───────────────────────────────────────── + new( "ShellCommand", "Shell Command", "Execute a system shell command (bash/cmd).", + "Shell", typeof( ShellCommandParameters ), [ + new( "Content", "Command", FieldType.TextArea, Required: true, + Placeholder: "echo 'Hello, World!'", + HelpText: "The shell command to execute." ), + new( "TimeoutMinutes", "Timeout (minutes)", FieldType.Number, DefaultValue: "30", Min: 1, + HelpText: "Maximum execution time in minutes." ), + ] ), + + new( "ShellScript", "Shell Script", "Execute a shell script file (bash/sh).", + "Shell", typeof( ShellScriptParameters ), [ + new( "ScriptPath", "Script Path", FieldType.Text, Required: true, + Placeholder: "/path/to/script.sh", + HelpText: "Path to the shell script file." ), + new( "Arguments", "Arguments", FieldType.Text, + Placeholder: "--flag value", + HelpText: "Optional arguments to pass to the script." ), + new( "TimeoutMinutes", "Timeout (minutes)", FieldType.Number, DefaultValue: "30", Min: 1, + HelpText: "Maximum execution time in minutes." ), + ] ), + + // ── PowerShell operations ──────────────────────────────────── + new( "PowerShellCommand", "PowerShell Command", "Execute an inline PowerShell command or script block.", + "PowerShell", typeof( PowerShellCommandParameters ), [ + new( "Content", "Command", FieldType.TextArea, Required: true, + Placeholder: "Get-Process | Where-Object { $_.CPU -gt 100 }", + HelpText: "The PowerShell command or script block to execute." ), + new( "TimeoutMinutes", "Timeout (minutes)", FieldType.Number, DefaultValue: "30", Min: 1, + HelpText: "Maximum execution time in minutes." ), + ] ), + + new( "PowerShellScript", "PowerShell Script", "Execute a PowerShell script file (.ps1).", + "PowerShell", typeof( PowerShellScriptParameters ), [ + new( "ScriptPath", "Script Path", FieldType.Text, Required: true, + Placeholder: "C:\\scripts\\deploy.ps1", + HelpText: "Path to the PowerShell script file." ), + new( "Arguments", "Arguments", FieldType.Text, + Placeholder: "-Environment Production -Verbose", + HelpText: "Optional arguments to pass to the script." ), + new( "TimeoutMinutes", "Timeout (minutes)", FieldType.Number, DefaultValue: "30", Min: 1, + HelpText: "Maximum execution time in minutes." ), + ] ), ]; /// Fast lookup by action key (case-insensitive). diff --git a/src/Werkr.Common/Models/Actions/PowerShellCommandParameters.cs b/src/Werkr.Common/Models/Actions/PowerShellCommandParameters.cs new file mode 100644 index 0000000..4671767 --- /dev/null +++ b/src/Werkr.Common/Models/Actions/PowerShellCommandParameters.cs @@ -0,0 +1,10 @@ +namespace Werkr.Common.Models.Actions; + +/// Parameters for the PowerShellCommand action. +public sealed record PowerShellCommandParameters { + /// The PowerShell command or script block to execute. + public required string Content { get; init; } + + /// Timeout in minutes. Defaults to 30. + public long TimeoutMinutes { get; init; } = 30; +} diff --git a/src/Werkr.Common/Models/Actions/PowerShellScriptParameters.cs b/src/Werkr.Common/Models/Actions/PowerShellScriptParameters.cs new file mode 100644 index 0000000..113efb4 --- /dev/null +++ b/src/Werkr.Common/Models/Actions/PowerShellScriptParameters.cs @@ -0,0 +1,13 @@ +namespace Werkr.Common.Models.Actions; + +/// Parameters for the PowerShellScript action. +public sealed record PowerShellScriptParameters { + /// Path to the PowerShell script (.ps1) to execute. + public required string ScriptPath { get; init; } + + /// Optional arguments to pass to the script. + public string? Arguments { get; init; } + + /// Timeout in minutes. Defaults to 30. + public long TimeoutMinutes { get; init; } = 30; +} diff --git a/src/Werkr.Common/Models/Actions/ShellCommandParameters.cs b/src/Werkr.Common/Models/Actions/ShellCommandParameters.cs new file mode 100644 index 0000000..4bdd5c8 --- /dev/null +++ b/src/Werkr.Common/Models/Actions/ShellCommandParameters.cs @@ -0,0 +1,10 @@ +namespace Werkr.Common.Models.Actions; + +/// Parameters for the ShellCommand action. +public sealed record ShellCommandParameters { + /// The shell command to execute. + public required string Content { get; init; } + + /// Timeout in minutes. Defaults to 30. + public long TimeoutMinutes { get; init; } = 30; +} diff --git a/src/Werkr.Common/Models/Actions/ShellScriptParameters.cs b/src/Werkr.Common/Models/Actions/ShellScriptParameters.cs new file mode 100644 index 0000000..43f6b5e --- /dev/null +++ b/src/Werkr.Common/Models/Actions/ShellScriptParameters.cs @@ -0,0 +1,13 @@ +namespace Werkr.Common.Models.Actions; + +/// Parameters for the ShellScript action. +public sealed record ShellScriptParameters { + /// Path to the shell script to execute. + public required string ScriptPath { get; init; } + + /// Optional arguments to pass to the script. + public string? Arguments { get; init; } + + /// Timeout in minutes. Defaults to 30. + public long TimeoutMinutes { get; init; } = 30; +} diff --git a/src/Werkr.Common/Models/AnnotationDto.cs b/src/Werkr.Common/Models/AnnotationDto.cs new file mode 100644 index 0000000..df0f66c --- /dev/null +++ b/src/Werkr.Common/Models/AnnotationDto.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; + +namespace Werkr.Common.Models; + +/// DTO for a DAG canvas sticky-note annotation. +public sealed record AnnotationDto { + + /// Unique identifier for the annotation. + [Required] + public Guid Id { get; init; } + + /// Annotation text content (max 500 chars). + [Required, MaxLength( 500 )] + public string Text { get; init; } = ""; + + /// X position on the canvas. + [Range( -10000, 50000 )] + public double X { get; init; } + + /// Y position on the canvas. + [Range( -10000, 50000 )] + public double Y { get; init; } + + /// Width of the annotation card. + [Range( 80, 800 )] + public double Width { get; init; } + + /// Height of the annotation card. + [Range( 40, 600 )] + public double Height { get; init; } + + /// Background color as a hex string (e.g., #fef3cd). + [RegularExpression( @"^#[0-9a-fA-F]{6}$" )] + public string Color { get; init; } = "#fef3cd"; +} diff --git a/src/Werkr.Common/Models/DagEdgeDto.cs b/src/Werkr.Common/Models/DagEdgeDto.cs new file mode 100644 index 0000000..5a4500b --- /dev/null +++ b/src/Werkr.Common/Models/DagEdgeDto.cs @@ -0,0 +1,7 @@ +namespace Werkr.Common.Models; + +/// Edge data for X6 DAG rendering (dependency link). +public sealed record DagEdgeDto( + long SourceStepId, + long TargetStepId +); diff --git a/src/Werkr.Common/Models/DagNodeDto.cs b/src/Werkr.Common/Models/DagNodeDto.cs new file mode 100644 index 0000000..073b7fa --- /dev/null +++ b/src/Werkr.Common/Models/DagNodeDto.cs @@ -0,0 +1,10 @@ +namespace Werkr.Common.Models; + +/// Node data for X6 DAG rendering. +public sealed record DagNodeDto( + long StepId, + string StepLabel, + string TaskName, + string ControlStatement, + int Order +); diff --git a/src/Werkr.Common/Models/FilterCriteria.cs b/src/Werkr.Common/Models/FilterCriteria.cs new file mode 100644 index 0000000..e6b3457 --- /dev/null +++ b/src/Werkr.Common/Models/FilterCriteria.cs @@ -0,0 +1,12 @@ +namespace Werkr.Common.Models; + +/// Holds the current filter values keyed by field name. +public sealed class FilterCriteria { + + /// Filter values keyed by . + public Dictionary Values { get; set; } = []; + + /// Gets the value for the given , or if absent. + public string? Get( string key ) + => Values.TryGetValue( key, out string? v ) ? v : null; +} diff --git a/src/Werkr.Common/Models/FilterDefinition.cs b/src/Werkr.Common/Models/FilterDefinition.cs new file mode 100644 index 0000000..f7e3992 --- /dev/null +++ b/src/Werkr.Common/Models/FilterDefinition.cs @@ -0,0 +1,6 @@ +namespace Werkr.Common.Models; + +/// Declares the available filter fields for a list page. +public sealed record FilterDefinition( + IReadOnlyList Fields +); diff --git a/src/Werkr.Common/Models/FilterField.cs b/src/Werkr.Common/Models/FilterField.cs new file mode 100644 index 0000000..2e7a6e1 --- /dev/null +++ b/src/Werkr.Common/Models/FilterField.cs @@ -0,0 +1,9 @@ +namespace Werkr.Common.Models; + +/// Describes a single filterable field rendered in the FilterBar. +public sealed record FilterField( + string Key, + string Label, + FilterFieldType Type, + IReadOnlyList? Options = null +); diff --git a/src/Werkr.Common/Models/FilterFieldType.cs b/src/Werkr.Common/Models/FilterFieldType.cs new file mode 100644 index 0000000..491d3a7 --- /dev/null +++ b/src/Werkr.Common/Models/FilterFieldType.cs @@ -0,0 +1,17 @@ +namespace Werkr.Common.Models; + +/// Supported control types for filter fields. +public enum FilterFieldType { + + /// Drop-down select with predefined options. + Dropdown, + + /// Date picker. + Date, + + /// Free-form text input. + Text, + + /// Numeric input. + Number, +} diff --git a/src/Werkr.Common/Models/GanttItemDto.cs b/src/Werkr.Common/Models/GanttItemDto.cs new file mode 100644 index 0000000..baf4c14 --- /dev/null +++ b/src/Werkr.Common/Models/GanttItemDto.cs @@ -0,0 +1,11 @@ +namespace Werkr.Common.Models; + +/// DTO representing a single step execution bar for the Gantt timeline view. +public sealed record GanttItemDto( + string Id, + string Content, + string Start, + string? End, + string ClassName, + string Title +); diff --git a/src/Werkr.Common/Models/RetryFromFailedRequest.cs b/src/Werkr.Common/Models/RetryFromFailedRequest.cs new file mode 100644 index 0000000..01cb063 --- /dev/null +++ b/src/Werkr.Common/Models/RetryFromFailedRequest.cs @@ -0,0 +1,6 @@ +namespace Werkr.Common.Models; + +/// Request body for the retry-from-failed endpoint. +public sealed record RetryFromFailedRequest( + Dictionary? VariableOverrides +); diff --git a/src/Werkr.Common/Models/RunSparklineDto.cs b/src/Werkr.Common/Models/RunSparklineDto.cs new file mode 100644 index 0000000..b06f48b --- /dev/null +++ b/src/Werkr.Common/Models/RunSparklineDto.cs @@ -0,0 +1,8 @@ +namespace Werkr.Common.Models; + +/// Minimal run data for sparkline visualization. +public sealed record RunSparklineDto( + Guid RunId, + string Status, + double? DurationSeconds +); diff --git a/src/Werkr.Common/Models/SavedFilterView.cs b/src/Werkr.Common/Models/SavedFilterView.cs new file mode 100644 index 0000000..beba34c --- /dev/null +++ b/src/Werkr.Common/Models/SavedFilterView.cs @@ -0,0 +1,10 @@ +namespace Werkr.Common.Models; + +/// A named, persisted filter combination for a list page. +public sealed record SavedFilterView( + string Id, + string Name, + bool IsDefault, + bool IsShared, + FilterCriteria Criteria +); diff --git a/src/Werkr.Common/Models/StepExecutionDto.cs b/src/Werkr.Common/Models/StepExecutionDto.cs new file mode 100644 index 0000000..3ab7e1f --- /dev/null +++ b/src/Werkr.Common/Models/StepExecutionDto.cs @@ -0,0 +1,15 @@ +namespace Werkr.Common.Models; + +/// Step execution tracking DTO for API responses. +public sealed record StepExecutionDto( + long Id, + Guid WorkflowRunId, + long StepId, + int Attempt, + string Status, + DateTime? StartTime, + DateTime? EndTime, + Guid? JobId, + string? ErrorMessage, + string? SkipReason +); diff --git a/src/Werkr.Common/Models/StepExecutionStatus.cs b/src/Werkr.Common/Models/StepExecutionStatus.cs new file mode 100644 index 0000000..63a8b65 --- /dev/null +++ b/src/Werkr.Common/Models/StepExecutionStatus.cs @@ -0,0 +1,23 @@ +namespace Werkr.Common.Models; + +/// +/// Status of a single workflow step execution attempt. +/// Stored in Werkr.Common so both Werkr.Data and Werkr.Server can reference it. +/// +public enum StepExecutionStatus { + + /// Step is waiting to execute. + Pending = 0, + + /// Step is currently executing. + Running = 1, + + /// Step completed successfully. + Completed = 2, + + /// Step execution failed. + Failed = 3, + + /// Step was skipped by control flow evaluation. + Skipped = 4, +} diff --git a/src/Werkr.Common/Models/StepStatusSummaryDto.cs b/src/Werkr.Common/Models/StepStatusSummaryDto.cs new file mode 100644 index 0000000..2cda5f4 --- /dev/null +++ b/src/Werkr.Common/Models/StepStatusSummaryDto.cs @@ -0,0 +1,10 @@ +namespace Werkr.Common.Models; + +/// Lightweight step execution status for DAG overlay coloring. +public sealed record StepStatusSummaryDto( + long StepId, + string Status, + DateTime? StartTime, + DateTime? EndTime, + int? ExitCode +); diff --git a/src/Werkr.Common/Models/WorkflowDashboardDto.cs b/src/Werkr.Common/Models/WorkflowDashboardDto.cs new file mode 100644 index 0000000..f09fec1 --- /dev/null +++ b/src/Werkr.Common/Models/WorkflowDashboardDto.cs @@ -0,0 +1,15 @@ +namespace Werkr.Common.Models; + +/// Dashboard summary for a single workflow. +public sealed record WorkflowDashboardDto( + long Id, + string Name, + string Description, + bool Enabled, + string[]? TargetTags, + int StepCount, + int RunCount, + WorkflowRunSummaryDto? LastRun, + IReadOnlyList RecentRuns, + DateTime? NextScheduledRun +); diff --git a/src/Werkr.Common/Models/WorkflowDashboardPageDto.cs b/src/Werkr.Common/Models/WorkflowDashboardPageDto.cs new file mode 100644 index 0000000..724e4d0 --- /dev/null +++ b/src/Werkr.Common/Models/WorkflowDashboardPageDto.cs @@ -0,0 +1,9 @@ +namespace Werkr.Common.Models; + +/// Paginated response for the workflow dashboard endpoint. +public sealed record WorkflowDashboardPageDto( + IReadOnlyList Items, + int TotalCount, + int Page, + int PageSize +); diff --git a/src/Werkr.Common/Models/WorkflowDto.cs b/src/Werkr.Common/Models/WorkflowDto.cs index 94dbee3..669270d 100644 --- a/src/Werkr.Common/Models/WorkflowDto.cs +++ b/src/Werkr.Common/Models/WorkflowDto.cs @@ -7,5 +7,6 @@ public sealed record WorkflowDto( string Description, bool Enabled, IReadOnlyList Steps, - string[]? TargetTags = null + string[]? TargetTags = null, + List? Annotations = null ); diff --git a/src/Werkr.Common/Models/WorkflowLatestRunStatusDto.cs b/src/Werkr.Common/Models/WorkflowLatestRunStatusDto.cs new file mode 100644 index 0000000..ed2f98c --- /dev/null +++ b/src/Werkr.Common/Models/WorkflowLatestRunStatusDto.cs @@ -0,0 +1,10 @@ +namespace Werkr.Common.Models; + +/// Per-step execution status for the most recent workflow run. +public sealed record WorkflowLatestRunStatusDto( + Guid? RunId, + string? RunStatus, + DateTime? RunStartTime, + DateTime? RunEndTime, + IReadOnlyList Steps +); diff --git a/src/Werkr.Common/Models/WorkflowRunDetailDto.cs b/src/Werkr.Common/Models/WorkflowRunDetailDto.cs index eeed63e..27485af 100644 --- a/src/Werkr.Common/Models/WorkflowRunDetailDto.cs +++ b/src/Werkr.Common/Models/WorkflowRunDetailDto.cs @@ -1,11 +1,12 @@ namespace Werkr.Common.Models; -/// Response DTO for a workflow run with job details. +/// Response DTO for a workflow run with job details and step executions. public sealed record WorkflowRunDetailDto( Guid Id, long WorkflowId, DateTime StartTime, DateTime? EndTime, string Status, - IReadOnlyList Jobs + IReadOnlyList Jobs, + IReadOnlyList StepExecutions ); diff --git a/src/Werkr.Common/Models/WorkflowRunDto.cs b/src/Werkr.Common/Models/WorkflowRunDto.cs index a8316e4..4d53259 100644 --- a/src/Werkr.Common/Models/WorkflowRunDto.cs +++ b/src/Werkr.Common/Models/WorkflowRunDto.cs @@ -6,5 +6,6 @@ public sealed record WorkflowRunDto( long WorkflowId, DateTime StartTime, DateTime? EndTime, - string Status + string Status, + string? WorkflowName = null ); diff --git a/src/Werkr.Common/Models/WorkflowRunSummaryDto.cs b/src/Werkr.Common/Models/WorkflowRunSummaryDto.cs new file mode 100644 index 0000000..0052dcc --- /dev/null +++ b/src/Werkr.Common/Models/WorkflowRunSummaryDto.cs @@ -0,0 +1,9 @@ +namespace Werkr.Common.Models; + +/// Lightweight summary of a workflow run for dashboard display. +public sealed record WorkflowRunSummaryDto( + Guid Id, + string Status, + DateTime StartTime, + DateTime? EndTime +); diff --git a/src/Werkr.Common/Models/WorkflowStepBatchModels.cs b/src/Werkr.Common/Models/WorkflowStepBatchModels.cs new file mode 100644 index 0000000..aed5abb --- /dev/null +++ b/src/Werkr.Common/Models/WorkflowStepBatchModels.cs @@ -0,0 +1,70 @@ +namespace Werkr.Common.Models; + +/// +/// Atomic batch request for creating, updating, and deleting workflow steps and dependencies. +/// Processed within a single database transaction — all-or-nothing. +/// +/// Ordered list of step operations. +public sealed record WorkflowStepBatchRequest( + IReadOnlyList Operations +); + +/// +/// A single step operation within a batch. +/// StepId uses sign convention: negative = temp ID (new step), positive = real ID (existing step). +/// +/// "Add", "Update", or "Delete". +/// Negative temp ID for adds; positive real ID for updates/deletes. +/// Required for "Add" operations. +/// Step order (topological tiebreaker). +/// Control flow type: "Default", "If", "ElseIf", "Else", "While", "Do". +/// Condition for If/ElseIf/While/Do steps. +/// Loop guard for While/Do steps. +/// "All" or "Any". +/// Optional pin to specific agent. +/// Variable name to read from predecessor output. +/// Variable name to write step output into. +/// Optional per-step dependency mutations. +public sealed record StepBatchOperation( + string OperationType, + long StepId, + long? TaskId = null, + int Order = 0, + string ControlStatement = "Default", + string? ConditionExpression = null, + int MaxIterations = 100, + string DependencyMode = "All", + Guid? AgentConnectionIdOverride = null, + string? InputVariableName = null, + string? OutputVariableName = null, + IReadOnlyList? DependencyChanges = null +); + +/// +/// A dependency change within a batch operation. +/// DependsOnStepId uses sign convention: negative = temp ID, positive = real ID (resolved by endpoint). +/// +/// "Add" or "Delete". +/// Negative temp ID or positive real ID. +public sealed record DependencyBatchItem( + string OperationType, + long DependsOnStepId +); + +/// Response from a batch step operation. +/// Whether the entire batch succeeded. +/// Temp-to-real ID mappings for created steps. +/// Validation or processing errors (empty on success). +public sealed record WorkflowStepBatchResponse( + bool Success, + IReadOnlyList IdMappings, + IReadOnlyList Errors +); + +/// Maps a client-assigned temp ID to the server-generated real ID. +/// The negative temp ID used in the request. +/// The positive real ID assigned by the database. +public sealed record StepIdMapping( + long TempId, + long RealId +); diff --git a/src/Werkr.Common/Models/WorkflowStepDto.cs b/src/Werkr.Common/Models/WorkflowStepDto.cs index 8b47a43..03d8718 100644 --- a/src/Werkr.Common/Models/WorkflowStepDto.cs +++ b/src/Werkr.Common/Models/WorkflowStepDto.cs @@ -13,5 +13,6 @@ public sealed record WorkflowStepDto( string DependencyMode, IReadOnlyList Dependencies, string? InputVariableName = null, - string? OutputVariableName = null + string? OutputVariableName = null, + string? TaskName = null ); diff --git a/src/Werkr.Common/Models/WorkflowUpdateRequest.cs b/src/Werkr.Common/Models/WorkflowUpdateRequest.cs index ec87f76..80d62eb 100644 --- a/src/Werkr.Common/Models/WorkflowUpdateRequest.cs +++ b/src/Werkr.Common/Models/WorkflowUpdateRequest.cs @@ -5,5 +5,6 @@ public sealed record WorkflowUpdateRequest( string Name, string? Description = null, bool Enabled = true, - string[]? TargetTags = null + string[]? TargetTags = null, + List? Annotations = null ); diff --git a/src/Werkr.Common/Protos/JobReport.proto b/src/Werkr.Common/Protos/JobReport.proto index ebfe2f9..8e7aff2 100644 --- a/src/Werkr.Common/Protos/JobReport.proto +++ b/src/Werkr.Common/Protos/JobReport.proto @@ -9,6 +9,10 @@ import "EncryptedEnvelope.proto"; service JobReporting { // Request envelope contains JobResultRequest; response contains JobResultResponse. rpc ReportJobResult (EncryptedEnvelope) returns (EncryptedEnvelope); + // Request envelope contains StepStartedRequest; response contains StepEventResponse. + rpc ReportStepStarted (EncryptedEnvelope) returns (EncryptedEnvelope); + // Request envelope contains StepSkippedRequest; response contains StepEventResponse. + rpc ReportStepSkipped (EncryptedEnvelope) returns (EncryptedEnvelope); } message JobResultRequest { @@ -44,6 +48,8 @@ message JobResultRequest { string output_variable_name = 15; // JSON blob of the output variable value (empty = no output). string output_variable_value = 16; + // Optional workflow step ID linking this job to a specific workflow step. + int64 step_id = 17; } message JobResultResponse { @@ -52,3 +58,24 @@ message JobResultResponse { // The assigned job ID on the server. string job_id = 2; } + +message StepStartedRequest { + string connection_id = 1; + string workflow_run_id = 2; + int64 step_id = 3; + string step_name = 4; + int64 task_id = 5; + string start_time = 6; +} + +message StepSkippedRequest { + string connection_id = 1; + string workflow_run_id = 2; + int64 step_id = 3; + string step_name = 4; + string reason = 5; +} + +message StepEventResponse { + bool accepted = 1; +} diff --git a/src/Werkr.Common/Protos/OutputStreaming.proto b/src/Werkr.Common/Protos/OutputStreaming.proto index 4c0a957..4c74289 100644 --- a/src/Werkr.Common/Protos/OutputStreaming.proto +++ b/src/Werkr.Common/Protos/OutputStreaming.proto @@ -19,6 +19,9 @@ message OutputMessage { OutputLine line = 4; OutputComplete complete = 5; } + // Workflow context — populated by the agent during workflow execution. + string workflow_run_id = 6; + int64 step_id = 7; } message OutputLine { diff --git a/src/Werkr.Common/Protos/VariableService.proto b/src/Werkr.Common/Protos/VariableService.proto index f89cc4b..1ae6871 100644 --- a/src/Werkr.Common/Protos/VariableService.proto +++ b/src/Werkr.Common/Protos/VariableService.proto @@ -15,6 +15,12 @@ service VariableService { // cron-triggered workflows where no API-side run was pre-created. // Request envelope contains CreateWorkflowRunRequest; response contains CreateWorkflowRunResponse. rpc CreateWorkflowRun (EncryptedEnvelope) returns (EncryptedEnvelope); + // Agent calls when workflow run finishes (success or failure). + // Request envelope contains CompleteWorkflowRunRequest; response contains CompleteWorkflowRunResponse. + rpc CompleteWorkflowRun (EncryptedEnvelope) returns (EncryptedEnvelope); + // Returns step execution records for a workflow run (used by agent for retry skip logic). + // Request envelope contains GetStepExecutionsRequest; response contains GetStepExecutionsResponse. + rpc GetStepExecutions (EncryptedEnvelope) returns (EncryptedEnvelope); } message GetVariableRequest { @@ -54,3 +60,30 @@ message CreateWorkflowRunResponse { string workflow_run_id = 2; // API-generated run ID string error = 3; // non-empty on rejection } + +message CompleteWorkflowRunRequest { + string connection_id = 1; + string workflow_run_id = 2; + bool success = 3; + string end_time = 4; // ISO 8601 UTC + int64 failed_step_id = 5; // Optional — which step caused failure +} + +message CompleteWorkflowRunResponse { + bool accepted = 1; +} + +message GetStepExecutionsRequest { + string connection_id = 1; + string workflow_run_id = 2; +} + +message GetStepExecutionsResponse { + repeated StepExecutionEntry entries = 1; +} + +message StepExecutionEntry { + int64 step_id = 1; + int32 attempt = 2; + string status = 3; // "Pending", "Running", "Completed", "Failed", "Skipped" +} diff --git a/src/Werkr.Common/packages.lock.json b/src/Werkr.Common/packages.lock.json index d0457d9..ece49fb 100644 --- a/src/Werkr.Common/packages.lock.json +++ b/src/Werkr.Common/packages.lock.json @@ -1,195 +1,195 @@ -{ - "version": 2, - "dependencies": { - "net10.0": { - "Google.Protobuf": { - "type": "Direct", - "requested": "[3.34.0, )", - "resolved": "3.34.0", - "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" - }, - "Grpc.Tools": { - "type": "Direct", - "requested": "[2.78.0, )", - "resolved": "2.78.0", - "contentHash": "6jPG2gHon+w2PczW8jjrCRnW/g9eEfCdd7aK6mDooptWtuPsV3ZxAwKKEx7LGEDVoT4c2SViRl8Yu3L1XiWIIg==" - }, - "Microsoft.AspNetCore.Authorization": { - "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==", - "dependencies": { - "Microsoft.AspNetCore.Metadata": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Json": { - "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4" - } - }, - "Microsoft.IdentityModel.Tokens": { - "type": "Direct", - "requested": "[8.16.0, )", - "resolved": "8.16.0", - "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "10.0.0", - "Microsoft.IdentityModel.Logging": "8.16.0" - } - }, - "Microsoft.AspNetCore.Metadata": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w==" - }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.FileExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Physical": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ==" - }, - "Microsoft.Extensions.Diagnostics": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileProviders.Physical": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileSystemGlobbing": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileSystemGlobbing": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg==" - }, - "Microsoft.IdentityModel.Abstractions": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.16.0" - } - }, - "werkr.common.configuration": { - "type": "Project" - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - } - } - } +{ + "version": 2, + "dependencies": { + "net10.0": { + "Google.Protobuf": { + "type": "Direct", + "requested": "[3.34.0, )", + "resolved": "3.34.0", + "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" + }, + "Grpc.Tools": { + "type": "Direct", + "requested": "[2.78.0, )", + "resolved": "2.78.0", + "contentHash": "6jPG2gHon+w2PczW8jjrCRnW/g9eEfCdd7aK6mDooptWtuPsV3ZxAwKKEx7LGEDVoT4c2SViRl8Yu3L1XiWIIg==" + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Direct", + "requested": "[8.16.0, )", + "resolved": "8.16.0", + "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.IdentityModel.Logging": "8.16.0" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A==" + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.16.0" + } + }, + "werkr.common.configuration": { + "type": "Project" + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + } + } + } } \ No newline at end of file diff --git a/src/Werkr.Core/Communication/WorkflowEvent.cs b/src/Werkr.Core/Communication/WorkflowEvent.cs new file mode 100644 index 0000000..a0f5ad5 --- /dev/null +++ b/src/Werkr.Core/Communication/WorkflowEvent.cs @@ -0,0 +1,47 @@ +namespace Werkr.Core.Communication; + +/// +/// Base record for all workflow execution lifecycle events. +/// Published by and consumed by SSE endpoints. +/// +public abstract record WorkflowEvent( + Guid WorkflowRunId, + DateTime Timestamp +); + +/// Fired when a workflow step begins execution. +public sealed record StepStartedEvent( + Guid WorkflowRunId, long StepId, string StepName, + long TaskId, DateTime Timestamp +) : WorkflowEvent( WorkflowRunId, Timestamp ); + +/// Fired when a workflow step completes successfully. +public sealed record StepCompletedEvent( + Guid WorkflowRunId, long StepId, string StepName, + Guid JobId, int ExitCode, double RuntimeSeconds, DateTime Timestamp +) : WorkflowEvent( WorkflowRunId, Timestamp ); + +/// Fired when a workflow step fails. +public sealed record StepFailedEvent( + Guid WorkflowRunId, long StepId, string StepName, + Guid JobId, int ExitCode, string? ErrorMessage, DateTime Timestamp +) : WorkflowEvent( WorkflowRunId, Timestamp ); + +/// Fired when a workflow step is skipped by control flow evaluation. +public sealed record StepSkippedEvent( + Guid WorkflowRunId, long StepId, string StepName, + string Reason, DateTime Timestamp +) : WorkflowEvent( WorkflowRunId, Timestamp ); + +/// Fired when a workflow run completes (success or failure). +public sealed record RunCompletedEvent( + Guid WorkflowRunId, bool Success, + long? FailedStepId, int CompletedSteps, int FailedSteps, + DateTime Timestamp +) : WorkflowEvent( WorkflowRunId, Timestamp ); + +/// Fired when a log line is received for a workflow step. +public sealed record LogAppendedEvent( + Guid WorkflowRunId, long StepId, Guid JobId, + string Line, DateTime Timestamp +) : WorkflowEvent( WorkflowRunId, Timestamp ); diff --git a/src/Werkr.Core/Communication/WorkflowEventBroadcaster.cs b/src/Werkr.Core/Communication/WorkflowEventBroadcaster.cs new file mode 100644 index 0000000..e29a13f --- /dev/null +++ b/src/Werkr.Core/Communication/WorkflowEventBroadcaster.cs @@ -0,0 +1,80 @@ +using System.Threading.Channels; +using Microsoft.Extensions.Logging; + +namespace Werkr.Core.Communication; + +/// +/// Singleton broadcaster that fans out notifications to all +/// active SSE subscribers. Each subscriber receives its own bounded +/// so slow consumers cannot block producers. +/// +/// Logger instance. +public sealed partial class WorkflowEventBroadcaster( ILogger logger ) { + + private readonly Lock _lock = new( ); + private readonly List> _subscribers = []; + private readonly ILogger _logger = logger; + + /// + /// Creates a new subscription. The caller reads from the returned + /// and must call + /// (or dispose the ) when done. + /// + public WorkflowEventSubscription Subscribe( ) { + Channel channel = Channel.CreateBounded( + new BoundedChannelOptions( 512 ) { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false, + } ); + + lock (_lock) { + _subscribers.Add( channel.Writer ); + } + + LogSubscribed( _subscribers.Count ); + + return new WorkflowEventSubscription( channel.Reader, ( ) => Unsubscribe( channel.Writer ) ); + } + + /// + /// Publishes a to every active subscriber. + /// Non-blocking — events are dropped for subscribers whose channel is full. + /// + public void Publish( WorkflowEvent workflowEvent ) { + ChannelWriter[] snapshot; + lock (_lock) { + snapshot = [.. _subscribers]; + } + + int delivered = 0; + foreach (ChannelWriter writer in snapshot) { + if (writer.TryWrite( workflowEvent )) { + delivered++; + } + } + + LogPublished( workflowEvent.WorkflowRunId, workflowEvent.GetType( ).Name, delivered, snapshot.Length ); + } + + private void Unsubscribe( ChannelWriter writer ) { + lock (_lock) { + _ = _subscribers.Remove( writer ); + } + + _ = writer.TryComplete( ); + LogUnsubscribed( _subscribers.Count ); + } + + [LoggerMessage( Level = LogLevel.Debug, + Message = "Workflow SSE subscriber added. Active subscribers: {Count}." )] + private partial void LogSubscribed( int count ); + + [LoggerMessage( Level = LogLevel.Debug, + Message = "Published {EventType} for run {RunId} to {Delivered}/{Total} subscribers." )] + private partial void LogPublished( Guid runId, string eventType, int delivered, int total ); + + [LoggerMessage( Level = LogLevel.Debug, + Message = "Workflow SSE subscriber removed. Active subscribers: {Count}." )] + private partial void LogUnsubscribed( int count ); +} diff --git a/src/Werkr.Core/Communication/WorkflowEventSubscription.cs b/src/Werkr.Core/Communication/WorkflowEventSubscription.cs new file mode 100644 index 0000000..b861727 --- /dev/null +++ b/src/Werkr.Core/Communication/WorkflowEventSubscription.cs @@ -0,0 +1,28 @@ +using System.Threading.Channels; + +namespace Werkr.Core.Communication; + +/// +/// Represents an active SSE subscription to workflow events. +/// Disposing removes the subscriber from the broadcaster. +/// +/// Channel reader for this subscriber. +/// Callback to remove the subscriber from the broadcaster. +public sealed class WorkflowEventSubscription( ChannelReader reader, Action unsubscribe ) : IDisposable { + + private readonly Action _unsubscribe = unsubscribe; + private bool _disposed; + + /// The channel reader that delivers workflow events to this subscriber. + public ChannelReader Reader { get; } = reader; + + /// Unsubscribes from the broadcaster. + public void Dispose( ) { + if (_disposed) { + return; + } + + _disposed = true; + _unsubscribe( ); + } +} diff --git a/src/Werkr.Core/Scheduling/RetryFromFailedService.cs b/src/Werkr.Core/Scheduling/RetryFromFailedService.cs new file mode 100644 index 0000000..d4e72f7 --- /dev/null +++ b/src/Werkr.Core/Scheduling/RetryFromFailedService.cs @@ -0,0 +1,191 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Werkr.Common.Models; +using Werkr.Data; +using Werkr.Data.Entities.Schedule; +using Werkr.Data.Entities.Workflows; + +namespace Werkr.Core.Scheduling; + +/// +/// Orchestrates retry-from-failed for a workflow run. Atomically resets the +/// target step and all downstream steps, applies variable overrides, and +/// creates a one-time schedule for re-execution. +/// +public sealed partial class RetryFromFailedService( + WerkrDbContext dbContext, + ILogger logger +) { + + /// Result returned by . + public sealed record RetryResult( Guid RunId, long RetryFromStepId, int ResetStepCount, Guid ScheduleId ); + + /// + /// Retries a failed workflow run starting from the specified step. + /// The target step and all transitive downstream steps are reset to Pending. + /// + /// The workflow ID. + /// The run to retry. + /// The failed step to retry from. + /// Optional variable value overrides. + /// Cancellation token. + /// A on success. + /// If the run is not in Failed status or the step is not failed. + /// If the step doesn't exist or doesn't belong to the workflow. + public async Task RetryAsync( + long workflowId, Guid runId, long stepId, + Dictionary? variableOverrides, + CancellationToken ct = default ) { + + await using Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction transaction = + await dbContext.Database.BeginTransactionAsync( ct ); + + // Step 1: Atomic status transition via compare-and-swap + int rowsAffected = await dbContext.WorkflowRuns + .Where( r => r.Id == runId && r.Status == WorkflowRunStatus.Failed ) + .ExecuteUpdateAsync( s => s + .SetProperty( r => r.Status, WorkflowRunStatus.Running ) + .SetProperty( r => r.EndTime, (DateTime?)null ) + .SetProperty( r => r.LastUpdated, DateTime.UtcNow ), + ct ); + + if (rowsAffected == 0) { + throw new InvalidOperationException( "Run is not in Failed status. Cannot retry." ); + } + + // Step 2: Validate step belongs to workflow + WorkflowStep step = await dbContext.WorkflowSteps + .FirstOrDefaultAsync( s => s.Id == stepId && s.WorkflowId == workflowId, ct ) + ?? throw new KeyNotFoundException( $"Step {stepId} not found in workflow {workflowId}." ); + + // Step 3: Validate target step has a Failed execution + WorkflowStepExecution? failedExecution = await dbContext.WorkflowStepExecutions + .Where( e => e.WorkflowRunId == runId && e.StepId == stepId && e.Status == StepExecutionStatus.Failed ) + .OrderByDescending( e => e.Attempt ) + .FirstOrDefaultAsync( ct ) ?? throw new InvalidOperationException( $"Step {stepId} does not have a Failed execution in this run." ); + + // Step 4: Compute downstream steps (transitive dependents from the target step) + HashSet stepsToReset = await ComputeDownstreamStepsAsync( workflowId, stepId, ct ); + _ = stepsToReset.Add( stepId ); // Include the target step itself + + // Step 5: Reset step executions — insert new rows with incremented attempt + foreach (long resetStepId in stepsToReset) { + int maxAttempt = await dbContext.WorkflowStepExecutions + .Where( e => e.WorkflowRunId == runId && e.StepId == resetStepId ) + .MaxAsync( e => (int?)e.Attempt, ct ) ?? 0; + + WorkflowStepExecution pendingExecution = new( ) { + WorkflowRunId = runId, + StepId = resetStepId, + Attempt = maxAttempt + 1, + Status = StepExecutionStatus.Pending, + }; + _ = dbContext.WorkflowStepExecutions.Add( pendingExecution ); + } + + // Step 6: Apply variable overrides + if (variableOverrides is { Count: > 0 }) { + foreach (KeyValuePair kvp in variableOverrides) { + int maxVersion = await dbContext.Set( ) + .Where( v => v.WorkflowRunId == runId && v.VariableName == kvp.Key ) + .MaxAsync( v => (int?)v.Version, ct ) ?? 0; + + WorkflowRunVariable overrideVar = new( ) { + WorkflowRunId = runId, + VariableName = kvp.Key, + Value = kvp.Value, + Version = maxVersion + 1, + Source = VariableSource.ReExecutionEdit, + Created = DateTime.UtcNow, + }; + _ = dbContext.Set( ).Add( overrideVar ); + } + } + + _ = await dbContext.SaveChangesAsync( ct ); + + // Step 7: Create one-time schedule (same pattern as RunNowService) + DbSchedule schedule = new( ) { + Name = $"Retry from Step {stepId} – Run {runId}", + StopTaskAfterMinutes = 60, + CatchUpEnabled = true, + }; + _ = dbContext.Schedules.Add( schedule ); + _ = await dbContext.SaveChangesAsync( ct ); + + DateTime utcNow = DateTime.UtcNow; + StartDateTimeInfo startDt = new( ) { + ScheduleId = schedule.Id, + Date = DateOnly.FromDateTime( utcNow ), + Time = TimeOnly.FromDateTime( utcNow ), + TimeZone = TimeZoneInfo.Utc, + }; + _ = dbContext.StartDateTimeInfos.Add( startDt ); + + WorkflowSchedule link = new( ) { + WorkflowId = workflowId, + ScheduleId = schedule.Id, + IsOneTime = true, + CreatedAtUtc = DateTime.UtcNow, + WorkflowRunId = runId, + }; + _ = dbContext.WorkflowSchedules.Add( link ); + _ = await dbContext.SaveChangesAsync( ct ); + + await transaction.CommitAsync( ct ); + + LogRetryCreated( logger, runId, stepId, stepsToReset.Count, schedule.Id ); + return new RetryResult( runId, stepId, stepsToReset.Count, schedule.Id ); + } + + /// + /// Computes all steps transitively downstream from the given step in the DAG. + /// + private async Task> ComputeDownstreamStepsAsync( + long workflowId, long fromStepId, CancellationToken ct ) { + + // Build adjacency: step → list of dependents (steps that depend on it) + List allDeps = await dbContext.Set( ) + .Where( d => d.Step!.WorkflowId == workflowId ) + .ToListAsync( ct ); + + Dictionary> adjacency = []; + foreach (WorkflowStepDependency dep in allDeps) { + if (!adjacency.TryGetValue( dep.DependsOnStepId, out List? dependents )) { + dependents = []; + adjacency[dep.DependsOnStepId] = dependents; + } + dependents.Add( dep.StepId ); + } + + // BFS from fromStepId to find all transitive dependents + HashSet downstream = []; + Queue queue = new( ); + if (adjacency.TryGetValue( fromStepId, out List? initial )) { + foreach (long s in initial) { + queue.Enqueue( s ); + } + } + + while (queue.Count > 0) { + long current = queue.Dequeue( ); + if (!downstream.Add( current )) { + continue; + } + + if (adjacency.TryGetValue( current, out List? next )) { + foreach (long s in next) { + if (!downstream.Contains( s )) { + queue.Enqueue( s ); + } + } + } + } + + return downstream; + } + + [LoggerMessage( Level = LogLevel.Information, + Message = "Retry created for run {RunId} from step {StepId}: {ResetCount} steps reset, schedule {ScheduleId}." )] + private static partial void LogRetryCreated( ILogger logger, Guid runId, long stepId, int resetCount, Guid scheduleId ); +} diff --git a/src/Werkr.Core/Workflows/WorkflowService.cs b/src/Werkr.Core/Workflows/WorkflowService.cs index 1bfc795..f0bc41f 100644 --- a/src/Werkr.Core/Workflows/WorkflowService.cs +++ b/src/Werkr.Core/Workflows/WorkflowService.cs @@ -1,5 +1,7 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Logging; +using Werkr.Common.Models; using Werkr.Data; using Werkr.Data.Entities.Workflows; @@ -228,6 +230,8 @@ public async Task UpdateStepAsync( existing.MaxIterations = step.MaxIterations; existing.AgentConnectionIdOverride = step.AgentConnectionIdOverride; existing.DependencyMode = step.DependencyMode; + existing.InputVariableName = step.InputVariableName; + existing.OutputVariableName = step.OutputVariableName; _ = await dbContext.SaveChangesAsync( ct ); return existing; @@ -467,6 +471,202 @@ public async Task>> GetTopologicalLeve : (IReadOnlyList>)levels; } + /// + /// Atomically applies a batch of step add/update/delete operations and dependency changes + /// within a single database transaction. Validates the resulting DAG before committing. + /// + /// The workflow to apply changes to. + /// The batch request containing all operations. + /// Cancellation token. + /// Batch response with temp-to-real ID mappings and validation results. + public async Task BatchUpdateStepsAsync( + long workflowId, + WorkflowStepBatchRequest request, + CancellationToken ct = default + ) { + if (request.Operations.Count == 0) { + return new WorkflowStepBatchResponse( true, [], [] ); + } + + // Verify workflow exists + bool workflowExists = await dbContext.Workflows.AnyAsync( + w => w.Id == workflowId, + ct + ); + if (!workflowExists) { + return new WorkflowStepBatchResponse( false, [], [$"Workflow with Id={workflowId} was not found."] ); + } + + // Verify all positive StepIds belong to this workflow + List positiveStepIds = [.. request.Operations + .Where( o => o.StepId > 0 ) + .Select( o => o.StepId ) + .Distinct( )]; + + if (positiveStepIds.Count > 0) { + List existingIds = await dbContext.WorkflowSteps + .Where( s => s.WorkflowId == workflowId && positiveStepIds.Contains( s.Id ) ) + .Select( s => s.Id ) + .ToListAsync( ct ); + + List invalid = [.. positiveStepIds.Except( existingIds )]; + if (invalid.Count > 0) { + return new WorkflowStepBatchResponse( false, [], + [$"Step IDs do not belong to workflow {workflowId}: {string.Join( ", ", invalid )}"] ); + } + } + + await using IDbContextTransaction tx = await dbContext.Database.BeginTransactionAsync( ct ); + try { + Dictionary tempToReal = []; + List mappings = []; + + // ── Phase 1: Process "Add" operations ── + List adds = [.. request.Operations.Where( o => string.Equals( o.OperationType, "Add", StringComparison.OrdinalIgnoreCase ) )]; + + foreach (StepBatchOperation add in adds) { + if (add.TaskId is null) { + await tx.RollbackAsync( ct ); + return new WorkflowStepBatchResponse( false, [], + [$"Add operation for temp step {add.StepId} is missing TaskId."] ); + } + + WorkflowStep step = new( ) { + WorkflowId = workflowId, + TaskId = add.TaskId.Value, + Order = add.Order, + ControlStatement = Enum.Parse( add.ControlStatement, ignoreCase: true ), + ConditionExpression = add.ConditionExpression, + MaxIterations = add.MaxIterations, + AgentConnectionIdOverride = add.AgentConnectionIdOverride, + DependencyMode = Enum.Parse( add.DependencyMode, ignoreCase: true ), + InputVariableName = add.InputVariableName, + OutputVariableName = add.OutputVariableName, + }; + + _ = dbContext.WorkflowSteps.Add( step ); + _ = await dbContext.SaveChangesAsync( ct ); + + tempToReal[add.StepId] = step.Id; + mappings.Add( new StepIdMapping( add.StepId, step.Id ) ); + + if (logger.IsEnabled( LogLevel.Debug )) { + logger.LogDebug( "Batch: created step {RealId} (temp {TempId}) in workflow {WorkflowId}.", + step.Id.ToString( ), + add.StepId.ToString( ), + workflowId.ToString( ) + ); + } + } + + // Helper to resolve temp IDs to real IDs + long ResolveId( long id ) => id < 0 && tempToReal.TryGetValue( id, out long real ) ? real : id; + + // ── Phase 2: Process "Update" operations ── + List updates = [.. request.Operations.Where( o => string.Equals( o.OperationType, "Update", StringComparison.OrdinalIgnoreCase ) )]; + + foreach (StepBatchOperation update in updates) { + long realId = ResolveId( update.StepId ); + WorkflowStep existing = await dbContext.WorkflowSteps.FirstOrDefaultAsync( + s => s.Id == realId, + ct + ) ?? throw new KeyNotFoundException( $"Step {realId} not found during batch update." ); + + if (update.TaskId is not null) { + existing.TaskId = update.TaskId.Value; + } + existing.Order = update.Order; + existing.ControlStatement = Enum.Parse( update.ControlStatement, ignoreCase: true ); + existing.ConditionExpression = update.ConditionExpression; + existing.MaxIterations = update.MaxIterations; + existing.AgentConnectionIdOverride = update.AgentConnectionIdOverride; + existing.DependencyMode = Enum.Parse( update.DependencyMode, ignoreCase: true ); + existing.InputVariableName = update.InputVariableName; + existing.OutputVariableName = update.OutputVariableName; + } + + // ── Phase 3: Process dependency changes ── + foreach (StepBatchOperation op in request.Operations) { + if (op.DependencyChanges is null) { + continue; + } + + long realStepId = ResolveId( op.StepId ); + + foreach (DependencyBatchItem depChange in op.DependencyChanges) { + long realDepId = ResolveId( depChange.DependsOnStepId ); + + if (string.Equals( depChange.OperationType, "Add", StringComparison.OrdinalIgnoreCase )) { + if (realStepId == realDepId) { + continue; + } + + bool alreadyExists = await dbContext.WorkflowStepDependencies + .AnyAsync( d => d.StepId == realStepId && d.DependsOnStepId == realDepId, ct ); + if (alreadyExists) { + continue; + } + + _ = dbContext.WorkflowStepDependencies.Add( new WorkflowStepDependency { + StepId = realStepId, + DependsOnStepId = realDepId, + } ); + } else if (string.Equals( depChange.OperationType, "Delete", StringComparison.OrdinalIgnoreCase )) { + WorkflowStepDependency? dep = await dbContext.WorkflowStepDependencies + .FirstOrDefaultAsync( d => d.StepId == realStepId && d.DependsOnStepId == realDepId, ct ); + if (dep is not null) { + _ = dbContext.WorkflowStepDependencies.Remove( dep ); + } + } + } + } + + // ── Phase 4: Process "Delete" operations (after deps cleaned up) ── + List deletes = [.. request.Operations.Where( o => string.Equals( o.OperationType, "Delete", StringComparison.OrdinalIgnoreCase ) )]; + + foreach (StepBatchOperation delete in deletes) { + long realId = ResolveId( delete.StepId ); + WorkflowStep? step = await dbContext.WorkflowSteps + .FirstOrDefaultAsync( s => s.Id == realId, ct ); + if (step is not null) { + // Remove dependencies first + List deps = await dbContext.WorkflowStepDependencies + .Where( d => d.StepId == realId || d.DependsOnStepId == realId ) + .ToListAsync( ct ); + dbContext.WorkflowStepDependencies.RemoveRange( deps ); + _ = dbContext.WorkflowSteps.Remove( step ); + } + } + + _ = await dbContext.SaveChangesAsync( ct ); + + // ── Phase 5: Validate resulting DAG ── + try { + _ = await ValidateDagAsync( workflowId, ct ); + } catch (InvalidOperationException ex) { + await tx.RollbackAsync( ct ); + return new WorkflowStepBatchResponse( false, [], [ex.Message] ); + } + + await tx.CommitAsync( ct ); + + if (logger.IsEnabled( LogLevel.Information )) { + logger.LogInformation( + "Batch completed for workflow {WorkflowId}: {AddCount} adds, {UpdateCount} updates, {DeleteCount} deletes.", + workflowId.ToString( ), + adds.Count.ToString( ), + updates.Count.ToString( ), + deletes.Count.ToString( ) + ); + } + + return new WorkflowStepBatchResponse( true, mappings, [] ); + } catch (Exception ex) when (ex is not InvalidOperationException) { + await tx.RollbackAsync( ct ); + return new WorkflowStepBatchResponse( false, [], [ex.Message] ); + } + } + /// Validates control flow constraints on a topologically sorted step list. private static void ValidateControlFlow( List sorted ) { HashSet processedIds = []; diff --git a/src/Werkr.Core/packages.lock.json b/src/Werkr.Core/packages.lock.json index 5dd923f..59da9ca 100644 --- a/src/Werkr.Core/packages.lock.json +++ b/src/Werkr.Core/packages.lock.json @@ -1,428 +1,428 @@ -{ - "version": 2, - "dependencies": { - "net10.0": { - "Grpc.Net.Client": { - "type": "Direct", - "requested": "[2.76.0, )", - "resolved": "2.76.0", - "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==", - "dependencies": { - "Grpc.Net.Common": "2.76.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" - } - }, - "Grpc.Tools": { - "type": "Direct", - "requested": "[2.78.0, )", - "resolved": "2.78.0", - "contentHash": "6jPG2gHon+w2PczW8jjrCRnW/g9eEfCdd7aK6mDooptWtuPsV3ZxAwKKEx7LGEDVoT4c2SViRl8Yu3L1XiWIIg==" - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "+5mQrqlBhqNUaPyDmFSNM/qiWStvE9LMxZW1MRF0NhEbO781xNeKryXNR9gGDJ0PmYFDAVoMT9ffX+I15LPFTw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4" - } - }, - "System.Security.Cryptography.ProtectedData": { - "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "LmDnaYkcWkZSlZ07L7YcB6bH8sCiBZ7j28kPbYiXdF6f0iUiN7rxsRORlZGdj5saN/wZIqvF7lDBn/cpjU+e2g==" - }, - "Grpc.Core.Api": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ==" - }, - "Grpc.Net.Common": { - "type": "Transitive", - "resolved": "2.76.0", - "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==", - "dependencies": { - "Grpc.Core.Api": "2.76.0" - } - }, - "Microsoft.AspNetCore.Metadata": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w==" - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite.Core": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==", - "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.4", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "uDRooaV6N3WZ0kdlNPMB68/MdGn/in1Fs7Db7DnIm85RBTPy4P321WO+daAImiYpH5dekjNggDqy1N44WaIlMA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.FileExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Physical": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "NkvJ8aSr3AG30yabjv7ZWwTG/wq5OElNTlNq39Ok2HSEF3TIwAc1f1xnTJlR/GuoJmEgkfT7WBO9YbSXRk41+g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg==" - }, - "Microsoft.Extensions.Diagnostics": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" - } - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileProviders.Physical": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==", - "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileSystemGlobbing": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.FileSystemGlobbing": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg==" - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "S8+6fCuMOhJZGk8sGFtOy3VsF9mk9x4UOL59GM91REiA/fmCDjunKKIw4RmStG87qyXPfxelDJf2pXIbTuaBdw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg==" - }, - "Microsoft.IdentityModel.Abstractions": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" - }, - "Microsoft.IdentityModel.Logging": { - "type": "Transitive", - "resolved": "8.16.0", - "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", - "dependencies": { - "Microsoft.IdentityModel.Abstractions": "8.16.0" - } - }, - "Npgsql": { - "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "10.0.0" - } - }, - "SQLitePCLRaw.bundle_e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", - "dependencies": { - "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", - "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" - } - }, - "SQLitePCLRaw.core": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" - }, - "SQLitePCLRaw.lib.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" - }, - "SQLitePCLRaw.provider.e_sqlite3": { - "type": "Transitive", - "resolved": "2.1.11", - "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", - "dependencies": { - "SQLitePCLRaw.core": "2.1.11" - } - }, - "werkr.common": { - "type": "Project", - "dependencies": { - "Google.Protobuf": "[3.34.0, )", - "Microsoft.AspNetCore.Authorization": "[10.0.4, )", - "Microsoft.Extensions.Configuration.Json": "[10.0.4, )", - "Microsoft.IdentityModel.Tokens": "[8.16.0, )", - "Werkr.Common.Configuration": "[1.0.0, )" - } - }, - "werkr.common.configuration": { - "type": "Project" - }, - "werkr.data": { - "type": "Project", - "dependencies": { - "EFCore.NamingConventions": "[10.0.1, )", - "Microsoft.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )", - "Werkr.Common": "[1.0.0, )" - } - }, - "EFCore.NamingConventions": { - "type": "CentralTransitive", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" - } - }, - "Google.Protobuf": { - "type": "CentralTransitive", - "requested": "[3.34.0, )", - "resolved": "3.34.0", - "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" - }, - "Microsoft.AspNetCore.Authorization": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==", - "dependencies": { - "Microsoft.AspNetCore.Metadata": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" - } - }, - "Microsoft.Data.Sqlite.Core": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==", - "dependencies": { - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.EntityFrameworkCore": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" - } - }, - "Microsoft.EntityFrameworkCore.Sqlite": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", - "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", - "SQLitePCLRaw.core": "2.1.11" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" - } - }, - "Microsoft.Extensions.Configuration.Json": { - "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "CentralTransitive", - "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" - } - }, - "Microsoft.IdentityModel.Tokens": { - "type": "CentralTransitive", - "requested": "[8.16.0, )", - "resolved": "8.16.0", - "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "10.0.0", - "Microsoft.IdentityModel.Logging": "8.16.0" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)", - "Npgsql": "10.0.0" - } - } - } - } +{ + "version": 2, + "dependencies": { + "net10.0": { + "Grpc.Net.Client": { + "type": "Direct", + "requested": "[2.76.0, )", + "resolved": "2.76.0", + "contentHash": "K1oldmqw2+Gn69nGRzZLhqSiUZwelX1GrBu/cUl9wNf1C0uB61vFS6JcxUUv9P8VoUJhFsmV44JA6lI2EUt4xw==", + "dependencies": { + "Grpc.Net.Common": "2.76.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "Grpc.Tools": { + "type": "Direct", + "requested": "[2.78.0, )", + "resolved": "2.78.0", + "contentHash": "6jPG2gHon+w2PczW8jjrCRnW/g9eEfCdd7aK6mDooptWtuPsV3ZxAwKKEx7LGEDVoT4c2SViRl8Yu3L1XiWIIg==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "kxR4O/8o32eNN3m4qbLe3UifYqeyEpallCyVAsLvL5ZFJVyT3JCb+9du/WHfC09VyJh1Q+p/Gd4+AwM7Rz4acg==" + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "cSxC2tdnFdXXuBgIn1pjc4YBx7LXTCp4M0qn+SMBS35VWZY+cEQYLWTBDDhdBH1HzU7BV+ncVZlniGQHMpRJKQ==" + }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.76.0", + "contentHash": "bZpiMVYgvpB44/wBh1RotrkqC7bg2FOasLri2GhR3hMKyzsiTxCoDE49YjPrJeFc4RW0wS8u+EInI09sjxVFRA==", + "dependencies": { + "Grpc.Core.Api": "2.76.0" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.16.0", + "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.16.0" + } + }, + "Npgsql": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "werkr.common": { + "type": "Project", + "dependencies": { + "Google.Protobuf": "[3.34.0, )", + "Microsoft.AspNetCore.Authorization": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Json": "[10.0.5, )", + "Microsoft.IdentityModel.Tokens": "[8.16.0, )", + "Werkr.Common.Configuration": "[1.0.0, )" + } + }, + "werkr.common.configuration": { + "type": "Project" + }, + "werkr.data": { + "type": "Project", + "dependencies": { + "EFCore.NamingConventions": "[10.0.1, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "Werkr.Common": "[1.0.0, )" + } + }, + "EFCore.NamingConventions": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + } + }, + "Google.Protobuf": { + "type": "CentralTransitive", + "requested": "[3.34.0, )", + "resolved": "3.34.0", + "contentHash": "a5US9akiNczS5kC7qBqYqJmnxHVQDITZD6GRRbwGHk/oa17EwOGE3PHIWFVeHTqCctq8mVjLSelwsxCkYYBinA==" + }, + "Microsoft.AspNetCore.Authorization": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Data.Sqlite.Core": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.3, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "CentralTransitive", + "requested": "[8.16.0, )", + "resolved": "8.16.0", + "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Microsoft.IdentityModel.Logging": "8.16.0" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + } + } + } } \ No newline at end of file diff --git a/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.cs b/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.cs index 4000d48..2ee2584 100644 --- a/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.cs +++ b/src/Werkr.Data.Identity/Migrations/Postgres/20260312050754_InitialCreate.cs @@ -1,366 +1,340 @@ -using System; using Microsoft.EntityFrameworkCore.Migrations; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; #nullable disable -namespace Werkr.Data.Identity.Migrations.Postgres -{ +namespace Werkr.Data.Identity.Migrations.Postgres; +/// +public partial class InitialCreate : Migration { /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "werkr_identity"); - - migrationBuilder.CreateTable( - name: "config_settings", - schema: "werkr_identity", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - default_key_size = table.Column(type: "integer", nullable: false), - server_name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - allow_registration = table.Column(type: "boolean", nullable: false), - polling_interval_seconds = table.Column(type: "integer", nullable: false), - run_detail_polling_interval_seconds = table.Column(type: "integer", nullable: false), - created = table.Column(type: "timestamp with time zone", nullable: false), - last_updated = table.Column(type: "timestamp with time zone", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_config_settings", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "roles", - schema: "werkr_identity", - columns: table => new - { - id = table.Column(type: "text", nullable: false), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - normalized_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - concurrency_stamp = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_roles", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "users", - schema: "werkr_identity", - columns: table => new - { - id = table.Column(type: "text", nullable: false), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - enabled = table.Column(type: "boolean", nullable: false), - change_password = table.Column(type: "boolean", nullable: false), - requires2fa = table.Column(type: "boolean", nullable: false), - last_login_utc = table.Column(type: "timestamp with time zone", nullable: true), - user_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - normalized_user_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - normalized_email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - email_confirmed = table.Column(type: "boolean", nullable: false), - password_hash = table.Column(type: "text", nullable: true), - security_stamp = table.Column(type: "text", nullable: true), - concurrency_stamp = table.Column(type: "text", nullable: true), - phone_number = table.Column(type: "text", nullable: true), - phone_number_confirmed = table.Column(type: "boolean", nullable: false), - two_factor_enabled = table.Column(type: "boolean", nullable: false), - lockout_end = table.Column(type: "timestamp with time zone", nullable: true), - lockout_enabled = table.Column(type: "boolean", nullable: false), - access_failed_count = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_users", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "role_claims", - schema: "werkr_identity", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - role_id = table.Column(type: "text", nullable: false), - claim_type = table.Column(type: "text", nullable: true), - claim_value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_role_claims", x => x.id); - table.ForeignKey( - name: "fk_role_claims_roles_role_id", - column: x => x.role_id, - principalSchema: "werkr_identity", - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "role_permissions", - schema: "werkr_identity", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - role_id = table.Column(type: "text", nullable: false), - permission = table.Column(type: "character varying(64)", maxLength: 64, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_role_permissions", x => x.id); - table.ForeignKey( - name: "fk_role_permissions_roles_role_id", - column: x => x.role_id, - principalSchema: "werkr_identity", - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "api_keys", - schema: "werkr_identity", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - key_hash = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - key_prefix = table.Column(type: "character varying(16)", maxLength: 16, nullable: false), - name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - role = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - created_by_user_id = table.Column(type: "text", nullable: false), - created_utc = table.Column(type: "timestamp with time zone", nullable: false), - expires_utc = table.Column(type: "timestamp with time zone", nullable: true), - is_revoked = table.Column(type: "boolean", nullable: false), - last_used_utc = table.Column(type: "timestamp with time zone", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_api_keys", x => x.id); - table.ForeignKey( - name: "fk_api_keys_users_created_by_user_id", - column: x => x.created_by_user_id, - principalSchema: "werkr_identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_claims", - schema: "werkr_identity", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - user_id = table.Column(type: "text", nullable: false), - claim_type = table.Column(type: "text", nullable: true), - claim_value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_user_claims", x => x.id); - table.ForeignKey( - name: "fk_user_claims_users_user_id", - column: x => x.user_id, - principalSchema: "werkr_identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_logins", - schema: "werkr_identity", - columns: table => new - { - login_provider = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - provider_key = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - provider_display_name = table.Column(type: "text", nullable: true), - user_id = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_user_logins", x => new { x.login_provider, x.provider_key }); - table.ForeignKey( - name: "fk_user_logins_users_user_id", - column: x => x.user_id, - principalSchema: "werkr_identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_roles", - schema: "werkr_identity", - columns: table => new - { - user_id = table.Column(type: "text", nullable: false), - role_id = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_user_roles", x => new { x.user_id, x.role_id }); - table.ForeignKey( - name: "fk_user_roles_roles_role_id", - column: x => x.role_id, - principalSchema: "werkr_identity", - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_user_roles_users_user_id", - column: x => x.user_id, - principalSchema: "werkr_identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_tokens", - schema: "werkr_identity", - columns: table => new - { - user_id = table.Column(type: "text", nullable: false), - login_provider = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - value = table.Column(type: "text", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_user_tokens", x => new { x.user_id, x.login_provider, x.name }); - table.ForeignKey( - name: "fk_user_tokens_users_user_id", - column: x => x.user_id, - principalSchema: "werkr_identity", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "ix_api_keys_created_by_user_id", - schema: "werkr_identity", - table: "api_keys", - column: "created_by_user_id"); - - migrationBuilder.CreateIndex( - name: "ix_api_keys_key_hash", - schema: "werkr_identity", - table: "api_keys", - column: "key_hash", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_api_keys_key_prefix", - schema: "werkr_identity", - table: "api_keys", - column: "key_prefix"); - - migrationBuilder.CreateIndex( - name: "ix_role_claims_role_id", - schema: "werkr_identity", - table: "role_claims", - column: "role_id"); - - migrationBuilder.CreateIndex( - name: "ix_role_permissions_role_id_permission", - schema: "werkr_identity", - table: "role_permissions", - columns: new[] { "role_id", "permission" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "RoleNameIndex", - schema: "werkr_identity", - table: "roles", - column: "normalized_name", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_user_claims_user_id", - schema: "werkr_identity", - table: "user_claims", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "ix_user_logins_user_id", - schema: "werkr_identity", - table: "user_logins", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "ix_user_roles_role_id", - schema: "werkr_identity", - table: "user_roles", - column: "role_id"); - - migrationBuilder.CreateIndex( - name: "EmailIndex", - schema: "werkr_identity", - table: "users", - column: "normalized_email"); - - migrationBuilder.CreateIndex( - name: "UserNameIndex", - schema: "werkr_identity", - table: "users", - column: "normalized_user_name", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "api_keys", - schema: "werkr_identity"); - - migrationBuilder.DropTable( - name: "config_settings", - schema: "werkr_identity"); - - migrationBuilder.DropTable( - name: "role_claims", - schema: "werkr_identity"); - - migrationBuilder.DropTable( - name: "role_permissions", - schema: "werkr_identity"); - - migrationBuilder.DropTable( - name: "user_claims", - schema: "werkr_identity"); - - migrationBuilder.DropTable( - name: "user_logins", - schema: "werkr_identity"); - - migrationBuilder.DropTable( - name: "user_roles", - schema: "werkr_identity"); - - migrationBuilder.DropTable( - name: "user_tokens", - schema: "werkr_identity"); - - migrationBuilder.DropTable( - name: "roles", - schema: "werkr_identity"); - - migrationBuilder.DropTable( - name: "users", - schema: "werkr_identity"); - } + protected override void Up( MigrationBuilder migrationBuilder ) { + _ = migrationBuilder.EnsureSchema( + name: "werkr_identity" ); + + _ = migrationBuilder.CreateTable( + name: "config_settings", + schema: "werkr_identity", + columns: table => new { + id = table.Column( type: "uuid", nullable: false ), + default_key_size = table.Column( type: "integer", nullable: false ), + server_name = table.Column( type: "character varying(200)", maxLength: 200, nullable: false ), + allow_registration = table.Column( type: "boolean", nullable: false ), + polling_interval_seconds = table.Column( type: "integer", nullable: false ), + run_detail_polling_interval_seconds = table.Column( type: "integer", nullable: false ), + created = table.Column( type: "timestamp with time zone", nullable: false ), + last_updated = table.Column( type: "timestamp with time zone", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_config_settings", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "roles", + schema: "werkr_identity", + columns: table => new { + id = table.Column( type: "text", nullable: false ), + name = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), + normalized_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), + concurrency_stamp = table.Column( type: "text", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_roles", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "users", + schema: "werkr_identity", + columns: table => new { + id = table.Column( type: "text", nullable: false ), + name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), + enabled = table.Column( type: "boolean", nullable: false ), + change_password = table.Column( type: "boolean", nullable: false ), + requires2fa = table.Column( type: "boolean", nullable: false ), + last_login_utc = table.Column( type: "timestamp with time zone", nullable: true ), + user_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), + normalized_user_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), + email = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), + normalized_email = table.Column( type: "character varying(256)", maxLength: 256, nullable: true ), + email_confirmed = table.Column( type: "boolean", nullable: false ), + password_hash = table.Column( type: "text", nullable: true ), + security_stamp = table.Column( type: "text", nullable: true ), + concurrency_stamp = table.Column( type: "text", nullable: true ), + phone_number = table.Column( type: "text", nullable: true ), + phone_number_confirmed = table.Column( type: "boolean", nullable: false ), + two_factor_enabled = table.Column( type: "boolean", nullable: false ), + lockout_end = table.Column( type: "timestamp with time zone", nullable: true ), + lockout_enabled = table.Column( type: "boolean", nullable: false ), + access_failed_count = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_users", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "role_claims", + schema: "werkr_identity", + columns: table => new { + id = table.Column( type: "integer", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), + role_id = table.Column( type: "text", nullable: false ), + claim_type = table.Column( type: "text", nullable: true ), + claim_value = table.Column( type: "text", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_role_claims", x => x.id ); + _ = table.ForeignKey( + name: "fk_role_claims_roles_role_id", + column: x => x.role_id, + principalSchema: "werkr_identity", + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "role_permissions", + schema: "werkr_identity", + columns: table => new { + id = table.Column( type: "bigint", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), + role_id = table.Column( type: "text", nullable: false ), + permission = table.Column( type: "character varying(64)", maxLength: 64, nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_role_permissions", x => x.id ); + _ = table.ForeignKey( + name: "fk_role_permissions_roles_role_id", + column: x => x.role_id, + principalSchema: "werkr_identity", + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "api_keys", + schema: "werkr_identity", + columns: table => new { + id = table.Column( type: "uuid", nullable: false ), + key_hash = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), + key_prefix = table.Column( type: "character varying(16)", maxLength: 16, nullable: false ), + name = table.Column( type: "character varying(200)", maxLength: 200, nullable: false ), + role = table.Column( type: "character varying(64)", maxLength: 64, nullable: false ), + created_by_user_id = table.Column( type: "text", nullable: false ), + created_utc = table.Column( type: "timestamp with time zone", nullable: false ), + expires_utc = table.Column( type: "timestamp with time zone", nullable: true ), + is_revoked = table.Column( type: "boolean", nullable: false ), + last_used_utc = table.Column( type: "timestamp with time zone", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_api_keys", x => x.id ); + _ = table.ForeignKey( + name: "fk_api_keys_users_created_by_user_id", + column: x => x.created_by_user_id, + principalSchema: "werkr_identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "user_claims", + schema: "werkr_identity", + columns: table => new { + id = table.Column( type: "integer", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), + user_id = table.Column( type: "text", nullable: false ), + claim_type = table.Column( type: "text", nullable: true ), + claim_value = table.Column( type: "text", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_user_claims", x => x.id ); + _ = table.ForeignKey( + name: "fk_user_claims_users_user_id", + column: x => x.user_id, + principalSchema: "werkr_identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "user_logins", + schema: "werkr_identity", + columns: table => new { + login_provider = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), + provider_key = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), + provider_display_name = table.Column( type: "text", nullable: true ), + user_id = table.Column( type: "text", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_user_logins", x => new { x.login_provider, x.provider_key } ); + _ = table.ForeignKey( + name: "fk_user_logins_users_user_id", + column: x => x.user_id, + principalSchema: "werkr_identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "user_roles", + schema: "werkr_identity", + columns: table => new { + user_id = table.Column( type: "text", nullable: false ), + role_id = table.Column( type: "text", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_user_roles", x => new { x.user_id, x.role_id } ); + _ = table.ForeignKey( + name: "fk_user_roles_roles_role_id", + column: x => x.role_id, + principalSchema: "werkr_identity", + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_user_roles_users_user_id", + column: x => x.user_id, + principalSchema: "werkr_identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "user_tokens", + schema: "werkr_identity", + columns: table => new { + user_id = table.Column( type: "text", nullable: false ), + login_provider = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), + name = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), + value = table.Column( type: "text", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_user_tokens", x => new { x.user_id, x.login_provider, x.name } ); + _ = table.ForeignKey( + name: "fk_user_tokens_users_user_id", + column: x => x.user_id, + principalSchema: "werkr_identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateIndex( + name: "ix_api_keys_created_by_user_id", + schema: "werkr_identity", + table: "api_keys", + column: "created_by_user_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_api_keys_key_hash", + schema: "werkr_identity", + table: "api_keys", + column: "key_hash", + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_api_keys_key_prefix", + schema: "werkr_identity", + table: "api_keys", + column: "key_prefix" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_role_claims_role_id", + schema: "werkr_identity", + table: "role_claims", + column: "role_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_role_permissions_role_id_permission", + schema: "werkr_identity", + table: "role_permissions", + columns: ["role_id", "permission"], + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "RoleNameIndex", + schema: "werkr_identity", + table: "roles", + column: "normalized_name", + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_user_claims_user_id", + schema: "werkr_identity", + table: "user_claims", + column: "user_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_user_logins_user_id", + schema: "werkr_identity", + table: "user_logins", + column: "user_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_user_roles_role_id", + schema: "werkr_identity", + table: "user_roles", + column: "role_id" ); + + _ = migrationBuilder.CreateIndex( + name: "EmailIndex", + schema: "werkr_identity", + table: "users", + column: "normalized_email" ); + + _ = migrationBuilder.CreateIndex( + name: "UserNameIndex", + schema: "werkr_identity", + table: "users", + column: "normalized_user_name", + unique: true ); + } + + /// + protected override void Down( MigrationBuilder migrationBuilder ) { + _ = migrationBuilder.DropTable( + name: "api_keys", + schema: "werkr_identity" ); + + _ = migrationBuilder.DropTable( + name: "config_settings", + schema: "werkr_identity" ); + + _ = migrationBuilder.DropTable( + name: "role_claims", + schema: "werkr_identity" ); + + _ = migrationBuilder.DropTable( + name: "role_permissions", + schema: "werkr_identity" ); + + _ = migrationBuilder.DropTable( + name: "user_claims", + schema: "werkr_identity" ); + + _ = migrationBuilder.DropTable( + name: "user_logins", + schema: "werkr_identity" ); + + _ = migrationBuilder.DropTable( + name: "user_roles", + schema: "werkr_identity" ); + + _ = migrationBuilder.DropTable( + name: "user_tokens", + schema: "werkr_identity" ); + + _ = migrationBuilder.DropTable( + name: "roles", + schema: "werkr_identity" ); + + _ = migrationBuilder.DropTable( + name: "users", + schema: "werkr_identity" ); } } diff --git a/src/Werkr.Data.Identity/Migrations/Sqlite/20260312050800_InitialCreate.cs b/src/Werkr.Data.Identity/Migrations/Sqlite/20260312050800_InitialCreate.cs index 76bd699..80dc900 100644 --- a/src/Werkr.Data.Identity/Migrations/Sqlite/20260312050800_InitialCreate.cs +++ b/src/Werkr.Data.Identity/Migrations/Sqlite/20260312050800_InitialCreate.cs @@ -1,323 +1,297 @@ -using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Werkr.Data.Identity.Migrations.Sqlite -{ +namespace Werkr.Data.Identity.Migrations.Sqlite; +/// +public partial class InitialCreate : Migration { /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "config_settings", - columns: table => new - { - id = table.Column(type: "TEXT", nullable: false), - default_key_size = table.Column(type: "INTEGER", nullable: false), - server_name = table.Column(type: "TEXT", maxLength: 200, nullable: false), - allow_registration = table.Column(type: "INTEGER", nullable: false), - polling_interval_seconds = table.Column(type: "INTEGER", nullable: false), - run_detail_polling_interval_seconds = table.Column(type: "INTEGER", nullable: false), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_config_settings", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "roles", - columns: table => new - { - id = table.Column(type: "TEXT", nullable: false), - name = table.Column(type: "TEXT", maxLength: 256, nullable: true), - normalized_name = table.Column(type: "TEXT", maxLength: 256, nullable: true), - concurrency_stamp = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_roles", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "users", - columns: table => new - { - id = table.Column(type: "TEXT", nullable: false), - name = table.Column(type: "TEXT", maxLength: 256, nullable: false), - enabled = table.Column(type: "INTEGER", nullable: false), - change_password = table.Column(type: "INTEGER", nullable: false), - requires2fa = table.Column(type: "INTEGER", nullable: false), - last_login_utc = table.Column(type: "TEXT", nullable: true), - user_name = table.Column(type: "TEXT", maxLength: 256, nullable: true), - normalized_user_name = table.Column(type: "TEXT", maxLength: 256, nullable: true), - email = table.Column(type: "TEXT", maxLength: 256, nullable: true), - normalized_email = table.Column(type: "TEXT", maxLength: 256, nullable: true), - email_confirmed = table.Column(type: "INTEGER", nullable: false), - password_hash = table.Column(type: "TEXT", nullable: true), - security_stamp = table.Column(type: "TEXT", nullable: true), - concurrency_stamp = table.Column(type: "TEXT", nullable: true), - phone_number = table.Column(type: "TEXT", nullable: true), - phone_number_confirmed = table.Column(type: "INTEGER", nullable: false), - two_factor_enabled = table.Column(type: "INTEGER", nullable: false), - lockout_end = table.Column(type: "TEXT", nullable: true), - lockout_enabled = table.Column(type: "INTEGER", nullable: false), - access_failed_count = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_users", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "role_claims", - columns: table => new - { - id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - role_id = table.Column(type: "TEXT", nullable: false), - claim_type = table.Column(type: "TEXT", nullable: true), - claim_value = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_role_claims", x => x.id); - table.ForeignKey( - name: "fk_role_claims_roles_role_id", - column: x => x.role_id, - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "role_permissions", - columns: table => new - { - id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - role_id = table.Column(type: "TEXT", nullable: false), - permission = table.Column(type: "TEXT", maxLength: 64, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_role_permissions", x => x.id); - table.ForeignKey( - name: "fk_role_permissions_roles_role_id", - column: x => x.role_id, - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "api_keys", - columns: table => new - { - id = table.Column(type: "TEXT", nullable: false), - key_hash = table.Column(type: "TEXT", maxLength: 128, nullable: false), - key_prefix = table.Column(type: "TEXT", maxLength: 16, nullable: false), - name = table.Column(type: "TEXT", maxLength: 200, nullable: false), - role = table.Column(type: "TEXT", maxLength: 64, nullable: false), - created_by_user_id = table.Column(type: "TEXT", nullable: false), - created_utc = table.Column(type: "TEXT", nullable: false), - expires_utc = table.Column(type: "TEXT", nullable: true), - is_revoked = table.Column(type: "INTEGER", nullable: false), - last_used_utc = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_api_keys", x => x.id); - table.ForeignKey( - name: "fk_api_keys_users_created_by_user_id", - column: x => x.created_by_user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_claims", - columns: table => new - { - id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - user_id = table.Column(type: "TEXT", nullable: false), - claim_type = table.Column(type: "TEXT", nullable: true), - claim_value = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_user_claims", x => x.id); - table.ForeignKey( - name: "fk_user_claims_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_logins", - columns: table => new - { - login_provider = table.Column(type: "TEXT", maxLength: 128, nullable: false), - provider_key = table.Column(type: "TEXT", maxLength: 128, nullable: false), - provider_display_name = table.Column(type: "TEXT", nullable: true), - user_id = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_user_logins", x => new { x.login_provider, x.provider_key }); - table.ForeignKey( - name: "fk_user_logins_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_roles", - columns: table => new - { - user_id = table.Column(type: "TEXT", nullable: false), - role_id = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_user_roles", x => new { x.user_id, x.role_id }); - table.ForeignKey( - name: "fk_user_roles_roles_role_id", - column: x => x.role_id, - principalTable: "roles", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_user_roles_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "user_tokens", - columns: table => new - { - user_id = table.Column(type: "TEXT", nullable: false), - login_provider = table.Column(type: "TEXT", maxLength: 128, nullable: false), - name = table.Column(type: "TEXT", maxLength: 128, nullable: false), - value = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_user_tokens", x => new { x.user_id, x.login_provider, x.name }); - table.ForeignKey( - name: "fk_user_tokens_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "ix_api_keys_created_by_user_id", - table: "api_keys", - column: "created_by_user_id"); - - migrationBuilder.CreateIndex( - name: "ix_api_keys_key_hash", - table: "api_keys", - column: "key_hash", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_api_keys_key_prefix", - table: "api_keys", - column: "key_prefix"); - - migrationBuilder.CreateIndex( - name: "ix_role_claims_role_id", - table: "role_claims", - column: "role_id"); - - migrationBuilder.CreateIndex( - name: "ix_role_permissions_role_id_permission", - table: "role_permissions", - columns: new[] { "role_id", "permission" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "RoleNameIndex", - table: "roles", - column: "normalized_name", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_user_claims_user_id", - table: "user_claims", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "ix_user_logins_user_id", - table: "user_logins", - column: "user_id"); - - migrationBuilder.CreateIndex( - name: "ix_user_roles_role_id", - table: "user_roles", - column: "role_id"); - - migrationBuilder.CreateIndex( - name: "EmailIndex", - table: "users", - column: "normalized_email"); - - migrationBuilder.CreateIndex( - name: "UserNameIndex", - table: "users", - column: "normalized_user_name", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "api_keys"); - - migrationBuilder.DropTable( - name: "config_settings"); - - migrationBuilder.DropTable( - name: "role_claims"); - - migrationBuilder.DropTable( - name: "role_permissions"); - - migrationBuilder.DropTable( - name: "user_claims"); - - migrationBuilder.DropTable( - name: "user_logins"); - - migrationBuilder.DropTable( - name: "user_roles"); - - migrationBuilder.DropTable( - name: "user_tokens"); - - migrationBuilder.DropTable( - name: "roles"); - - migrationBuilder.DropTable( - name: "users"); - } + protected override void Up( MigrationBuilder migrationBuilder ) { + _ = migrationBuilder.CreateTable( + name: "config_settings", + columns: table => new { + id = table.Column( type: "TEXT", nullable: false ), + default_key_size = table.Column( type: "INTEGER", nullable: false ), + server_name = table.Column( type: "TEXT", maxLength: 200, nullable: false ), + allow_registration = table.Column( type: "INTEGER", nullable: false ), + polling_interval_seconds = table.Column( type: "INTEGER", nullable: false ), + run_detail_polling_interval_seconds = table.Column( type: "INTEGER", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_config_settings", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "roles", + columns: table => new { + id = table.Column( type: "TEXT", nullable: false ), + name = table.Column( type: "TEXT", maxLength: 256, nullable: true ), + normalized_name = table.Column( type: "TEXT", maxLength: 256, nullable: true ), + concurrency_stamp = table.Column( type: "TEXT", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_roles", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "users", + columns: table => new { + id = table.Column( type: "TEXT", nullable: false ), + name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), + enabled = table.Column( type: "INTEGER", nullable: false ), + change_password = table.Column( type: "INTEGER", nullable: false ), + requires2fa = table.Column( type: "INTEGER", nullable: false ), + last_login_utc = table.Column( type: "TEXT", nullable: true ), + user_name = table.Column( type: "TEXT", maxLength: 256, nullable: true ), + normalized_user_name = table.Column( type: "TEXT", maxLength: 256, nullable: true ), + email = table.Column( type: "TEXT", maxLength: 256, nullable: true ), + normalized_email = table.Column( type: "TEXT", maxLength: 256, nullable: true ), + email_confirmed = table.Column( type: "INTEGER", nullable: false ), + password_hash = table.Column( type: "TEXT", nullable: true ), + security_stamp = table.Column( type: "TEXT", nullable: true ), + concurrency_stamp = table.Column( type: "TEXT", nullable: true ), + phone_number = table.Column( type: "TEXT", nullable: true ), + phone_number_confirmed = table.Column( type: "INTEGER", nullable: false ), + two_factor_enabled = table.Column( type: "INTEGER", nullable: false ), + lockout_end = table.Column( type: "TEXT", nullable: true ), + lockout_enabled = table.Column( type: "INTEGER", nullable: false ), + access_failed_count = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_users", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "role_claims", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + role_id = table.Column( type: "TEXT", nullable: false ), + claim_type = table.Column( type: "TEXT", nullable: true ), + claim_value = table.Column( type: "TEXT", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_role_claims", x => x.id ); + _ = table.ForeignKey( + name: "fk_role_claims_roles_role_id", + column: x => x.role_id, + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "role_permissions", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + role_id = table.Column( type: "TEXT", nullable: false ), + permission = table.Column( type: "TEXT", maxLength: 64, nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_role_permissions", x => x.id ); + _ = table.ForeignKey( + name: "fk_role_permissions_roles_role_id", + column: x => x.role_id, + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "api_keys", + columns: table => new { + id = table.Column( type: "TEXT", nullable: false ), + key_hash = table.Column( type: "TEXT", maxLength: 128, nullable: false ), + key_prefix = table.Column( type: "TEXT", maxLength: 16, nullable: false ), + name = table.Column( type: "TEXT", maxLength: 200, nullable: false ), + role = table.Column( type: "TEXT", maxLength: 64, nullable: false ), + created_by_user_id = table.Column( type: "TEXT", nullable: false ), + created_utc = table.Column( type: "TEXT", nullable: false ), + expires_utc = table.Column( type: "TEXT", nullable: true ), + is_revoked = table.Column( type: "INTEGER", nullable: false ), + last_used_utc = table.Column( type: "TEXT", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_api_keys", x => x.id ); + _ = table.ForeignKey( + name: "fk_api_keys_users_created_by_user_id", + column: x => x.created_by_user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "user_claims", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + user_id = table.Column( type: "TEXT", nullable: false ), + claim_type = table.Column( type: "TEXT", nullable: true ), + claim_value = table.Column( type: "TEXT", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_user_claims", x => x.id ); + _ = table.ForeignKey( + name: "fk_user_claims_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "user_logins", + columns: table => new { + login_provider = table.Column( type: "TEXT", maxLength: 128, nullable: false ), + provider_key = table.Column( type: "TEXT", maxLength: 128, nullable: false ), + provider_display_name = table.Column( type: "TEXT", nullable: true ), + user_id = table.Column( type: "TEXT", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_user_logins", x => new { x.login_provider, x.provider_key } ); + _ = table.ForeignKey( + name: "fk_user_logins_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "user_roles", + columns: table => new { + user_id = table.Column( type: "TEXT", nullable: false ), + role_id = table.Column( type: "TEXT", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_user_roles", x => new { x.user_id, x.role_id } ); + _ = table.ForeignKey( + name: "fk_user_roles_roles_role_id", + column: x => x.role_id, + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_user_roles_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "user_tokens", + columns: table => new { + user_id = table.Column( type: "TEXT", nullable: false ), + login_provider = table.Column( type: "TEXT", maxLength: 128, nullable: false ), + name = table.Column( type: "TEXT", maxLength: 128, nullable: false ), + value = table.Column( type: "TEXT", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_user_tokens", x => new { x.user_id, x.login_provider, x.name } ); + _ = table.ForeignKey( + name: "fk_user_tokens_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateIndex( + name: "ix_api_keys_created_by_user_id", + table: "api_keys", + column: "created_by_user_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_api_keys_key_hash", + table: "api_keys", + column: "key_hash", + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_api_keys_key_prefix", + table: "api_keys", + column: "key_prefix" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_role_claims_role_id", + table: "role_claims", + column: "role_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_role_permissions_role_id_permission", + table: "role_permissions", + columns: ["role_id", "permission"], + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "roles", + column: "normalized_name", + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_user_claims_user_id", + table: "user_claims", + column: "user_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_user_logins_user_id", + table: "user_logins", + column: "user_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_user_roles_role_id", + table: "user_roles", + column: "role_id" ); + + _ = migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "users", + column: "normalized_email" ); + + _ = migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "users", + column: "normalized_user_name", + unique: true ); + } + + /// + protected override void Down( MigrationBuilder migrationBuilder ) { + _ = migrationBuilder.DropTable( + name: "api_keys" ); + + _ = migrationBuilder.DropTable( + name: "config_settings" ); + + _ = migrationBuilder.DropTable( + name: "role_claims" ); + + _ = migrationBuilder.DropTable( + name: "role_permissions" ); + + _ = migrationBuilder.DropTable( + name: "user_claims" ); + + _ = migrationBuilder.DropTable( + name: "user_logins" ); + + _ = migrationBuilder.DropTable( + name: "user_roles" ); + + _ = migrationBuilder.DropTable( + name: "user_tokens" ); + + _ = migrationBuilder.DropTable( + name: "roles" ); + + _ = migrationBuilder.DropTable( + name: "users" ); } } diff --git a/src/Werkr.Data.Identity/packages.lock.json b/src/Werkr.Data.Identity/packages.lock.json index 7625bf0..a875786 100644 --- a/src/Werkr.Data.Identity/packages.lock.json +++ b/src/Werkr.Data.Identity/packages.lock.json @@ -4,35 +4,35 @@ "net10.0": { "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "xcuRo8Ubxwf3xay2z7mD3b3TmEnvMhSLzvfx5cqmk3V10oxTDqgIyZ8C5qav+iAc27In/TXteSJ6kS2iZpah7w==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "oo1uauTwgcnhYgituZ2nI3wJ8XN9z76ggu2zkOJu1BCfOOsqr+g08Kr4MOiMuXEhwySkpIV+MVoC25hC1288NA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "10.0.4" + "Microsoft.EntityFrameworkCore.Relational": "10.0.5" } }, "Microsoft.AspNetCore.Identity.UI": { "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "ZJlXBj2KgSKeR7WWl/1XIOiJLgvxKL7v0/YEkfWoCQQdVqLPz2sEsg3EcvqiQ6w2oR/gjdqDsCa2zkKgHYMnNg==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "qzYhpJ4Uxng18hmuKqwqydZaPzItrv9WOwNULJ2ka952TZKlOQkERTSkVO8G/19WiRtoznZatrcRyOvppYRGFA==", "dependencies": { - "Microsoft.Extensions.FileProviders.Embedded": "10.0.4" + "Microsoft.Extensions.FileProviders.Embedded": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Design": { "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "FmiUU5xdu1chVxnmsu/mEpCKVQ5+lvIxdP0194lE7HfoU1jO4z/9qnWZpd0kSkVve4gOnRm1lE20kkhlMqJJIg==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "gm6f0cC2w/2tcd4GeZJqEMruTercpIJfO5sSAFLtqTqblDBHgAFk70xwshUIUVX4I6sZwdEUSd1YxoKFk1AL0w==", "dependencies": { "Humanizer.Core": "2.14.1", "Microsoft.Build.Framework": "18.0.2", "Microsoft.CodeAnalysis.CSharp": "5.0.0", "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", "Mono.TextTemplating": "3.0.0", "Newtonsoft.Json": "13.0.3" } @@ -109,42 +109,42 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw==" + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA==" + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4" + "Microsoft.EntityFrameworkCore": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.4", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", "SQLitePCLRaw.core": "2.1.11" } }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg==" + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "yM9BTzWdqLDk3v08UcmRJqUAbFiiP/cYFkkoHRphDv+YdMFS7cIc6YL2cbfaFlLfBomBA4Zx8+8lB6lRxvDFxA==" + "resolved": "10.0.5", + "contentHash": "DQzkbLFNfwmjxErAnWyZTTyBd4cMo6vmGteM4Ayedhk5Pccm2VuKoeKcOZjJG1T+dYK6lMCNk2L7Ftl7dLhgqg==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -179,8 +179,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==" + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==" }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", @@ -277,9 +277,9 @@ "type": "Project", "dependencies": { "EFCore.NamingConventions": "[10.0.1, )", - "Microsoft.EntityFrameworkCore": "[10.0.4, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.4, )", - "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", "Werkr.Common": "[1.0.0, )" } }, @@ -302,30 +302,30 @@ "Microsoft.Data.Sqlite.Core": { "type": "CentralTransitive", "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", "dependencies": { "SQLitePCLRaw.core": "2.1.11" } }, "Microsoft.EntityFrameworkCore": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4" + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", "SQLitePCLRaw.core": "2.1.11" } @@ -341,13 +341,13 @@ }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "CentralTransitive", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)", - "Npgsql": "10.0.0" + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" } } } diff --git a/src/Werkr.Data/Entities/Settings/SavedFilter.cs b/src/Werkr.Data/Entities/Settings/SavedFilter.cs new file mode 100644 index 0000000..5df33ae --- /dev/null +++ b/src/Werkr.Data/Entities/Settings/SavedFilter.cs @@ -0,0 +1,40 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Werkr.Data.Entities.Interfaces; + +namespace Werkr.Data.Entities.Settings; + +/// +/// Named filter combination persisted on the server for cross-device / shared access. +/// +[Table( "saved_filters" )] +public class SavedFilter : ConcurrencyBase, IKey { + + /// Database-generated primary key. + [Key] + [DatabaseGenerated( DatabaseGeneratedOption.Identity )] + public long Id { get; set; } + + /// Identity user ID of the filter owner. + [Required] + [MaxLength( 450 )] + public string OwnerId { get; set; } = ""; + + /// Page key identifying which list page this filter applies to. + [Required] + [MaxLength( 50 )] + public string PageKey { get; set; } = ""; + + /// User-chosen display name for the filter. + [Required] + [MaxLength( 200 )] + public string Name { get; set; } = ""; + + /// Serialized FilterCriteria JSON. Maximum 4 KB. + [Required] + [MaxLength( 4096 )] + public string CriteriaJson { get; set; } = "{}"; + + /// When , the filter is visible to all users. + public bool IsShared { get; set; } +} diff --git a/src/Werkr.Data/Entities/Tasks/WerkrJob.cs b/src/Werkr.Data/Entities/Tasks/WerkrJob.cs index b74a34c..203d4fc 100644 --- a/src/Werkr.Data/Entities/Tasks/WerkrJob.cs +++ b/src/Werkr.Data/Entities/Tasks/WerkrJob.cs @@ -3,6 +3,7 @@ using Werkr.Data.Entities.Interfaces; using Werkr.Data.Entities.Registration; using Werkr.Data.Entities.Schedule; +using Werkr.Data.Entities.Workflows; namespace Werkr.Data.Entities.Tasks; @@ -68,6 +69,9 @@ public class WerkrJob : ConcurrencyBase, IKey { /// public Guid? ScheduleId { get; set; } + /// Foreign key to the workflow step that produced this job. Null for standalone task jobs. + public long? StepId { get; set; } + /// Navigation property to the source task. [ForeignKey( nameof( TaskId ) )] public WerkrTask? Task { get; set; } @@ -83,4 +87,8 @@ public class WerkrJob : ConcurrencyBase, IKey { /// Navigation property to the schedule that triggered this job. [ForeignKey( nameof( ScheduleId ) )] public DbSchedule? Schedule { get; set; } + + /// Navigation property to the workflow step. + [ForeignKey( nameof( StepId ) )] + public WorkflowStep? Step { get; set; } } diff --git a/src/Werkr.Data/Entities/Workflows/Workflow.cs b/src/Werkr.Data/Entities/Workflows/Workflow.cs index daa9f16..965d72f 100644 --- a/src/Werkr.Data/Entities/Workflows/Workflow.cs +++ b/src/Werkr.Data/Entities/Workflows/Workflow.cs @@ -31,6 +31,9 @@ public class Workflow : ConcurrencyBase, IKey { /// Agent targeting tags for workflow-level override. public string[]? TargetTags { get; set; } + /// JSON-serialized annotation cards for the DAG canvas (sticky notes). + public string? Annotations { get; set; } + /// Navigation property for workflow steps. public ICollection Steps { get; set; } = []; diff --git a/src/Werkr.Data/Entities/Workflows/WorkflowRun.cs b/src/Werkr.Data/Entities/Workflows/WorkflowRun.cs index 98d5d91..f23364d 100644 --- a/src/Werkr.Data/Entities/Workflows/WorkflowRun.cs +++ b/src/Werkr.Data/Entities/Workflows/WorkflowRun.cs @@ -37,4 +37,7 @@ public class WorkflowRun : ConcurrencyBase, IKey { /// Navigation to runtime variable values for this run. public ICollection RunVariables { get; set; } = []; + + /// Navigation to step execution records for this run. + public ICollection StepExecutions { get; set; } = []; } diff --git a/src/Werkr.Data/Entities/Workflows/WorkflowStepExecution.cs b/src/Werkr.Data/Entities/Workflows/WorkflowStepExecution.cs new file mode 100644 index 0000000..d62c3fa --- /dev/null +++ b/src/Werkr.Data/Entities/Workflows/WorkflowStepExecution.cs @@ -0,0 +1,61 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Werkr.Common.Models; +using Werkr.Data.Entities.Interfaces; +using Werkr.Data.Entities.Tasks; + +namespace Werkr.Data.Entities.Workflows; + +/// +/// Tracks the execution status of a single workflow step within a run. +/// Supports multiple attempts for retry scenarios. +/// +[Table( "workflow_step_executions" )] +public class WorkflowStepExecution : ConcurrencyBase, IKey { + + /// Database-generated primary key. + [Key] + [DatabaseGenerated( DatabaseGeneratedOption.Identity )] + public long Id { get; set; } + + /// Foreign key to the workflow run. + public Guid WorkflowRunId { get; set; } + + /// Foreign key to the workflow step. + public long StepId { get; set; } + + /// Attempt number (1-based). Incremented on retry. + public int Attempt { get; set; } = 1; + + /// Current execution status of this step attempt. + public StepExecutionStatus Status { get; set; } = StepExecutionStatus.Pending; + + /// When the step started executing (UTC). Null if still pending. + public DateTime? StartTime { get; set; } + + /// When the step finished executing (UTC). Null if still running or pending. + public DateTime? EndTime { get; set; } + + /// Foreign key to the job created for this step execution. Null until a job is dispatched. + public Guid? JobId { get; set; } + + /// Error message if the step failed. + [MaxLength( 4000 )] + public string? ErrorMessage { get; set; } + + /// Reason the step was skipped (e.g., control flow evaluation). + [MaxLength( 2000 )] + public string? SkipReason { get; set; } + + /// Navigation property to the workflow run. + [ForeignKey( nameof( WorkflowRunId ) )] + public WorkflowRun? WorkflowRun { get; set; } + + /// Navigation property to the workflow step. + [ForeignKey( nameof( StepId ) )] + public WorkflowStep? Step { get; set; } + + /// Navigation property to the associated job. + [ForeignKey( nameof( JobId ) )] + public WerkrJob? Job { get; set; } +} diff --git a/src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.cs b/src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.cs deleted file mode 100644 index e2e9e2b..0000000 --- a/src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.cs +++ /dev/null @@ -1,977 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Werkr.Data.Migrations.Postgres -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "werkr"); - - migrationBuilder.CreateTable( - name: "holiday_calendars", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - description = table.Column(type: "character varying(1024)", maxLength: 1024, nullable: false), - is_system_calendar = table.Column(type: "boolean", nullable: false), - created_utc = table.Column(type: "text", nullable: false), - updated_utc = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_holiday_calendars", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "registered_connections", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - connection_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - remote_url = table.Column(type: "character varying(2048)", maxLength: 2048, nullable: false), - local_public_key = table.Column(type: "text", nullable: false), - local_private_key = table.Column(type: "text", nullable: false), - remote_public_key = table.Column(type: "text", nullable: false), - outbound_api_key = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), - inbound_api_key_hash = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), - shared_key = table.Column(type: "text", nullable: false), - previous_shared_key = table.Column(type: "text", nullable: true), - active_key_id = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - previous_key_id = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - is_server = table.Column(type: "boolean", nullable: false), - status = table.Column(type: "text", nullable: false), - last_seen = table.Column(type: "text", nullable: true), - tags = table.Column(type: "text", nullable: false), - allowed_paths = table.Column(type: "text", nullable: false), - enforce_allowlist = table.Column(type: "boolean", nullable: false), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_registered_connections", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "registration_bundles", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - connection_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - server_public_key = table.Column(type: "text", nullable: false), - server_private_key = table.Column(type: "text", nullable: false), - bundle_id = table.Column(type: "text", nullable: false), - status = table.Column(type: "text", nullable: false), - expires_at = table.Column(type: "text", nullable: false), - key_size = table.Column(type: "integer", nullable: false), - tags = table.Column(type: "text[]", nullable: false), - allowed_paths = table.Column(type: "text", nullable: false), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_registration_bundles", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "schedules", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - stop_task_after_minutes = table.Column(type: "bigint", nullable: false), - catch_up_enabled = table.Column(type: "boolean", nullable: false), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedules", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "workflows", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), - enabled = table.Column(type: "boolean", nullable: false), - target_tags = table.Column(type: "text", nullable: true), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflows", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "holiday_rules", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn), - holiday_calendar_id = table.Column(type: "uuid", nullable: false), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - rule_type = table.Column(type: "text", nullable: false), - month = table.Column(type: "integer", nullable: true), - day = table.Column(type: "integer", nullable: true), - day_of_week = table.Column(type: "integer", nullable: true), - week_number = table.Column(type: "integer", nullable: true), - window_start = table.Column(type: "time without time zone", nullable: true), - window_end = table.Column(type: "time without time zone", nullable: true), - window_time_zone_id = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - observance_rule = table.Column(type: "text", nullable: false), - year_start = table.Column(type: "integer", nullable: true), - year_end = table.Column(type: "integer", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_holiday_rules", x => x.id); - table.ForeignKey( - name: "fk_holiday_rules_holiday_calendars_holiday_calendar_id", - column: x => x.holiday_calendar_id, - principalSchema: "werkr", - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "daily_recurrence", - schema: "werkr", - columns: table => new - { - schedule_id = table.Column(type: "uuid", nullable: false), - day_interval = table.Column(type: "integer", nullable: false), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_daily_recurrence", x => x.schedule_id); - table.ForeignKey( - name: "fk_daily_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "monthly_recurrence", - schema: "werkr", - columns: table => new - { - schedule_id = table.Column(type: "uuid", nullable: false), - day_numbers = table.Column(type: "text", nullable: true), - months_of_year = table.Column(type: "integer", nullable: false), - week_number = table.Column(type: "integer", nullable: true), - days_of_week = table.Column(type: "integer", nullable: true), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_monthly_recurrence", x => x.schedule_id); - table.ForeignKey( - name: "fk_monthly_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "schedule_audit_log", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn), - schedule_id = table.Column(type: "uuid", nullable: false), - occurrence_utc_time = table.Column(type: "text", nullable: false), - calendar_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - holiday_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - mode = table.Column(type: "text", nullable: false), - created_utc = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedule_audit_log", x => x.id); - table.ForeignKey( - name: "fk_schedule_audit_log_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "schedule_expiration", - schema: "werkr", - columns: table => new - { - schedule_id = table.Column(type: "uuid", nullable: false), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false), - date = table.Column(type: "date", nullable: false), - time = table.Column(type: "time without time zone", nullable: false), - time_zone = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedule_expiration", x => x.schedule_id); - table.ForeignKey( - name: "fk_schedule_expiration_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "schedule_holiday_calendars", - schema: "werkr", - columns: table => new - { - schedule_id = table.Column(type: "uuid", nullable: false), - holiday_calendar_id = table.Column(type: "uuid", nullable: false), - mode = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedule_holiday_calendars", x => new { x.schedule_id, x.holiday_calendar_id }); - table.ForeignKey( - name: "fk_schedule_holiday_calendars_holiday_calendars_holiday_calend", - column: x => x.holiday_calendar_id, - principalSchema: "werkr", - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_schedule_holiday_calendars_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "schedule_repeat_options", - schema: "werkr", - columns: table => new - { - schedule_id = table.Column(type: "uuid", nullable: false), - repeat_interval_minutes = table.Column(type: "integer", nullable: false), - repeat_duration_minutes = table.Column(type: "integer", nullable: false), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedule_repeat_options", x => x.schedule_id); - table.ForeignKey( - name: "fk_schedule_repeat_options_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "schedule_start_datetimeinfo", - schema: "werkr", - columns: table => new - { - schedule_id = table.Column(type: "uuid", nullable: false), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false), - date = table.Column(type: "date", nullable: false), - time = table.Column(type: "time without time zone", nullable: false), - time_zone = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedule_start_datetimeinfo", x => x.schedule_id); - table.ForeignKey( - name: "fk_schedule_start_datetimeinfo_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "weekly_recurrence", - schema: "werkr", - columns: table => new - { - schedule_id = table.Column(type: "uuid", nullable: false), - week_interval = table.Column(type: "integer", nullable: false), - days_of_week = table.Column(type: "integer", nullable: false), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_weekly_recurrence", x => x.schedule_id); - table.ForeignKey( - name: "fk_weekly_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "tasks", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), - action_type = table.Column(type: "text", nullable: false), - workflow_id = table.Column(type: "bigint", nullable: true), - content = table.Column(type: "character varying(8000)", maxLength: 8000, nullable: false), - arguments = table.Column(type: "text", nullable: true), - target_tags = table.Column(type: "text", nullable: false), - enabled = table.Column(type: "boolean", nullable: false), - is_ephemeral = table.Column(type: "boolean", nullable: false), - timeout_minutes = table.Column(type: "bigint", nullable: true), - sync_interval_minutes = table.Column(type: "integer", nullable: false), - success_criteria = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - action_sub_type = table.Column(type: "character varying(30)", maxLength: 30, nullable: true), - action_parameters = table.Column(type: "text", nullable: true), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_tasks", x => x.id); - table.ForeignKey( - name: "fk_tasks_workflows_workflow_id", - column: x => x.workflow_id, - principalSchema: "werkr", - principalTable: "workflows", - principalColumn: "id"); - }); - - migrationBuilder.CreateTable( - name: "workflow_runs", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - workflow_id = table.Column(type: "bigint", nullable: false), - start_time = table.Column(type: "text", nullable: false), - end_time = table.Column(type: "text", nullable: true), - status = table.Column(type: "text", nullable: false), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_runs", x => x.id); - table.ForeignKey( - name: "fk_workflow_runs_workflows_workflow_id", - column: x => x.workflow_id, - principalSchema: "werkr", - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "workflow_schedules", - schema: "werkr", - columns: table => new - { - workflow_id = table.Column(type: "bigint", nullable: false), - schedule_id = table.Column(type: "uuid", nullable: false), - created_at_utc = table.Column(type: "text", nullable: false), - is_one_time = table.Column(type: "boolean", nullable: false), - workflow_run_id = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_schedules", x => new { x.workflow_id, x.schedule_id }); - table.ForeignKey( - name: "fk_workflow_schedules_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_workflow_schedules_workflows_workflow_id", - column: x => x.workflow_id, - principalSchema: "werkr", - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "workflow_variables", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - workflow_id = table.Column(type: "bigint", nullable: false), - name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - description = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - default_value = table.Column(type: "text", nullable: true), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_variables", x => x.id); - table.ForeignKey( - name: "fk_workflow_variables_workflows_workflow_id", - column: x => x.workflow_id, - principalSchema: "werkr", - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "holiday_dates", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn), - holiday_calendar_id = table.Column(type: "uuid", nullable: false), - holiday_rule_id = table.Column(type: "bigint", nullable: true), - date = table.Column(type: "date", nullable: false), - name = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), - year = table.Column(type: "integer", nullable: false), - window_start = table.Column(type: "time without time zone", nullable: true), - window_end = table.Column(type: "time without time zone", nullable: true), - window_time_zone_id = table.Column(type: "character varying(128)", maxLength: 128, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_holiday_dates", x => x.id); - table.ForeignKey( - name: "fk_holiday_dates_holiday_calendars_holiday_calendar_id", - column: x => x.holiday_calendar_id, - principalSchema: "werkr", - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_holiday_dates_holiday_rules_holiday_rule_id", - column: x => x.holiday_rule_id, - principalSchema: "werkr", - principalTable: "holiday_rules", - principalColumn: "id", - onDelete: ReferentialAction.SetNull); - }); - - migrationBuilder.CreateTable( - name: "task_schedules", - schema: "werkr", - columns: table => new - { - task_id = table.Column(type: "bigint", nullable: false), - schedule_id = table.Column(type: "uuid", nullable: false), - created_at_utc = table.Column(type: "text", nullable: false), - is_one_time = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_task_schedules", x => new { x.task_id, x.schedule_id }); - table.ForeignKey( - name: "fk_task_schedules_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_task_schedules_tasks_task_id", - column: x => x.task_id, - principalSchema: "werkr", - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "workflow_steps", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - workflow_id = table.Column(type: "bigint", nullable: false), - task_id = table.Column(type: "bigint", nullable: false), - order = table.Column(type: "integer", nullable: false), - control_statement = table.Column(type: "text", nullable: false), - condition_expression = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), - max_iterations = table.Column(type: "integer", nullable: false), - agent_connection_id_override = table.Column(type: "uuid", nullable: true), - dependency_mode = table.Column(type: "text", nullable: false), - input_variable_name = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - output_variable_name = table.Column(type: "character varying(128)", maxLength: 128, nullable: true), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_steps", x => x.id); - table.ForeignKey( - name: "fk_workflow_steps_registered_connections_agent_connection_id_o", - column: x => x.agent_connection_id_override, - principalSchema: "werkr", - principalTable: "registered_connections", - principalColumn: "id"); - table.ForeignKey( - name: "fk_workflow_steps_tasks_task_id", - column: x => x.task_id, - principalSchema: "werkr", - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_workflow_steps_workflows_workflow_id", - column: x => x.workflow_id, - principalSchema: "werkr", - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "jobs", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - task_id = table.Column(type: "bigint", nullable: false), - task_snapshot = table.Column(type: "character varying(8000)", maxLength: 8000, nullable: false), - runtime_seconds = table.Column(type: "double precision", nullable: false), - start_time = table.Column(type: "text", nullable: false), - end_time = table.Column(type: "text", nullable: true), - success = table.Column(type: "boolean", nullable: false), - agent_connection_id = table.Column(type: "uuid", nullable: true), - exit_code = table.Column(type: "integer", nullable: true), - error_category = table.Column(type: "text", nullable: false), - output = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true), - output_path = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - workflow_run_id = table.Column(type: "uuid", nullable: true), - schedule_id = table.Column(type: "uuid", nullable: true), - created = table.Column(type: "text", nullable: false), - last_updated = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_jobs", x => x.id); - table.ForeignKey( - name: "fk_jobs_registered_connections_agent_connection_id", - column: x => x.agent_connection_id, - principalSchema: "werkr", - principalTable: "registered_connections", - principalColumn: "id"); - table.ForeignKey( - name: "fk_jobs_schedules_schedule_id", - column: x => x.schedule_id, - principalSchema: "werkr", - principalTable: "schedules", - principalColumn: "id"); - table.ForeignKey( - name: "fk_jobs_tasks_task_id", - column: x => x.task_id, - principalSchema: "werkr", - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_jobs_workflow_runs_workflow_run_id", - column: x => x.workflow_run_id, - principalSchema: "werkr", - principalTable: "workflow_runs", - principalColumn: "id"); - }); - - migrationBuilder.CreateTable( - name: "workflow_step_dependencies", - schema: "werkr", - columns: table => new - { - step_id = table.Column(type: "bigint", nullable: false), - depends_on_step_id = table.Column(type: "bigint", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_step_dependencies", x => new { x.step_id, x.depends_on_step_id }); - table.ForeignKey( - name: "fk_workflow_step_dependencies_workflow_steps_depends_on_step_id", - column: x => x.depends_on_step_id, - principalSchema: "werkr", - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "fk_workflow_step_dependencies_workflow_steps_step_id", - column: x => x.step_id, - principalSchema: "werkr", - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "workflow_run_variables", - schema: "werkr", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - workflow_run_id = table.Column(type: "uuid", nullable: false), - variable_name = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - value = table.Column(type: "text", nullable: false), - version = table.Column(type: "integer", nullable: false), - produced_by_step_id = table.Column(type: "bigint", nullable: true), - produced_by_job_id = table.Column(type: "uuid", nullable: true), - source = table.Column(type: "text", nullable: false), - created = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_run_variables", x => x.id); - table.ForeignKey( - name: "fk_workflow_run_variables_jobs_produced_by_job_id", - column: x => x.produced_by_job_id, - principalSchema: "werkr", - principalTable: "jobs", - principalColumn: "id", - onDelete: ReferentialAction.SetNull); - table.ForeignKey( - name: "fk_workflow_run_variables_workflow_runs_workflow_run_id", - column: x => x.workflow_run_id, - principalSchema: "werkr", - principalTable: "workflow_runs", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_workflow_run_variables_workflow_steps_produced_by_step_id", - column: x => x.produced_by_step_id, - principalSchema: "werkr", - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.SetNull); - }); - - migrationBuilder.CreateIndex( - name: "ix_holiday_calendars_name", - schema: "werkr", - table: "holiday_calendars", - column: "name", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_holiday_dates_holiday_calendar_id_date", - schema: "werkr", - table: "holiday_dates", - columns: new[] { "holiday_calendar_id", "date" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_holiday_dates_holiday_rule_id", - schema: "werkr", - table: "holiday_dates", - column: "holiday_rule_id"); - - migrationBuilder.CreateIndex( - name: "ix_holiday_rules_holiday_calendar_id", - schema: "werkr", - table: "holiday_rules", - column: "holiday_calendar_id"); - - migrationBuilder.CreateIndex( - name: "ix_jobs_agent_connection_id", - schema: "werkr", - table: "jobs", - column: "agent_connection_id"); - - migrationBuilder.CreateIndex( - name: "ix_jobs_schedule_id", - schema: "werkr", - table: "jobs", - column: "schedule_id"); - - migrationBuilder.CreateIndex( - name: "ix_jobs_task_id", - schema: "werkr", - table: "jobs", - column: "task_id"); - - migrationBuilder.CreateIndex( - name: "ix_jobs_workflow_run_id", - schema: "werkr", - table: "jobs", - column: "workflow_run_id"); - - migrationBuilder.CreateIndex( - name: "ix_registered_connections_connection_name", - schema: "werkr", - table: "registered_connections", - column: "connection_name"); - - migrationBuilder.CreateIndex( - name: "ix_registered_connections_remote_url", - schema: "werkr", - table: "registered_connections", - column: "remote_url"); - - migrationBuilder.CreateIndex( - name: "ix_registration_bundles_bundle_id", - schema: "werkr", - table: "registration_bundles", - column: "bundle_id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_schedule_audit_log_schedule_id_occurrence_utc_time", - schema: "werkr", - table: "schedule_audit_log", - columns: new[] { "schedule_id", "occurrence_utc_time" }); - - migrationBuilder.CreateIndex( - name: "ix_schedule_holiday_calendars_holiday_calendar_id", - schema: "werkr", - table: "schedule_holiday_calendars", - column: "holiday_calendar_id"); - - migrationBuilder.CreateIndex( - name: "ix_schedule_holiday_calendars_schedule_id", - schema: "werkr", - table: "schedule_holiday_calendars", - column: "schedule_id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_task_schedules_schedule_id", - schema: "werkr", - table: "task_schedules", - column: "schedule_id"); - - migrationBuilder.CreateIndex( - name: "ix_tasks_workflow_id", - schema: "werkr", - table: "tasks", - column: "workflow_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_produced_by_job_id", - schema: "werkr", - table: "workflow_run_variables", - column: "produced_by_job_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_produced_by_step_id", - schema: "werkr", - table: "workflow_run_variables", - column: "produced_by_step_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_workflow_run_id_variable_name_version", - schema: "werkr", - table: "workflow_run_variables", - columns: new[] { "workflow_run_id", "variable_name", "version" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_workflow_runs_workflow_id", - schema: "werkr", - table: "workflow_runs", - column: "workflow_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_schedules_schedule_id", - schema: "werkr", - table: "workflow_schedules", - column: "schedule_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_step_dependencies_depends_on_step_id", - schema: "werkr", - table: "workflow_step_dependencies", - column: "depends_on_step_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_steps_agent_connection_id_override", - schema: "werkr", - table: "workflow_steps", - column: "agent_connection_id_override"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_steps_task_id", - schema: "werkr", - table: "workflow_steps", - column: "task_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_steps_workflow_id", - schema: "werkr", - table: "workflow_steps", - column: "workflow_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_variables_workflow_id_name", - schema: "werkr", - table: "workflow_variables", - columns: new[] { "workflow_id", "name" }, - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "daily_recurrence", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "holiday_dates", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "monthly_recurrence", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "registration_bundles", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "schedule_audit_log", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "schedule_expiration", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "schedule_holiday_calendars", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "schedule_repeat_options", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "schedule_start_datetimeinfo", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "task_schedules", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "weekly_recurrence", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "workflow_run_variables", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "workflow_schedules", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "workflow_step_dependencies", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "workflow_variables", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "holiday_rules", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "jobs", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "workflow_steps", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "holiday_calendars", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "schedules", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "workflow_runs", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "registered_connections", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "tasks", - schema: "werkr"); - - migrationBuilder.DropTable( - name: "workflows", - schema: "werkr"); - } - } -} diff --git a/src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.Designer.cs b/src/Werkr.Data/Migrations/Postgres/20260314004316_InitialCreate.Designer.cs similarity index 89% rename from src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.Designer.cs rename to src/Werkr.Data/Migrations/Postgres/20260314004316_InitialCreate.Designer.cs index ff8e77f..70423f9 100644 --- a/src/Werkr.Data/Migrations/Postgres/20260312050739_InitialCreate.Designer.cs +++ b/src/Werkr.Data/Migrations/Postgres/20260314004316_InitialCreate.Designer.cs @@ -12,7 +12,7 @@ namespace Werkr.Data.Migrations.Postgres { [DbContext(typeof(PostgresWerkrDbContext))] - [Migration("20260312050739_InitialCreate")] + [Migration("20260314004316_InitialCreate")] partial class InitialCreate { /// @@ -740,6 +740,70 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("weekly_recurrence", "werkr"); }); + modelBuilder.Entity("Werkr.Data.Entities.Settings.SavedFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created"); + + b.Property("CriteriaJson") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("criteria_json"); + + b.Property("IsShared") + .HasColumnType("boolean") + .HasColumnName("is_shared"); + + b.Property("LastUpdated") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_updated"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("owner_id"); + + b.Property("PageKey") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("page_key"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("integer") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_saved_filters"); + + b.HasIndex("PageKey", "IsShared") + .HasDatabaseName("ix_saved_filters_page_key_is_shared"); + + b.HasIndex("PageKey", "OwnerId") + .HasDatabaseName("ix_saved_filters_page_key_owner_id"); + + b.ToTable("saved_filters", "werkr"); + }); + modelBuilder.Entity("Werkr.Data.Entities.Tasks.TaskSchedule", b => { b.Property("TaskId") @@ -825,6 +889,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("start_time"); + b.Property("StepId") + .HasColumnType("bigint") + .HasColumnName("step_id"); + b.Property("Success") .HasColumnType("boolean") .HasColumnName("success"); @@ -857,11 +925,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("ScheduleId") .HasDatabaseName("ix_jobs_schedule_id"); + b.HasIndex("StepId") + .HasDatabaseName("ix_jobs_step_id"); + b.HasIndex("TaskId") .HasDatabaseName("ix_jobs_task_id"); - b.HasIndex("WorkflowRunId") - .HasDatabaseName("ix_jobs_workflow_run_id"); + b.HasIndex("WorkflowRunId", "StepId") + .HasDatabaseName("IX_jobs_WorkflowRunId_StepId"); b.ToTable("jobs", "werkr"); }); @@ -974,6 +1045,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Annotations") + .HasColumnType("text") + .HasColumnName("annotations"); + b.Property("Created") .IsRequired() .HasColumnType("text") @@ -1261,6 +1336,88 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("workflow_step_dependencies", "werkr"); }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStepExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempt") + .HasColumnType("integer") + .HasColumnName("attempt"); + + b.Property("Created") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created"); + + b.Property("EndTime") + .HasColumnType("text") + .HasColumnName("end_time"); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error_message"); + + b.Property("JobId") + .HasColumnType("uuid") + .HasColumnName("job_id"); + + b.Property("LastUpdated") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_updated"); + + b.Property("SkipReason") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("skip_reason"); + + b.Property("StartTime") + .HasColumnType("text") + .HasColumnName("start_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("StepId") + .HasColumnType("bigint") + .HasColumnName("step_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("integer") + .HasColumnName("version"); + + b.Property("WorkflowRunId") + .HasColumnType("uuid") + .HasColumnName("workflow_run_id"); + + b.HasKey("Id") + .HasName("pk_workflow_step_executions"); + + b.HasIndex("JobId") + .HasDatabaseName("ix_workflow_step_executions_job_id"); + + b.HasIndex("StepId") + .HasDatabaseName("ix_workflow_step_executions_step_id"); + + b.HasIndex("WorkflowRunId") + .HasDatabaseName("ix_workflow_step_executions_workflow_run_id"); + + b.HasIndex("WorkflowRunId", "StepId", "Attempt") + .IsUnique() + .HasDatabaseName("ix_workflow_step_executions_workflow_run_id_step_id_attempt"); + + b.ToTable("workflow_step_executions", "werkr"); + }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowVariable", b => { b.Property("Id") @@ -1484,6 +1641,12 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasForeignKey("ScheduleId") .HasConstraintName("fk_jobs_schedules_schedule_id"); + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowStep", "Step") + .WithMany() + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_jobs_workflow_steps_step_id"); + b.HasOne("Werkr.Data.Entities.Tasks.WerkrTask", "Task") .WithMany() .HasForeignKey("TaskId") @@ -1500,6 +1663,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Schedule"); + b.Navigation("Step"); + b.Navigation("Task"); b.Navigation("WorkflowRun"); @@ -1625,6 +1790,35 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Step"); }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStepExecution", b => + { + b.HasOne("Werkr.Data.Entities.Tasks.WerkrJob", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_workflow_step_executions_jobs_job_id"); + + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowStep", "Step") + .WithMany() + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_workflow_step_executions_workflow_steps_step_id"); + + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowRun", "WorkflowRun") + .WithMany("StepExecutions") + .HasForeignKey("WorkflowRunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_workflow_step_executions_workflow_runs_workflow_run_id"); + + b.Navigation("Job"); + + b.Navigation("Step"); + + b.Navigation("WorkflowRun"); + }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowVariable", b => { b.HasOne("Werkr.Data.Entities.Workflows.Workflow", "Workflow") @@ -1695,6 +1889,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Jobs"); b.Navigation("RunVariables"); + + b.Navigation("StepExecutions"); }); modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStep", b => diff --git a/src/Werkr.Data/Migrations/Postgres/20260314004316_InitialCreate.cs b/src/Werkr.Data/Migrations/Postgres/20260314004316_InitialCreate.cs new file mode 100644 index 0000000..8074102 --- /dev/null +++ b/src/Werkr.Data/Migrations/Postgres/20260314004316_InitialCreate.cs @@ -0,0 +1,1047 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Werkr.Data.Migrations.Postgres; + +/// +public partial class InitialCreate : Migration { + /// + protected override void Up( MigrationBuilder migrationBuilder ) { + _ = migrationBuilder.EnsureSchema( + name: "werkr" ); + + _ = migrationBuilder.CreateTable( + name: "holiday_calendars", + schema: "werkr", + columns: table => new { + id = table.Column( type: "uuid", nullable: false ), + name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), + description = table.Column( type: "character varying(1024)", maxLength: 1024, nullable: false ), + is_system_calendar = table.Column( type: "boolean", nullable: false ), + created_utc = table.Column( type: "text", nullable: false ), + updated_utc = table.Column( type: "text", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_holiday_calendars", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "registered_connections", + schema: "werkr", + columns: table => new { + id = table.Column( type: "uuid", nullable: false ), + connection_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), + remote_url = table.Column( type: "character varying(2048)", maxLength: 2048, nullable: false ), + local_public_key = table.Column( type: "text", nullable: false ), + local_private_key = table.Column( type: "text", nullable: false ), + remote_public_key = table.Column( type: "text", nullable: false ), + outbound_api_key = table.Column( type: "character varying(512)", maxLength: 512, nullable: false ), + inbound_api_key_hash = table.Column( type: "character varying(512)", maxLength: 512, nullable: false ), + shared_key = table.Column( type: "text", nullable: false ), + previous_shared_key = table.Column( type: "text", nullable: true ), + active_key_id = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ), + previous_key_id = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ), + is_server = table.Column( type: "boolean", nullable: false ), + status = table.Column( type: "text", nullable: false ), + last_seen = table.Column( type: "text", nullable: true ), + tags = table.Column( type: "text", nullable: false ), + allowed_paths = table.Column( type: "text", nullable: false ), + enforce_allowlist = table.Column( type: "boolean", nullable: false ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_registered_connections", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "registration_bundles", + schema: "werkr", + columns: table => new { + id = table.Column( type: "uuid", nullable: false ), + connection_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), + server_public_key = table.Column( type: "text", nullable: false ), + server_private_key = table.Column( type: "text", nullable: false ), + bundle_id = table.Column( type: "text", nullable: false ), + status = table.Column( type: "text", nullable: false ), + expires_at = table.Column( type: "text", nullable: false ), + key_size = table.Column( type: "integer", nullable: false ), + tags = table.Column( type: "text[]", nullable: false ), + allowed_paths = table.Column( type: "text", nullable: false ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_registration_bundles", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "saved_filters", + schema: "werkr", + columns: table => new { + id = table.Column( type: "bigint", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), + owner_id = table.Column( type: "character varying(450)", maxLength: 450, nullable: false ), + page_key = table.Column( type: "character varying(50)", maxLength: 50, nullable: false ), + name = table.Column( type: "character varying(200)", maxLength: 200, nullable: false ), + criteria_json = table.Column( type: "character varying(4096)", maxLength: 4096, nullable: false ), + is_shared = table.Column( type: "boolean", nullable: false ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_saved_filters", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedules", + schema: "werkr", + columns: table => new { + id = table.Column( type: "uuid", nullable: false ), + name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), + stop_task_after_minutes = table.Column( type: "bigint", nullable: false ), + catch_up_enabled = table.Column( type: "boolean", nullable: false ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedules", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflows", + schema: "werkr", + columns: table => new { + id = table.Column( type: "bigint", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), + name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), + description = table.Column( type: "character varying(2000)", maxLength: 2000, nullable: false ), + enabled = table.Column( type: "boolean", nullable: false ), + target_tags = table.Column( type: "text", nullable: true ), + annotations = table.Column( type: "text", nullable: true ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflows", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "holiday_rules", + schema: "werkr", + columns: table => new { + id = table.Column( type: "bigint", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn ), + holiday_calendar_id = table.Column( type: "uuid", nullable: false ), + name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), + rule_type = table.Column( type: "text", nullable: false ), + month = table.Column( type: "integer", nullable: true ), + day = table.Column( type: "integer", nullable: true ), + day_of_week = table.Column( type: "integer", nullable: true ), + week_number = table.Column( type: "integer", nullable: true ), + window_start = table.Column( type: "time without time zone", nullable: true ), + window_end = table.Column( type: "time without time zone", nullable: true ), + window_time_zone_id = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ), + observance_rule = table.Column( type: "text", nullable: false ), + year_start = table.Column( type: "integer", nullable: true ), + year_end = table.Column( type: "integer", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_holiday_rules", x => x.id ); + _ = table.ForeignKey( + name: "fk_holiday_rules_holiday_calendars_holiday_calendar_id", + column: x => x.holiday_calendar_id, + principalSchema: "werkr", + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "daily_recurrence", + schema: "werkr", + columns: table => new { + schedule_id = table.Column( type: "uuid", nullable: false ), + day_interval = table.Column( type: "integer", nullable: false ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_daily_recurrence", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_daily_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "monthly_recurrence", + schema: "werkr", + columns: table => new { + schedule_id = table.Column( type: "uuid", nullable: false ), + day_numbers = table.Column( type: "text", nullable: true ), + months_of_year = table.Column( type: "integer", nullable: false ), + week_number = table.Column( type: "integer", nullable: true ), + days_of_week = table.Column( type: "integer", nullable: true ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_monthly_recurrence", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_monthly_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedule_audit_log", + schema: "werkr", + columns: table => new { + id = table.Column( type: "bigint", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn ), + schedule_id = table.Column( type: "uuid", nullable: false ), + occurrence_utc_time = table.Column( type: "text", nullable: false ), + calendar_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), + holiday_name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), + mode = table.Column( type: "text", nullable: false ), + created_utc = table.Column( type: "text", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedule_audit_log", x => x.id ); + _ = table.ForeignKey( + name: "fk_schedule_audit_log_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedule_expiration", + schema: "werkr", + columns: table => new { + schedule_id = table.Column( type: "uuid", nullable: false ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ), + date = table.Column( type: "date", nullable: false ), + time = table.Column( type: "time without time zone", nullable: false ), + time_zone = table.Column( type: "text", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedule_expiration", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_schedule_expiration_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedule_holiday_calendars", + schema: "werkr", + columns: table => new { + schedule_id = table.Column( type: "uuid", nullable: false ), + holiday_calendar_id = table.Column( type: "uuid", nullable: false ), + mode = table.Column( type: "text", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedule_holiday_calendars", x => new { x.schedule_id, x.holiday_calendar_id } ); + _ = table.ForeignKey( + name: "fk_schedule_holiday_calendars_holiday_calendars_holiday_calend", + column: x => x.holiday_calendar_id, + principalSchema: "werkr", + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_schedule_holiday_calendars_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedule_repeat_options", + schema: "werkr", + columns: table => new { + schedule_id = table.Column( type: "uuid", nullable: false ), + repeat_interval_minutes = table.Column( type: "integer", nullable: false ), + repeat_duration_minutes = table.Column( type: "integer", nullable: false ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedule_repeat_options", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_schedule_repeat_options_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedule_start_datetimeinfo", + schema: "werkr", + columns: table => new { + schedule_id = table.Column( type: "uuid", nullable: false ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ), + date = table.Column( type: "date", nullable: false ), + time = table.Column( type: "time without time zone", nullable: false ), + time_zone = table.Column( type: "text", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedule_start_datetimeinfo", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_schedule_start_datetimeinfo_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "weekly_recurrence", + schema: "werkr", + columns: table => new { + schedule_id = table.Column( type: "uuid", nullable: false ), + week_interval = table.Column( type: "integer", nullable: false ), + days_of_week = table.Column( type: "integer", nullable: false ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_weekly_recurrence", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_weekly_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "tasks", + schema: "werkr", + columns: table => new { + id = table.Column( type: "bigint", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), + name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), + description = table.Column( type: "character varying(2000)", maxLength: 2000, nullable: false ), + action_type = table.Column( type: "text", nullable: false ), + workflow_id = table.Column( type: "bigint", nullable: true ), + content = table.Column( type: "character varying(8000)", maxLength: 8000, nullable: false ), + arguments = table.Column( type: "text", nullable: true ), + target_tags = table.Column( type: "text", nullable: false ), + enabled = table.Column( type: "boolean", nullable: false ), + is_ephemeral = table.Column( type: "boolean", nullable: false ), + timeout_minutes = table.Column( type: "bigint", nullable: true ), + sync_interval_minutes = table.Column( type: "integer", nullable: false ), + success_criteria = table.Column( type: "character varying(500)", maxLength: 500, nullable: true ), + action_sub_type = table.Column( type: "character varying(30)", maxLength: 30, nullable: true ), + action_parameters = table.Column( type: "text", nullable: true ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_tasks", x => x.id ); + _ = table.ForeignKey( + name: "fk_tasks_workflows_workflow_id", + column: x => x.workflow_id, + principalSchema: "werkr", + principalTable: "workflows", + principalColumn: "id" ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_runs", + schema: "werkr", + columns: table => new { + id = table.Column( type: "uuid", nullable: false ), + workflow_id = table.Column( type: "bigint", nullable: false ), + start_time = table.Column( type: "text", nullable: false ), + end_time = table.Column( type: "text", nullable: true ), + status = table.Column( type: "text", nullable: false ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_runs", x => x.id ); + _ = table.ForeignKey( + name: "fk_workflow_runs_workflows_workflow_id", + column: x => x.workflow_id, + principalSchema: "werkr", + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_schedules", + schema: "werkr", + columns: table => new { + workflow_id = table.Column( type: "bigint", nullable: false ), + schedule_id = table.Column( type: "uuid", nullable: false ), + created_at_utc = table.Column( type: "text", nullable: false ), + is_one_time = table.Column( type: "boolean", nullable: false ), + workflow_run_id = table.Column( type: "uuid", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_schedules", x => new { x.workflow_id, x.schedule_id } ); + _ = table.ForeignKey( + name: "fk_workflow_schedules_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_workflow_schedules_workflows_workflow_id", + column: x => x.workflow_id, + principalSchema: "werkr", + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_variables", + schema: "werkr", + columns: table => new { + id = table.Column( type: "bigint", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), + workflow_id = table.Column( type: "bigint", nullable: false ), + name = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), + description = table.Column( type: "character varying(500)", maxLength: 500, nullable: true ), + default_value = table.Column( type: "text", nullable: true ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_variables", x => x.id ); + _ = table.ForeignKey( + name: "fk_workflow_variables_workflows_workflow_id", + column: x => x.workflow_id, + principalSchema: "werkr", + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "holiday_dates", + schema: "werkr", + columns: table => new { + id = table.Column( type: "bigint", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityAlwaysColumn ), + holiday_calendar_id = table.Column( type: "uuid", nullable: false ), + holiday_rule_id = table.Column( type: "bigint", nullable: true ), + date = table.Column( type: "date", nullable: false ), + name = table.Column( type: "character varying(256)", maxLength: 256, nullable: false ), + year = table.Column( type: "integer", nullable: false ), + window_start = table.Column( type: "time without time zone", nullable: true ), + window_end = table.Column( type: "time without time zone", nullable: true ), + window_time_zone_id = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_holiday_dates", x => x.id ); + _ = table.ForeignKey( + name: "fk_holiday_dates_holiday_calendars_holiday_calendar_id", + column: x => x.holiday_calendar_id, + principalSchema: "werkr", + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_holiday_dates_holiday_rules_holiday_rule_id", + column: x => x.holiday_rule_id, + principalSchema: "werkr", + principalTable: "holiday_rules", + principalColumn: "id", + onDelete: ReferentialAction.SetNull ); + } ); + + _ = migrationBuilder.CreateTable( + name: "task_schedules", + schema: "werkr", + columns: table => new { + task_id = table.Column( type: "bigint", nullable: false ), + schedule_id = table.Column( type: "uuid", nullable: false ), + created_at_utc = table.Column( type: "text", nullable: false ), + is_one_time = table.Column( type: "boolean", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_task_schedules", x => new { x.task_id, x.schedule_id } ); + _ = table.ForeignKey( + name: "fk_task_schedules_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_task_schedules_tasks_task_id", + column: x => x.task_id, + principalSchema: "werkr", + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_steps", + schema: "werkr", + columns: table => new { + id = table.Column( type: "bigint", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), + workflow_id = table.Column( type: "bigint", nullable: false ), + task_id = table.Column( type: "bigint", nullable: false ), + order = table.Column( type: "integer", nullable: false ), + control_statement = table.Column( type: "text", nullable: false ), + condition_expression = table.Column( type: "character varying(2000)", maxLength: 2000, nullable: true ), + max_iterations = table.Column( type: "integer", nullable: false ), + agent_connection_id_override = table.Column( type: "uuid", nullable: true ), + dependency_mode = table.Column( type: "text", nullable: false ), + input_variable_name = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ), + output_variable_name = table.Column( type: "character varying(128)", maxLength: 128, nullable: true ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_steps", x => x.id ); + _ = table.ForeignKey( + name: "fk_workflow_steps_registered_connections_agent_connection_id_o", + column: x => x.agent_connection_id_override, + principalSchema: "werkr", + principalTable: "registered_connections", + principalColumn: "id" ); + _ = table.ForeignKey( + name: "fk_workflow_steps_tasks_task_id", + column: x => x.task_id, + principalSchema: "werkr", + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_workflow_steps_workflows_workflow_id", + column: x => x.workflow_id, + principalSchema: "werkr", + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "jobs", + schema: "werkr", + columns: table => new { + id = table.Column( type: "uuid", nullable: false ), + task_id = table.Column( type: "bigint", nullable: false ), + task_snapshot = table.Column( type: "character varying(8000)", maxLength: 8000, nullable: false ), + runtime_seconds = table.Column( type: "double precision", nullable: false ), + start_time = table.Column( type: "text", nullable: false ), + end_time = table.Column( type: "text", nullable: true ), + success = table.Column( type: "boolean", nullable: false ), + agent_connection_id = table.Column( type: "uuid", nullable: true ), + exit_code = table.Column( type: "integer", nullable: true ), + error_category = table.Column( type: "text", nullable: false ), + output = table.Column( type: "character varying(2000)", maxLength: 2000, nullable: true ), + output_path = table.Column( type: "character varying(512)", maxLength: 512, nullable: true ), + workflow_run_id = table.Column( type: "uuid", nullable: true ), + schedule_id = table.Column( type: "uuid", nullable: true ), + step_id = table.Column( type: "bigint", nullable: true ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_jobs", x => x.id ); + _ = table.ForeignKey( + name: "fk_jobs_registered_connections_agent_connection_id", + column: x => x.agent_connection_id, + principalSchema: "werkr", + principalTable: "registered_connections", + principalColumn: "id" ); + _ = table.ForeignKey( + name: "fk_jobs_schedules_schedule_id", + column: x => x.schedule_id, + principalSchema: "werkr", + principalTable: "schedules", + principalColumn: "id" ); + _ = table.ForeignKey( + name: "fk_jobs_tasks_task_id", + column: x => x.task_id, + principalSchema: "werkr", + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_jobs_workflow_runs_workflow_run_id", + column: x => x.workflow_run_id, + principalSchema: "werkr", + principalTable: "workflow_runs", + principalColumn: "id" ); + _ = table.ForeignKey( + name: "fk_jobs_workflow_steps_step_id", + column: x => x.step_id, + principalSchema: "werkr", + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.SetNull ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_step_dependencies", + schema: "werkr", + columns: table => new { + step_id = table.Column( type: "bigint", nullable: false ), + depends_on_step_id = table.Column( type: "bigint", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_step_dependencies", x => new { x.step_id, x.depends_on_step_id } ); + _ = table.ForeignKey( + name: "fk_workflow_step_dependencies_workflow_steps_depends_on_step_id", + column: x => x.depends_on_step_id, + principalSchema: "werkr", + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.Restrict ); + _ = table.ForeignKey( + name: "fk_workflow_step_dependencies_workflow_steps_step_id", + column: x => x.step_id, + principalSchema: "werkr", + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_run_variables", + schema: "werkr", + columns: table => new { + id = table.Column( type: "bigint", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), + workflow_run_id = table.Column( type: "uuid", nullable: false ), + variable_name = table.Column( type: "character varying(128)", maxLength: 128, nullable: false ), + value = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ), + produced_by_step_id = table.Column( type: "bigint", nullable: true ), + produced_by_job_id = table.Column( type: "uuid", nullable: true ), + source = table.Column( type: "text", nullable: false ), + created = table.Column( type: "text", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_run_variables", x => x.id ); + _ = table.ForeignKey( + name: "fk_workflow_run_variables_jobs_produced_by_job_id", + column: x => x.produced_by_job_id, + principalSchema: "werkr", + principalTable: "jobs", + principalColumn: "id", + onDelete: ReferentialAction.SetNull ); + _ = table.ForeignKey( + name: "fk_workflow_run_variables_workflow_runs_workflow_run_id", + column: x => x.workflow_run_id, + principalSchema: "werkr", + principalTable: "workflow_runs", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_workflow_run_variables_workflow_steps_produced_by_step_id", + column: x => x.produced_by_step_id, + principalSchema: "werkr", + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.SetNull ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_step_executions", + schema: "werkr", + columns: table => new { + id = table.Column( type: "bigint", nullable: false ) + .Annotation( "Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn ), + workflow_run_id = table.Column( type: "uuid", nullable: false ), + step_id = table.Column( type: "bigint", nullable: false ), + attempt = table.Column( type: "integer", nullable: false ), + status = table.Column( type: "text", nullable: false ), + start_time = table.Column( type: "text", nullable: true ), + end_time = table.Column( type: "text", nullable: true ), + job_id = table.Column( type: "uuid", nullable: true ), + error_message = table.Column( type: "character varying(4000)", maxLength: 4000, nullable: true ), + skip_reason = table.Column( type: "character varying(2000)", maxLength: 2000, nullable: true ), + created = table.Column( type: "text", nullable: false ), + last_updated = table.Column( type: "text", nullable: false ), + version = table.Column( type: "integer", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_step_executions", x => x.id ); + _ = table.ForeignKey( + name: "fk_workflow_step_executions_jobs_job_id", + column: x => x.job_id, + principalSchema: "werkr", + principalTable: "jobs", + principalColumn: "id", + onDelete: ReferentialAction.SetNull ); + _ = table.ForeignKey( + name: "fk_workflow_step_executions_workflow_runs_workflow_run_id", + column: x => x.workflow_run_id, + principalSchema: "werkr", + principalTable: "workflow_runs", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_workflow_step_executions_workflow_steps_step_id", + column: x => x.step_id, + principalSchema: "werkr", + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateIndex( + name: "ix_holiday_calendars_name", + schema: "werkr", + table: "holiday_calendars", + column: "name", + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_holiday_dates_holiday_calendar_id_date", + schema: "werkr", + table: "holiday_dates", + columns: ["holiday_calendar_id", "date"], + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_holiday_dates_holiday_rule_id", + schema: "werkr", + table: "holiday_dates", + column: "holiday_rule_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_holiday_rules_holiday_calendar_id", + schema: "werkr", + table: "holiday_rules", + column: "holiday_calendar_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_jobs_agent_connection_id", + schema: "werkr", + table: "jobs", + column: "agent_connection_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_jobs_schedule_id", + schema: "werkr", + table: "jobs", + column: "schedule_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_jobs_step_id", + schema: "werkr", + table: "jobs", + column: "step_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_jobs_task_id", + schema: "werkr", + table: "jobs", + column: "task_id" ); + + _ = migrationBuilder.CreateIndex( + name: "IX_jobs_WorkflowRunId_StepId", + schema: "werkr", + table: "jobs", + columns: ["workflow_run_id", "step_id"] ); + + _ = migrationBuilder.CreateIndex( + name: "ix_registered_connections_connection_name", + schema: "werkr", + table: "registered_connections", + column: "connection_name" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_registered_connections_remote_url", + schema: "werkr", + table: "registered_connections", + column: "remote_url" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_registration_bundles_bundle_id", + schema: "werkr", + table: "registration_bundles", + column: "bundle_id", + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_saved_filters_page_key_is_shared", + schema: "werkr", + table: "saved_filters", + columns: ["page_key", "is_shared"] ); + + _ = migrationBuilder.CreateIndex( + name: "ix_saved_filters_page_key_owner_id", + schema: "werkr", + table: "saved_filters", + columns: ["page_key", "owner_id"] ); + + _ = migrationBuilder.CreateIndex( + name: "ix_schedule_audit_log_schedule_id_occurrence_utc_time", + schema: "werkr", + table: "schedule_audit_log", + columns: ["schedule_id", "occurrence_utc_time"] ); + + _ = migrationBuilder.CreateIndex( + name: "ix_schedule_holiday_calendars_holiday_calendar_id", + schema: "werkr", + table: "schedule_holiday_calendars", + column: "holiday_calendar_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_schedule_holiday_calendars_schedule_id", + schema: "werkr", + table: "schedule_holiday_calendars", + column: "schedule_id", + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_task_schedules_schedule_id", + schema: "werkr", + table: "task_schedules", + column: "schedule_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_tasks_workflow_id", + schema: "werkr", + table: "tasks", + column: "workflow_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_produced_by_job_id", + schema: "werkr", + table: "workflow_run_variables", + column: "produced_by_job_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_produced_by_step_id", + schema: "werkr", + table: "workflow_run_variables", + column: "produced_by_step_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_workflow_run_id_variable_name_version", + schema: "werkr", + table: "workflow_run_variables", + columns: ["workflow_run_id", "variable_name", "version"], + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_runs_workflow_id", + schema: "werkr", + table: "workflow_runs", + column: "workflow_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_schedules_schedule_id", + schema: "werkr", + table: "workflow_schedules", + column: "schedule_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_step_dependencies_depends_on_step_id", + schema: "werkr", + table: "workflow_step_dependencies", + column: "depends_on_step_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_step_executions_job_id", + schema: "werkr", + table: "workflow_step_executions", + column: "job_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_step_executions_step_id", + schema: "werkr", + table: "workflow_step_executions", + column: "step_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_step_executions_workflow_run_id", + schema: "werkr", + table: "workflow_step_executions", + column: "workflow_run_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_step_executions_workflow_run_id_step_id_attempt", + schema: "werkr", + table: "workflow_step_executions", + columns: ["workflow_run_id", "step_id", "attempt"], + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_steps_agent_connection_id_override", + schema: "werkr", + table: "workflow_steps", + column: "agent_connection_id_override" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_steps_task_id", + schema: "werkr", + table: "workflow_steps", + column: "task_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_steps_workflow_id", + schema: "werkr", + table: "workflow_steps", + column: "workflow_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_variables_workflow_id_name", + schema: "werkr", + table: "workflow_variables", + columns: ["workflow_id", "name"], + unique: true ); + } + + /// + protected override void Down( MigrationBuilder migrationBuilder ) { + _ = migrationBuilder.DropTable( + name: "daily_recurrence", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "holiday_dates", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "monthly_recurrence", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "registration_bundles", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "saved_filters", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "schedule_audit_log", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "schedule_expiration", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "schedule_holiday_calendars", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "schedule_repeat_options", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "schedule_start_datetimeinfo", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "task_schedules", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "weekly_recurrence", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "workflow_run_variables", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "workflow_schedules", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "workflow_step_dependencies", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "workflow_step_executions", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "workflow_variables", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "holiday_rules", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "jobs", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "holiday_calendars", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "schedules", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "workflow_runs", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "workflow_steps", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "registered_connections", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "tasks", + schema: "werkr" ); + + _ = migrationBuilder.DropTable( + name: "workflows", + schema: "werkr" ); + } +} diff --git a/src/Werkr.Data/Migrations/Postgres/PostgresWerkrDbContextModelSnapshot.cs b/src/Werkr.Data/Migrations/Postgres/PostgresWerkrDbContextModelSnapshot.cs index 2604fe7..9cd0479 100644 --- a/src/Werkr.Data/Migrations/Postgres/PostgresWerkrDbContextModelSnapshot.cs +++ b/src/Werkr.Data/Migrations/Postgres/PostgresWerkrDbContextModelSnapshot.cs @@ -737,6 +737,70 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("weekly_recurrence", "werkr"); }); + modelBuilder.Entity("Werkr.Data.Entities.Settings.SavedFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Created") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created"); + + b.Property("CriteriaJson") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("criteria_json"); + + b.Property("IsShared") + .HasColumnType("boolean") + .HasColumnName("is_shared"); + + b.Property("LastUpdated") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_updated"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("character varying(450)") + .HasColumnName("owner_id"); + + b.Property("PageKey") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("page_key"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("integer") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_saved_filters"); + + b.HasIndex("PageKey", "IsShared") + .HasDatabaseName("ix_saved_filters_page_key_is_shared"); + + b.HasIndex("PageKey", "OwnerId") + .HasDatabaseName("ix_saved_filters_page_key_owner_id"); + + b.ToTable("saved_filters", "werkr"); + }); + modelBuilder.Entity("Werkr.Data.Entities.Tasks.TaskSchedule", b => { b.Property("TaskId") @@ -822,6 +886,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("start_time"); + b.Property("StepId") + .HasColumnType("bigint") + .HasColumnName("step_id"); + b.Property("Success") .HasColumnType("boolean") .HasColumnName("success"); @@ -854,11 +922,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ScheduleId") .HasDatabaseName("ix_jobs_schedule_id"); + b.HasIndex("StepId") + .HasDatabaseName("ix_jobs_step_id"); + b.HasIndex("TaskId") .HasDatabaseName("ix_jobs_task_id"); - b.HasIndex("WorkflowRunId") - .HasDatabaseName("ix_jobs_workflow_run_id"); + b.HasIndex("WorkflowRunId", "StepId") + .HasDatabaseName("IX_jobs_WorkflowRunId_StepId"); b.ToTable("jobs", "werkr"); }); @@ -971,6 +1042,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("Annotations") + .HasColumnType("text") + .HasColumnName("annotations"); + b.Property("Created") .IsRequired() .HasColumnType("text") @@ -1258,6 +1333,88 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("workflow_step_dependencies", "werkr"); }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStepExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Attempt") + .HasColumnType("integer") + .HasColumnName("attempt"); + + b.Property("Created") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created"); + + b.Property("EndTime") + .HasColumnType("text") + .HasColumnName("end_time"); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)") + .HasColumnName("error_message"); + + b.Property("JobId") + .HasColumnType("uuid") + .HasColumnName("job_id"); + + b.Property("LastUpdated") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_updated"); + + b.Property("SkipReason") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("skip_reason"); + + b.Property("StartTime") + .HasColumnType("text") + .HasColumnName("start_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("StepId") + .HasColumnType("bigint") + .HasColumnName("step_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("integer") + .HasColumnName("version"); + + b.Property("WorkflowRunId") + .HasColumnType("uuid") + .HasColumnName("workflow_run_id"); + + b.HasKey("Id") + .HasName("pk_workflow_step_executions"); + + b.HasIndex("JobId") + .HasDatabaseName("ix_workflow_step_executions_job_id"); + + b.HasIndex("StepId") + .HasDatabaseName("ix_workflow_step_executions_step_id"); + + b.HasIndex("WorkflowRunId") + .HasDatabaseName("ix_workflow_step_executions_workflow_run_id"); + + b.HasIndex("WorkflowRunId", "StepId", "Attempt") + .IsUnique() + .HasDatabaseName("ix_workflow_step_executions_workflow_run_id_step_id_attempt"); + + b.ToTable("workflow_step_executions", "werkr"); + }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowVariable", b => { b.Property("Id") @@ -1481,6 +1638,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("ScheduleId") .HasConstraintName("fk_jobs_schedules_schedule_id"); + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowStep", "Step") + .WithMany() + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_jobs_workflow_steps_step_id"); + b.HasOne("Werkr.Data.Entities.Tasks.WerkrTask", "Task") .WithMany() .HasForeignKey("TaskId") @@ -1497,6 +1660,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Schedule"); + b.Navigation("Step"); + b.Navigation("Task"); b.Navigation("WorkflowRun"); @@ -1622,6 +1787,35 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Step"); }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStepExecution", b => + { + b.HasOne("Werkr.Data.Entities.Tasks.WerkrJob", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_workflow_step_executions_jobs_job_id"); + + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowStep", "Step") + .WithMany() + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_workflow_step_executions_workflow_steps_step_id"); + + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowRun", "WorkflowRun") + .WithMany("StepExecutions") + .HasForeignKey("WorkflowRunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_workflow_step_executions_workflow_runs_workflow_run_id"); + + b.Navigation("Job"); + + b.Navigation("Step"); + + b.Navigation("WorkflowRun"); + }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowVariable", b => { b.HasOne("Werkr.Data.Entities.Workflows.Workflow", "Workflow") @@ -1692,6 +1886,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Jobs"); b.Navigation("RunVariables"); + + b.Navigation("StepExecutions"); }); modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStep", b => diff --git a/src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.cs b/src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.cs deleted file mode 100644 index 8424198..0000000 --- a/src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.cs +++ /dev/null @@ -1,868 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Werkr.Data.Migrations.Sqlite -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "holiday_calendars", - columns: table => new - { - id = table.Column(type: "TEXT", nullable: false), - name = table.Column(type: "TEXT", maxLength: 256, nullable: false), - description = table.Column(type: "TEXT", maxLength: 1024, nullable: false), - is_system_calendar = table.Column(type: "INTEGER", nullable: false), - created_utc = table.Column(type: "TEXT", nullable: false), - updated_utc = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_holiday_calendars", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "registered_connections", - columns: table => new - { - id = table.Column(type: "TEXT", nullable: false), - connection_name = table.Column(type: "TEXT", maxLength: 256, nullable: false), - remote_url = table.Column(type: "TEXT", maxLength: 2048, nullable: false), - local_public_key = table.Column(type: "TEXT", nullable: false), - local_private_key = table.Column(type: "TEXT", nullable: false), - remote_public_key = table.Column(type: "TEXT", nullable: false), - outbound_api_key = table.Column(type: "TEXT", maxLength: 512, nullable: false), - inbound_api_key_hash = table.Column(type: "TEXT", maxLength: 512, nullable: false), - shared_key = table.Column(type: "TEXT", nullable: false), - previous_shared_key = table.Column(type: "TEXT", nullable: true), - active_key_id = table.Column(type: "TEXT", maxLength: 128, nullable: true), - previous_key_id = table.Column(type: "TEXT", maxLength: 128, nullable: true), - is_server = table.Column(type: "INTEGER", nullable: false), - status = table.Column(type: "TEXT", nullable: false), - last_seen = table.Column(type: "TEXT", nullable: true), - tags = table.Column(type: "TEXT", nullable: false), - allowed_paths = table.Column(type: "TEXT", nullable: false), - enforce_allowlist = table.Column(type: "INTEGER", nullable: false), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_registered_connections", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "registration_bundles", - columns: table => new - { - id = table.Column(type: "TEXT", nullable: false), - connection_name = table.Column(type: "TEXT", maxLength: 256, nullable: false), - server_public_key = table.Column(type: "TEXT", nullable: false), - server_private_key = table.Column(type: "TEXT", nullable: false), - bundle_id = table.Column(type: "TEXT", nullable: false), - status = table.Column(type: "TEXT", nullable: false), - expires_at = table.Column(type: "TEXT", nullable: false), - key_size = table.Column(type: "INTEGER", nullable: false), - tags = table.Column(type: "TEXT", nullable: false), - allowed_paths = table.Column(type: "TEXT", nullable: false), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_registration_bundles", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "schedules", - columns: table => new - { - id = table.Column(type: "TEXT", nullable: false), - name = table.Column(type: "TEXT", maxLength: 256, nullable: false), - stop_task_after_minutes = table.Column(type: "INTEGER", nullable: false), - catch_up_enabled = table.Column(type: "INTEGER", nullable: false), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedules", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "workflows", - columns: table => new - { - id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - name = table.Column(type: "TEXT", maxLength: 256, nullable: false), - description = table.Column(type: "TEXT", maxLength: 2000, nullable: false), - enabled = table.Column(type: "INTEGER", nullable: false), - target_tags = table.Column(type: "TEXT", nullable: true), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflows", x => x.id); - }); - - migrationBuilder.CreateTable( - name: "holiday_rules", - columns: table => new - { - id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - holiday_calendar_id = table.Column(type: "TEXT", nullable: false), - name = table.Column(type: "TEXT", maxLength: 256, nullable: false), - rule_type = table.Column(type: "TEXT", nullable: false), - month = table.Column(type: "INTEGER", nullable: true), - day = table.Column(type: "INTEGER", nullable: true), - day_of_week = table.Column(type: "INTEGER", nullable: true), - week_number = table.Column(type: "INTEGER", nullable: true), - window_start = table.Column(type: "TEXT", nullable: true), - window_end = table.Column(type: "TEXT", nullable: true), - window_time_zone_id = table.Column(type: "TEXT", maxLength: 128, nullable: true), - observance_rule = table.Column(type: "TEXT", nullable: false), - year_start = table.Column(type: "INTEGER", nullable: true), - year_end = table.Column(type: "INTEGER", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_holiday_rules", x => x.id); - table.ForeignKey( - name: "fk_holiday_rules_holiday_calendars_holiday_calendar_id", - column: x => x.holiday_calendar_id, - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "daily_recurrence", - columns: table => new - { - schedule_id = table.Column(type: "TEXT", nullable: false), - day_interval = table.Column(type: "INTEGER", nullable: false), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_daily_recurrence", x => x.schedule_id); - table.ForeignKey( - name: "fk_daily_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "monthly_recurrence", - columns: table => new - { - schedule_id = table.Column(type: "TEXT", nullable: false), - day_numbers = table.Column(type: "TEXT", nullable: true), - months_of_year = table.Column(type: "INTEGER", nullable: false), - week_number = table.Column(type: "INTEGER", nullable: true), - days_of_week = table.Column(type: "INTEGER", nullable: true), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_monthly_recurrence", x => x.schedule_id); - table.ForeignKey( - name: "fk_monthly_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "schedule_audit_log", - columns: table => new - { - id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - schedule_id = table.Column(type: "TEXT", nullable: false), - occurrence_utc_time = table.Column(type: "TEXT", nullable: false), - calendar_name = table.Column(type: "TEXT", maxLength: 256, nullable: false), - holiday_name = table.Column(type: "TEXT", maxLength: 256, nullable: false), - mode = table.Column(type: "TEXT", nullable: false), - created_utc = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedule_audit_log", x => x.id); - table.ForeignKey( - name: "fk_schedule_audit_log_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "schedule_expiration", - columns: table => new - { - schedule_id = table.Column(type: "TEXT", nullable: false), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false), - date = table.Column(type: "TEXT", nullable: false), - time = table.Column(type: "TEXT", nullable: false), - time_zone = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedule_expiration", x => x.schedule_id); - table.ForeignKey( - name: "fk_schedule_expiration_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "schedule_holiday_calendars", - columns: table => new - { - schedule_id = table.Column(type: "TEXT", nullable: false), - holiday_calendar_id = table.Column(type: "TEXT", nullable: false), - mode = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedule_holiday_calendars", x => new { x.schedule_id, x.holiday_calendar_id }); - table.ForeignKey( - name: "fk_schedule_holiday_calendars_holiday_calendars_holiday_calendar_id", - column: x => x.holiday_calendar_id, - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_schedule_holiday_calendars_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "schedule_repeat_options", - columns: table => new - { - schedule_id = table.Column(type: "TEXT", nullable: false), - repeat_interval_minutes = table.Column(type: "INTEGER", nullable: false), - repeat_duration_minutes = table.Column(type: "INTEGER", nullable: false), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedule_repeat_options", x => x.schedule_id); - table.ForeignKey( - name: "fk_schedule_repeat_options_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "schedule_start_datetimeinfo", - columns: table => new - { - schedule_id = table.Column(type: "TEXT", nullable: false), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false), - date = table.Column(type: "TEXT", nullable: false), - time = table.Column(type: "TEXT", nullable: false), - time_zone = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_schedule_start_datetimeinfo", x => x.schedule_id); - table.ForeignKey( - name: "fk_schedule_start_datetimeinfo_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "weekly_recurrence", - columns: table => new - { - schedule_id = table.Column(type: "TEXT", nullable: false), - week_interval = table.Column(type: "INTEGER", nullable: false), - days_of_week = table.Column(type: "INTEGER", nullable: false), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_weekly_recurrence", x => x.schedule_id); - table.ForeignKey( - name: "fk_weekly_recurrence_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "tasks", - columns: table => new - { - id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - name = table.Column(type: "TEXT", maxLength: 256, nullable: false), - description = table.Column(type: "TEXT", maxLength: 2000, nullable: false), - action_type = table.Column(type: "TEXT", nullable: false), - workflow_id = table.Column(type: "INTEGER", nullable: true), - content = table.Column(type: "TEXT", maxLength: 8000, nullable: false), - arguments = table.Column(type: "TEXT", nullable: true), - target_tags = table.Column(type: "TEXT", nullable: false), - enabled = table.Column(type: "INTEGER", nullable: false), - is_ephemeral = table.Column(type: "INTEGER", nullable: false), - timeout_minutes = table.Column(type: "INTEGER", nullable: true), - sync_interval_minutes = table.Column(type: "INTEGER", nullable: false), - success_criteria = table.Column(type: "TEXT", maxLength: 500, nullable: true), - action_sub_type = table.Column(type: "TEXT", maxLength: 30, nullable: true), - action_parameters = table.Column(type: "TEXT", nullable: true), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_tasks", x => x.id); - table.ForeignKey( - name: "fk_tasks_workflows_workflow_id", - column: x => x.workflow_id, - principalTable: "workflows", - principalColumn: "id"); - }); - - migrationBuilder.CreateTable( - name: "workflow_runs", - columns: table => new - { - id = table.Column(type: "TEXT", nullable: false), - workflow_id = table.Column(type: "INTEGER", nullable: false), - start_time = table.Column(type: "TEXT", nullable: false), - end_time = table.Column(type: "TEXT", nullable: true), - status = table.Column(type: "TEXT", nullable: false), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_runs", x => x.id); - table.ForeignKey( - name: "fk_workflow_runs_workflows_workflow_id", - column: x => x.workflow_id, - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "workflow_schedules", - columns: table => new - { - workflow_id = table.Column(type: "INTEGER", nullable: false), - schedule_id = table.Column(type: "TEXT", nullable: false), - created_at_utc = table.Column(type: "TEXT", nullable: false), - is_one_time = table.Column(type: "INTEGER", nullable: false), - workflow_run_id = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_schedules", x => new { x.workflow_id, x.schedule_id }); - table.ForeignKey( - name: "fk_workflow_schedules_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_workflow_schedules_workflows_workflow_id", - column: x => x.workflow_id, - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "workflow_variables", - columns: table => new - { - id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - workflow_id = table.Column(type: "INTEGER", nullable: false), - name = table.Column(type: "TEXT", maxLength: 128, nullable: false), - description = table.Column(type: "TEXT", maxLength: 500, nullable: true), - default_value = table.Column(type: "TEXT", nullable: true), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_variables", x => x.id); - table.ForeignKey( - name: "fk_workflow_variables_workflows_workflow_id", - column: x => x.workflow_id, - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "holiday_dates", - columns: table => new - { - id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - holiday_calendar_id = table.Column(type: "TEXT", nullable: false), - holiday_rule_id = table.Column(type: "INTEGER", nullable: true), - date = table.Column(type: "TEXT", nullable: false), - name = table.Column(type: "TEXT", maxLength: 256, nullable: false), - year = table.Column(type: "INTEGER", nullable: false), - window_start = table.Column(type: "TEXT", nullable: true), - window_end = table.Column(type: "TEXT", nullable: true), - window_time_zone_id = table.Column(type: "TEXT", maxLength: 128, nullable: true) - }, - constraints: table => - { - table.PrimaryKey("pk_holiday_dates", x => x.id); - table.ForeignKey( - name: "fk_holiday_dates_holiday_calendars_holiday_calendar_id", - column: x => x.holiday_calendar_id, - principalTable: "holiday_calendars", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_holiday_dates_holiday_rules_holiday_rule_id", - column: x => x.holiday_rule_id, - principalTable: "holiday_rules", - principalColumn: "id", - onDelete: ReferentialAction.SetNull); - }); - - migrationBuilder.CreateTable( - name: "task_schedules", - columns: table => new - { - task_id = table.Column(type: "INTEGER", nullable: false), - schedule_id = table.Column(type: "TEXT", nullable: false), - created_at_utc = table.Column(type: "TEXT", nullable: false), - is_one_time = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_task_schedules", x => new { x.task_id, x.schedule_id }); - table.ForeignKey( - name: "fk_task_schedules_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_task_schedules_tasks_task_id", - column: x => x.task_id, - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "workflow_steps", - columns: table => new - { - id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - workflow_id = table.Column(type: "INTEGER", nullable: false), - task_id = table.Column(type: "INTEGER", nullable: false), - order = table.Column(type: "INTEGER", nullable: false), - control_statement = table.Column(type: "TEXT", nullable: false), - condition_expression = table.Column(type: "TEXT", maxLength: 2000, nullable: true), - max_iterations = table.Column(type: "INTEGER", nullable: false), - agent_connection_id_override = table.Column(type: "TEXT", nullable: true), - dependency_mode = table.Column(type: "TEXT", nullable: false), - input_variable_name = table.Column(type: "TEXT", maxLength: 128, nullable: true), - output_variable_name = table.Column(type: "TEXT", maxLength: 128, nullable: true), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_steps", x => x.id); - table.ForeignKey( - name: "fk_workflow_steps_registered_connections_agent_connection_id_override", - column: x => x.agent_connection_id_override, - principalTable: "registered_connections", - principalColumn: "id"); - table.ForeignKey( - name: "fk_workflow_steps_tasks_task_id", - column: x => x.task_id, - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_workflow_steps_workflows_workflow_id", - column: x => x.workflow_id, - principalTable: "workflows", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "jobs", - columns: table => new - { - id = table.Column(type: "TEXT", nullable: false), - task_id = table.Column(type: "INTEGER", nullable: false), - task_snapshot = table.Column(type: "TEXT", maxLength: 8000, nullable: false), - runtime_seconds = table.Column(type: "REAL", nullable: false), - start_time = table.Column(type: "TEXT", nullable: false), - end_time = table.Column(type: "TEXT", nullable: true), - success = table.Column(type: "INTEGER", nullable: false), - agent_connection_id = table.Column(type: "TEXT", nullable: true), - exit_code = table.Column(type: "INTEGER", nullable: true), - error_category = table.Column(type: "TEXT", nullable: false), - output = table.Column(type: "TEXT", maxLength: 2000, nullable: true), - output_path = table.Column(type: "TEXT", maxLength: 512, nullable: true), - workflow_run_id = table.Column(type: "TEXT", nullable: true), - schedule_id = table.Column(type: "TEXT", nullable: true), - created = table.Column(type: "TEXT", nullable: false), - last_updated = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_jobs", x => x.id); - table.ForeignKey( - name: "fk_jobs_registered_connections_agent_connection_id", - column: x => x.agent_connection_id, - principalTable: "registered_connections", - principalColumn: "id"); - table.ForeignKey( - name: "fk_jobs_schedules_schedule_id", - column: x => x.schedule_id, - principalTable: "schedules", - principalColumn: "id"); - table.ForeignKey( - name: "fk_jobs_tasks_task_id", - column: x => x.task_id, - principalTable: "tasks", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_jobs_workflow_runs_workflow_run_id", - column: x => x.workflow_run_id, - principalTable: "workflow_runs", - principalColumn: "id"); - }); - - migrationBuilder.CreateTable( - name: "workflow_step_dependencies", - columns: table => new - { - step_id = table.Column(type: "INTEGER", nullable: false), - depends_on_step_id = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_step_dependencies", x => new { x.step_id, x.depends_on_step_id }); - table.ForeignKey( - name: "fk_workflow_step_dependencies_workflow_steps_depends_on_step_id", - column: x => x.depends_on_step_id, - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "fk_workflow_step_dependencies_workflow_steps_step_id", - column: x => x.step_id, - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "workflow_run_variables", - columns: table => new - { - id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - workflow_run_id = table.Column(type: "TEXT", nullable: false), - variable_name = table.Column(type: "TEXT", maxLength: 128, nullable: false), - value = table.Column(type: "TEXT", nullable: false), - version = table.Column(type: "INTEGER", nullable: false), - produced_by_step_id = table.Column(type: "INTEGER", nullable: true), - produced_by_job_id = table.Column(type: "TEXT", nullable: true), - source = table.Column(type: "TEXT", nullable: false), - created = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_workflow_run_variables", x => x.id); - table.ForeignKey( - name: "fk_workflow_run_variables_jobs_produced_by_job_id", - column: x => x.produced_by_job_id, - principalTable: "jobs", - principalColumn: "id", - onDelete: ReferentialAction.SetNull); - table.ForeignKey( - name: "fk_workflow_run_variables_workflow_runs_workflow_run_id", - column: x => x.workflow_run_id, - principalTable: "workflow_runs", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "fk_workflow_run_variables_workflow_steps_produced_by_step_id", - column: x => x.produced_by_step_id, - principalTable: "workflow_steps", - principalColumn: "id", - onDelete: ReferentialAction.SetNull); - }); - - migrationBuilder.CreateIndex( - name: "ix_holiday_calendars_name", - table: "holiday_calendars", - column: "name", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_holiday_dates_holiday_calendar_id_date", - table: "holiday_dates", - columns: new[] { "holiday_calendar_id", "date" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_holiday_dates_holiday_rule_id", - table: "holiday_dates", - column: "holiday_rule_id"); - - migrationBuilder.CreateIndex( - name: "ix_holiday_rules_holiday_calendar_id", - table: "holiday_rules", - column: "holiday_calendar_id"); - - migrationBuilder.CreateIndex( - name: "ix_jobs_agent_connection_id", - table: "jobs", - column: "agent_connection_id"); - - migrationBuilder.CreateIndex( - name: "ix_jobs_schedule_id", - table: "jobs", - column: "schedule_id"); - - migrationBuilder.CreateIndex( - name: "ix_jobs_task_id", - table: "jobs", - column: "task_id"); - - migrationBuilder.CreateIndex( - name: "ix_jobs_workflow_run_id", - table: "jobs", - column: "workflow_run_id"); - - migrationBuilder.CreateIndex( - name: "ix_registered_connections_connection_name", - table: "registered_connections", - column: "connection_name"); - - migrationBuilder.CreateIndex( - name: "ix_registered_connections_remote_url", - table: "registered_connections", - column: "remote_url"); - - migrationBuilder.CreateIndex( - name: "ix_registration_bundles_bundle_id", - table: "registration_bundles", - column: "bundle_id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_schedule_audit_log_schedule_id_occurrence_utc_time", - table: "schedule_audit_log", - columns: new[] { "schedule_id", "occurrence_utc_time" }); - - migrationBuilder.CreateIndex( - name: "ix_schedule_holiday_calendars_holiday_calendar_id", - table: "schedule_holiday_calendars", - column: "holiday_calendar_id"); - - migrationBuilder.CreateIndex( - name: "ix_schedule_holiday_calendars_schedule_id", - table: "schedule_holiday_calendars", - column: "schedule_id", - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_task_schedules_schedule_id", - table: "task_schedules", - column: "schedule_id"); - - migrationBuilder.CreateIndex( - name: "ix_tasks_workflow_id", - table: "tasks", - column: "workflow_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_produced_by_job_id", - table: "workflow_run_variables", - column: "produced_by_job_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_produced_by_step_id", - table: "workflow_run_variables", - column: "produced_by_step_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_run_variables_workflow_run_id_variable_name_version", - table: "workflow_run_variables", - columns: new[] { "workflow_run_id", "variable_name", "version" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "ix_workflow_runs_workflow_id", - table: "workflow_runs", - column: "workflow_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_schedules_schedule_id", - table: "workflow_schedules", - column: "schedule_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_step_dependencies_depends_on_step_id", - table: "workflow_step_dependencies", - column: "depends_on_step_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_steps_agent_connection_id_override", - table: "workflow_steps", - column: "agent_connection_id_override"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_steps_task_id", - table: "workflow_steps", - column: "task_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_steps_workflow_id", - table: "workflow_steps", - column: "workflow_id"); - - migrationBuilder.CreateIndex( - name: "ix_workflow_variables_workflow_id_name", - table: "workflow_variables", - columns: new[] { "workflow_id", "name" }, - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "daily_recurrence"); - - migrationBuilder.DropTable( - name: "holiday_dates"); - - migrationBuilder.DropTable( - name: "monthly_recurrence"); - - migrationBuilder.DropTable( - name: "registration_bundles"); - - migrationBuilder.DropTable( - name: "schedule_audit_log"); - - migrationBuilder.DropTable( - name: "schedule_expiration"); - - migrationBuilder.DropTable( - name: "schedule_holiday_calendars"); - - migrationBuilder.DropTable( - name: "schedule_repeat_options"); - - migrationBuilder.DropTable( - name: "schedule_start_datetimeinfo"); - - migrationBuilder.DropTable( - name: "task_schedules"); - - migrationBuilder.DropTable( - name: "weekly_recurrence"); - - migrationBuilder.DropTable( - name: "workflow_run_variables"); - - migrationBuilder.DropTable( - name: "workflow_schedules"); - - migrationBuilder.DropTable( - name: "workflow_step_dependencies"); - - migrationBuilder.DropTable( - name: "workflow_variables"); - - migrationBuilder.DropTable( - name: "holiday_rules"); - - migrationBuilder.DropTable( - name: "jobs"); - - migrationBuilder.DropTable( - name: "workflow_steps"); - - migrationBuilder.DropTable( - name: "holiday_calendars"); - - migrationBuilder.DropTable( - name: "schedules"); - - migrationBuilder.DropTable( - name: "workflow_runs"); - - migrationBuilder.DropTable( - name: "registered_connections"); - - migrationBuilder.DropTable( - name: "tasks"); - - migrationBuilder.DropTable( - name: "workflows"); - } - } -} diff --git a/src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.Designer.cs b/src/Werkr.Data/Migrations/Sqlite/20260314004323_InitialCreate.Designer.cs similarity index 89% rename from src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.Designer.cs rename to src/Werkr.Data/Migrations/Sqlite/20260314004323_InitialCreate.Designer.cs index c0268ff..a8c96d5 100644 --- a/src/Werkr.Data/Migrations/Sqlite/20260312050748_InitialCreate.Designer.cs +++ b/src/Werkr.Data/Migrations/Sqlite/20260314004323_InitialCreate.Designer.cs @@ -12,7 +12,7 @@ namespace Werkr.Data.Migrations.Sqlite { [DbContext(typeof(SqliteWerkrDbContext))] - [Migration("20260312050748_InitialCreate")] + [Migration("20260314004323_InitialCreate")] partial class InitialCreate { /// @@ -732,6 +732,68 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("weekly_recurrence", (string)null); }); + modelBuilder.Entity("Werkr.Data.Entities.Settings.SavedFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Created") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("created"); + + b.Property("CriteriaJson") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("criteria_json"); + + b.Property("IsShared") + .HasColumnType("INTEGER") + .HasColumnName("is_shared"); + + b.Property("LastUpdated") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_updated"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("TEXT") + .HasColumnName("owner_id"); + + b.Property("PageKey") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("page_key"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_saved_filters"); + + b.HasIndex("PageKey", "IsShared") + .HasDatabaseName("ix_saved_filters_page_key_is_shared"); + + b.HasIndex("PageKey", "OwnerId") + .HasDatabaseName("ix_saved_filters_page_key_owner_id"); + + b.ToTable("saved_filters", (string)null); + }); + modelBuilder.Entity("Werkr.Data.Entities.Tasks.TaskSchedule", b => { b.Property("TaskId") @@ -817,6 +879,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasColumnName("start_time"); + b.Property("StepId") + .HasColumnType("INTEGER") + .HasColumnName("step_id"); + b.Property("Success") .HasColumnType("INTEGER") .HasColumnName("success"); @@ -849,11 +915,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("ScheduleId") .HasDatabaseName("ix_jobs_schedule_id"); + b.HasIndex("StepId") + .HasDatabaseName("ix_jobs_step_id"); + b.HasIndex("TaskId") .HasDatabaseName("ix_jobs_task_id"); - b.HasIndex("WorkflowRunId") - .HasDatabaseName("ix_jobs_workflow_run_id"); + b.HasIndex("WorkflowRunId", "StepId") + .HasDatabaseName("IX_jobs_WorkflowRunId_StepId"); b.ToTable("jobs", (string)null); }); @@ -962,6 +1031,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER") .HasColumnName("id"); + b.Property("Annotations") + .HasColumnType("TEXT") + .HasColumnName("annotations"); + b.Property("Created") .IsRequired() .HasColumnType("TEXT") @@ -1245,6 +1318,86 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("workflow_step_dependencies", (string)null); }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStepExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Attempt") + .HasColumnType("INTEGER") + .HasColumnName("attempt"); + + b.Property("Created") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("created"); + + b.Property("EndTime") + .HasColumnType("TEXT") + .HasColumnName("end_time"); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("TEXT") + .HasColumnName("error_message"); + + b.Property("JobId") + .HasColumnType("TEXT") + .HasColumnName("job_id"); + + b.Property("LastUpdated") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_updated"); + + b.Property("SkipReason") + .HasMaxLength(2000) + .HasColumnType("TEXT") + .HasColumnName("skip_reason"); + + b.Property("StartTime") + .HasColumnType("TEXT") + .HasColumnName("start_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("StepId") + .HasColumnType("INTEGER") + .HasColumnName("step_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER") + .HasColumnName("version"); + + b.Property("WorkflowRunId") + .HasColumnType("TEXT") + .HasColumnName("workflow_run_id"); + + b.HasKey("Id") + .HasName("pk_workflow_step_executions"); + + b.HasIndex("JobId") + .HasDatabaseName("ix_workflow_step_executions_job_id"); + + b.HasIndex("StepId") + .HasDatabaseName("ix_workflow_step_executions_step_id"); + + b.HasIndex("WorkflowRunId") + .HasDatabaseName("ix_workflow_step_executions_workflow_run_id"); + + b.HasIndex("WorkflowRunId", "StepId", "Attempt") + .IsUnique() + .HasDatabaseName("ix_workflow_step_executions_workflow_run_id_step_id_attempt"); + + b.ToTable("workflow_step_executions", (string)null); + }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowVariable", b => { b.Property("Id") @@ -1466,6 +1619,12 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasForeignKey("ScheduleId") .HasConstraintName("fk_jobs_schedules_schedule_id"); + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowStep", "Step") + .WithMany() + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_jobs_workflow_steps_step_id"); + b.HasOne("Werkr.Data.Entities.Tasks.WerkrTask", "Task") .WithMany() .HasForeignKey("TaskId") @@ -1482,6 +1641,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Schedule"); + b.Navigation("Step"); + b.Navigation("Task"); b.Navigation("WorkflowRun"); @@ -1607,6 +1768,35 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Step"); }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStepExecution", b => + { + b.HasOne("Werkr.Data.Entities.Tasks.WerkrJob", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_workflow_step_executions_jobs_job_id"); + + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowStep", "Step") + .WithMany() + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_workflow_step_executions_workflow_steps_step_id"); + + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowRun", "WorkflowRun") + .WithMany("StepExecutions") + .HasForeignKey("WorkflowRunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_workflow_step_executions_workflow_runs_workflow_run_id"); + + b.Navigation("Job"); + + b.Navigation("Step"); + + b.Navigation("WorkflowRun"); + }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowVariable", b => { b.HasOne("Werkr.Data.Entities.Workflows.Workflow", "Workflow") @@ -1677,6 +1867,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Jobs"); b.Navigation("RunVariables"); + + b.Navigation("StepExecutions"); }); modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStep", b => diff --git a/src/Werkr.Data/Migrations/Sqlite/20260314004323_InitialCreate.cs b/src/Werkr.Data/Migrations/Sqlite/20260314004323_InitialCreate.cs new file mode 100644 index 0000000..fc59fd3 --- /dev/null +++ b/src/Werkr.Data/Migrations/Sqlite/20260314004323_InitialCreate.cs @@ -0,0 +1,923 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Werkr.Data.Migrations.Sqlite; + +/// +public partial class InitialCreate : Migration { + /// + protected override void Up( MigrationBuilder migrationBuilder ) { + _ = migrationBuilder.CreateTable( + name: "holiday_calendars", + columns: table => new { + id = table.Column( type: "TEXT", nullable: false ), + name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), + description = table.Column( type: "TEXT", maxLength: 1024, nullable: false ), + is_system_calendar = table.Column( type: "INTEGER", nullable: false ), + created_utc = table.Column( type: "TEXT", nullable: false ), + updated_utc = table.Column( type: "TEXT", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_holiday_calendars", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "registered_connections", + columns: table => new { + id = table.Column( type: "TEXT", nullable: false ), + connection_name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), + remote_url = table.Column( type: "TEXT", maxLength: 2048, nullable: false ), + local_public_key = table.Column( type: "TEXT", nullable: false ), + local_private_key = table.Column( type: "TEXT", nullable: false ), + remote_public_key = table.Column( type: "TEXT", nullable: false ), + outbound_api_key = table.Column( type: "TEXT", maxLength: 512, nullable: false ), + inbound_api_key_hash = table.Column( type: "TEXT", maxLength: 512, nullable: false ), + shared_key = table.Column( type: "TEXT", nullable: false ), + previous_shared_key = table.Column( type: "TEXT", nullable: true ), + active_key_id = table.Column( type: "TEXT", maxLength: 128, nullable: true ), + previous_key_id = table.Column( type: "TEXT", maxLength: 128, nullable: true ), + is_server = table.Column( type: "INTEGER", nullable: false ), + status = table.Column( type: "TEXT", nullable: false ), + last_seen = table.Column( type: "TEXT", nullable: true ), + tags = table.Column( type: "TEXT", nullable: false ), + allowed_paths = table.Column( type: "TEXT", nullable: false ), + enforce_allowlist = table.Column( type: "INTEGER", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_registered_connections", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "registration_bundles", + columns: table => new { + id = table.Column( type: "TEXT", nullable: false ), + connection_name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), + server_public_key = table.Column( type: "TEXT", nullable: false ), + server_private_key = table.Column( type: "TEXT", nullable: false ), + bundle_id = table.Column( type: "TEXT", nullable: false ), + status = table.Column( type: "TEXT", nullable: false ), + expires_at = table.Column( type: "TEXT", nullable: false ), + key_size = table.Column( type: "INTEGER", nullable: false ), + tags = table.Column( type: "TEXT", nullable: false ), + allowed_paths = table.Column( type: "TEXT", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_registration_bundles", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "saved_filters", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + owner_id = table.Column( type: "TEXT", maxLength: 450, nullable: false ), + page_key = table.Column( type: "TEXT", maxLength: 50, nullable: false ), + name = table.Column( type: "TEXT", maxLength: 200, nullable: false ), + criteria_json = table.Column( type: "TEXT", maxLength: 4096, nullable: false ), + is_shared = table.Column( type: "INTEGER", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_saved_filters", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedules", + columns: table => new { + id = table.Column( type: "TEXT", nullable: false ), + name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), + stop_task_after_minutes = table.Column( type: "INTEGER", nullable: false ), + catch_up_enabled = table.Column( type: "INTEGER", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedules", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflows", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), + description = table.Column( type: "TEXT", maxLength: 2000, nullable: false ), + enabled = table.Column( type: "INTEGER", nullable: false ), + target_tags = table.Column( type: "TEXT", nullable: true ), + annotations = table.Column( type: "TEXT", nullable: true ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflows", x => x.id ); + } ); + + _ = migrationBuilder.CreateTable( + name: "holiday_rules", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + holiday_calendar_id = table.Column( type: "TEXT", nullable: false ), + name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), + rule_type = table.Column( type: "TEXT", nullable: false ), + month = table.Column( type: "INTEGER", nullable: true ), + day = table.Column( type: "INTEGER", nullable: true ), + day_of_week = table.Column( type: "INTEGER", nullable: true ), + week_number = table.Column( type: "INTEGER", nullable: true ), + window_start = table.Column( type: "TEXT", nullable: true ), + window_end = table.Column( type: "TEXT", nullable: true ), + window_time_zone_id = table.Column( type: "TEXT", maxLength: 128, nullable: true ), + observance_rule = table.Column( type: "TEXT", nullable: false ), + year_start = table.Column( type: "INTEGER", nullable: true ), + year_end = table.Column( type: "INTEGER", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_holiday_rules", x => x.id ); + _ = table.ForeignKey( + name: "fk_holiday_rules_holiday_calendars_holiday_calendar_id", + column: x => x.holiday_calendar_id, + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "daily_recurrence", + columns: table => new { + schedule_id = table.Column( type: "TEXT", nullable: false ), + day_interval = table.Column( type: "INTEGER", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_daily_recurrence", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_daily_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "monthly_recurrence", + columns: table => new { + schedule_id = table.Column( type: "TEXT", nullable: false ), + day_numbers = table.Column( type: "TEXT", nullable: true ), + months_of_year = table.Column( type: "INTEGER", nullable: false ), + week_number = table.Column( type: "INTEGER", nullable: true ), + days_of_week = table.Column( type: "INTEGER", nullable: true ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_monthly_recurrence", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_monthly_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedule_audit_log", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + schedule_id = table.Column( type: "TEXT", nullable: false ), + occurrence_utc_time = table.Column( type: "TEXT", nullable: false ), + calendar_name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), + holiday_name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), + mode = table.Column( type: "TEXT", nullable: false ), + created_utc = table.Column( type: "TEXT", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedule_audit_log", x => x.id ); + _ = table.ForeignKey( + name: "fk_schedule_audit_log_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedule_expiration", + columns: table => new { + schedule_id = table.Column( type: "TEXT", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ), + date = table.Column( type: "TEXT", nullable: false ), + time = table.Column( type: "TEXT", nullable: false ), + time_zone = table.Column( type: "TEXT", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedule_expiration", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_schedule_expiration_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedule_holiday_calendars", + columns: table => new { + schedule_id = table.Column( type: "TEXT", nullable: false ), + holiday_calendar_id = table.Column( type: "TEXT", nullable: false ), + mode = table.Column( type: "TEXT", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedule_holiday_calendars", x => new { x.schedule_id, x.holiday_calendar_id } ); + _ = table.ForeignKey( + name: "fk_schedule_holiday_calendars_holiday_calendars_holiday_calendar_id", + column: x => x.holiday_calendar_id, + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_schedule_holiday_calendars_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedule_repeat_options", + columns: table => new { + schedule_id = table.Column( type: "TEXT", nullable: false ), + repeat_interval_minutes = table.Column( type: "INTEGER", nullable: false ), + repeat_duration_minutes = table.Column( type: "INTEGER", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedule_repeat_options", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_schedule_repeat_options_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "schedule_start_datetimeinfo", + columns: table => new { + schedule_id = table.Column( type: "TEXT", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ), + date = table.Column( type: "TEXT", nullable: false ), + time = table.Column( type: "TEXT", nullable: false ), + time_zone = table.Column( type: "TEXT", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_schedule_start_datetimeinfo", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_schedule_start_datetimeinfo_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "weekly_recurrence", + columns: table => new { + schedule_id = table.Column( type: "TEXT", nullable: false ), + week_interval = table.Column( type: "INTEGER", nullable: false ), + days_of_week = table.Column( type: "INTEGER", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_weekly_recurrence", x => x.schedule_id ); + _ = table.ForeignKey( + name: "fk_weekly_recurrence_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "tasks", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), + description = table.Column( type: "TEXT", maxLength: 2000, nullable: false ), + action_type = table.Column( type: "TEXT", nullable: false ), + workflow_id = table.Column( type: "INTEGER", nullable: true ), + content = table.Column( type: "TEXT", maxLength: 8000, nullable: false ), + arguments = table.Column( type: "TEXT", nullable: true ), + target_tags = table.Column( type: "TEXT", nullable: false ), + enabled = table.Column( type: "INTEGER", nullable: false ), + is_ephemeral = table.Column( type: "INTEGER", nullable: false ), + timeout_minutes = table.Column( type: "INTEGER", nullable: true ), + sync_interval_minutes = table.Column( type: "INTEGER", nullable: false ), + success_criteria = table.Column( type: "TEXT", maxLength: 500, nullable: true ), + action_sub_type = table.Column( type: "TEXT", maxLength: 30, nullable: true ), + action_parameters = table.Column( type: "TEXT", nullable: true ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_tasks", x => x.id ); + _ = table.ForeignKey( + name: "fk_tasks_workflows_workflow_id", + column: x => x.workflow_id, + principalTable: "workflows", + principalColumn: "id" ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_runs", + columns: table => new { + id = table.Column( type: "TEXT", nullable: false ), + workflow_id = table.Column( type: "INTEGER", nullable: false ), + start_time = table.Column( type: "TEXT", nullable: false ), + end_time = table.Column( type: "TEXT", nullable: true ), + status = table.Column( type: "TEXT", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_runs", x => x.id ); + _ = table.ForeignKey( + name: "fk_workflow_runs_workflows_workflow_id", + column: x => x.workflow_id, + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_schedules", + columns: table => new { + workflow_id = table.Column( type: "INTEGER", nullable: false ), + schedule_id = table.Column( type: "TEXT", nullable: false ), + created_at_utc = table.Column( type: "TEXT", nullable: false ), + is_one_time = table.Column( type: "INTEGER", nullable: false ), + workflow_run_id = table.Column( type: "TEXT", nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_schedules", x => new { x.workflow_id, x.schedule_id } ); + _ = table.ForeignKey( + name: "fk_workflow_schedules_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_workflow_schedules_workflows_workflow_id", + column: x => x.workflow_id, + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_variables", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + workflow_id = table.Column( type: "INTEGER", nullable: false ), + name = table.Column( type: "TEXT", maxLength: 128, nullable: false ), + description = table.Column( type: "TEXT", maxLength: 500, nullable: true ), + default_value = table.Column( type: "TEXT", nullable: true ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_variables", x => x.id ); + _ = table.ForeignKey( + name: "fk_workflow_variables_workflows_workflow_id", + column: x => x.workflow_id, + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "holiday_dates", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + holiday_calendar_id = table.Column( type: "TEXT", nullable: false ), + holiday_rule_id = table.Column( type: "INTEGER", nullable: true ), + date = table.Column( type: "TEXT", nullable: false ), + name = table.Column( type: "TEXT", maxLength: 256, nullable: false ), + year = table.Column( type: "INTEGER", nullable: false ), + window_start = table.Column( type: "TEXT", nullable: true ), + window_end = table.Column( type: "TEXT", nullable: true ), + window_time_zone_id = table.Column( type: "TEXT", maxLength: 128, nullable: true ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_holiday_dates", x => x.id ); + _ = table.ForeignKey( + name: "fk_holiday_dates_holiday_calendars_holiday_calendar_id", + column: x => x.holiday_calendar_id, + principalTable: "holiday_calendars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_holiday_dates_holiday_rules_holiday_rule_id", + column: x => x.holiday_rule_id, + principalTable: "holiday_rules", + principalColumn: "id", + onDelete: ReferentialAction.SetNull ); + } ); + + _ = migrationBuilder.CreateTable( + name: "task_schedules", + columns: table => new { + task_id = table.Column( type: "INTEGER", nullable: false ), + schedule_id = table.Column( type: "TEXT", nullable: false ), + created_at_utc = table.Column( type: "TEXT", nullable: false ), + is_one_time = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_task_schedules", x => new { x.task_id, x.schedule_id } ); + _ = table.ForeignKey( + name: "fk_task_schedules_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_task_schedules_tasks_task_id", + column: x => x.task_id, + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_steps", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + workflow_id = table.Column( type: "INTEGER", nullable: false ), + task_id = table.Column( type: "INTEGER", nullable: false ), + order = table.Column( type: "INTEGER", nullable: false ), + control_statement = table.Column( type: "TEXT", nullable: false ), + condition_expression = table.Column( type: "TEXT", maxLength: 2000, nullable: true ), + max_iterations = table.Column( type: "INTEGER", nullable: false ), + agent_connection_id_override = table.Column( type: "TEXT", nullable: true ), + dependency_mode = table.Column( type: "TEXT", nullable: false ), + input_variable_name = table.Column( type: "TEXT", maxLength: 128, nullable: true ), + output_variable_name = table.Column( type: "TEXT", maxLength: 128, nullable: true ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_steps", x => x.id ); + _ = table.ForeignKey( + name: "fk_workflow_steps_registered_connections_agent_connection_id_override", + column: x => x.agent_connection_id_override, + principalTable: "registered_connections", + principalColumn: "id" ); + _ = table.ForeignKey( + name: "fk_workflow_steps_tasks_task_id", + column: x => x.task_id, + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_workflow_steps_workflows_workflow_id", + column: x => x.workflow_id, + principalTable: "workflows", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "jobs", + columns: table => new { + id = table.Column( type: "TEXT", nullable: false ), + task_id = table.Column( type: "INTEGER", nullable: false ), + task_snapshot = table.Column( type: "TEXT", maxLength: 8000, nullable: false ), + runtime_seconds = table.Column( type: "REAL", nullable: false ), + start_time = table.Column( type: "TEXT", nullable: false ), + end_time = table.Column( type: "TEXT", nullable: true ), + success = table.Column( type: "INTEGER", nullable: false ), + agent_connection_id = table.Column( type: "TEXT", nullable: true ), + exit_code = table.Column( type: "INTEGER", nullable: true ), + error_category = table.Column( type: "TEXT", nullable: false ), + output = table.Column( type: "TEXT", maxLength: 2000, nullable: true ), + output_path = table.Column( type: "TEXT", maxLength: 512, nullable: true ), + workflow_run_id = table.Column( type: "TEXT", nullable: true ), + schedule_id = table.Column( type: "TEXT", nullable: true ), + step_id = table.Column( type: "INTEGER", nullable: true ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_jobs", x => x.id ); + _ = table.ForeignKey( + name: "fk_jobs_registered_connections_agent_connection_id", + column: x => x.agent_connection_id, + principalTable: "registered_connections", + principalColumn: "id" ); + _ = table.ForeignKey( + name: "fk_jobs_schedules_schedule_id", + column: x => x.schedule_id, + principalTable: "schedules", + principalColumn: "id" ); + _ = table.ForeignKey( + name: "fk_jobs_tasks_task_id", + column: x => x.task_id, + principalTable: "tasks", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_jobs_workflow_runs_workflow_run_id", + column: x => x.workflow_run_id, + principalTable: "workflow_runs", + principalColumn: "id" ); + _ = table.ForeignKey( + name: "fk_jobs_workflow_steps_step_id", + column: x => x.step_id, + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.SetNull ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_step_dependencies", + columns: table => new { + step_id = table.Column( type: "INTEGER", nullable: false ), + depends_on_step_id = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_step_dependencies", x => new { x.step_id, x.depends_on_step_id } ); + _ = table.ForeignKey( + name: "fk_workflow_step_dependencies_workflow_steps_depends_on_step_id", + column: x => x.depends_on_step_id, + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.Restrict ); + _ = table.ForeignKey( + name: "fk_workflow_step_dependencies_workflow_steps_step_id", + column: x => x.step_id, + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_run_variables", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + workflow_run_id = table.Column( type: "TEXT", nullable: false ), + variable_name = table.Column( type: "TEXT", maxLength: 128, nullable: false ), + value = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ), + produced_by_step_id = table.Column( type: "INTEGER", nullable: true ), + produced_by_job_id = table.Column( type: "TEXT", nullable: true ), + source = table.Column( type: "TEXT", nullable: false ), + created = table.Column( type: "TEXT", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_run_variables", x => x.id ); + _ = table.ForeignKey( + name: "fk_workflow_run_variables_jobs_produced_by_job_id", + column: x => x.produced_by_job_id, + principalTable: "jobs", + principalColumn: "id", + onDelete: ReferentialAction.SetNull ); + _ = table.ForeignKey( + name: "fk_workflow_run_variables_workflow_runs_workflow_run_id", + column: x => x.workflow_run_id, + principalTable: "workflow_runs", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_workflow_run_variables_workflow_steps_produced_by_step_id", + column: x => x.produced_by_step_id, + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.SetNull ); + } ); + + _ = migrationBuilder.CreateTable( + name: "workflow_step_executions", + columns: table => new { + id = table.Column( type: "INTEGER", nullable: false ) + .Annotation( "Sqlite:Autoincrement", true ), + workflow_run_id = table.Column( type: "TEXT", nullable: false ), + step_id = table.Column( type: "INTEGER", nullable: false ), + attempt = table.Column( type: "INTEGER", nullable: false ), + status = table.Column( type: "TEXT", nullable: false ), + start_time = table.Column( type: "TEXT", nullable: true ), + end_time = table.Column( type: "TEXT", nullable: true ), + job_id = table.Column( type: "TEXT", nullable: true ), + error_message = table.Column( type: "TEXT", maxLength: 4000, nullable: true ), + skip_reason = table.Column( type: "TEXT", maxLength: 2000, nullable: true ), + created = table.Column( type: "TEXT", nullable: false ), + last_updated = table.Column( type: "TEXT", nullable: false ), + version = table.Column( type: "INTEGER", nullable: false ) + }, + constraints: table => { + _ = table.PrimaryKey( "pk_workflow_step_executions", x => x.id ); + _ = table.ForeignKey( + name: "fk_workflow_step_executions_jobs_job_id", + column: x => x.job_id, + principalTable: "jobs", + principalColumn: "id", + onDelete: ReferentialAction.SetNull ); + _ = table.ForeignKey( + name: "fk_workflow_step_executions_workflow_runs_workflow_run_id", + column: x => x.workflow_run_id, + principalTable: "workflow_runs", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + _ = table.ForeignKey( + name: "fk_workflow_step_executions_workflow_steps_step_id", + column: x => x.step_id, + principalTable: "workflow_steps", + principalColumn: "id", + onDelete: ReferentialAction.Cascade ); + } ); + + _ = migrationBuilder.CreateIndex( + name: "ix_holiday_calendars_name", + table: "holiday_calendars", + column: "name", + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_holiday_dates_holiday_calendar_id_date", + table: "holiday_dates", + columns: ["holiday_calendar_id", "date"], + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_holiday_dates_holiday_rule_id", + table: "holiday_dates", + column: "holiday_rule_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_holiday_rules_holiday_calendar_id", + table: "holiday_rules", + column: "holiday_calendar_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_jobs_agent_connection_id", + table: "jobs", + column: "agent_connection_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_jobs_schedule_id", + table: "jobs", + column: "schedule_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_jobs_step_id", + table: "jobs", + column: "step_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_jobs_task_id", + table: "jobs", + column: "task_id" ); + + _ = migrationBuilder.CreateIndex( + name: "IX_jobs_WorkflowRunId_StepId", + table: "jobs", + columns: ["workflow_run_id", "step_id"] ); + + _ = migrationBuilder.CreateIndex( + name: "ix_registered_connections_connection_name", + table: "registered_connections", + column: "connection_name" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_registered_connections_remote_url", + table: "registered_connections", + column: "remote_url" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_registration_bundles_bundle_id", + table: "registration_bundles", + column: "bundle_id", + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_saved_filters_page_key_is_shared", + table: "saved_filters", + columns: ["page_key", "is_shared"] ); + + _ = migrationBuilder.CreateIndex( + name: "ix_saved_filters_page_key_owner_id", + table: "saved_filters", + columns: ["page_key", "owner_id"] ); + + _ = migrationBuilder.CreateIndex( + name: "ix_schedule_audit_log_schedule_id_occurrence_utc_time", + table: "schedule_audit_log", + columns: ["schedule_id", "occurrence_utc_time"] ); + + _ = migrationBuilder.CreateIndex( + name: "ix_schedule_holiday_calendars_holiday_calendar_id", + table: "schedule_holiday_calendars", + column: "holiday_calendar_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_schedule_holiday_calendars_schedule_id", + table: "schedule_holiday_calendars", + column: "schedule_id", + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_task_schedules_schedule_id", + table: "task_schedules", + column: "schedule_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_tasks_workflow_id", + table: "tasks", + column: "workflow_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_produced_by_job_id", + table: "workflow_run_variables", + column: "produced_by_job_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_produced_by_step_id", + table: "workflow_run_variables", + column: "produced_by_step_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_run_variables_workflow_run_id_variable_name_version", + table: "workflow_run_variables", + columns: ["workflow_run_id", "variable_name", "version"], + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_runs_workflow_id", + table: "workflow_runs", + column: "workflow_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_schedules_schedule_id", + table: "workflow_schedules", + column: "schedule_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_step_dependencies_depends_on_step_id", + table: "workflow_step_dependencies", + column: "depends_on_step_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_step_executions_job_id", + table: "workflow_step_executions", + column: "job_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_step_executions_step_id", + table: "workflow_step_executions", + column: "step_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_step_executions_workflow_run_id", + table: "workflow_step_executions", + column: "workflow_run_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_step_executions_workflow_run_id_step_id_attempt", + table: "workflow_step_executions", + columns: ["workflow_run_id", "step_id", "attempt"], + unique: true ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_steps_agent_connection_id_override", + table: "workflow_steps", + column: "agent_connection_id_override" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_steps_task_id", + table: "workflow_steps", + column: "task_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_steps_workflow_id", + table: "workflow_steps", + column: "workflow_id" ); + + _ = migrationBuilder.CreateIndex( + name: "ix_workflow_variables_workflow_id_name", + table: "workflow_variables", + columns: ["workflow_id", "name"], + unique: true ); + } + + /// + protected override void Down( MigrationBuilder migrationBuilder ) { + _ = migrationBuilder.DropTable( + name: "daily_recurrence" ); + + _ = migrationBuilder.DropTable( + name: "holiday_dates" ); + + _ = migrationBuilder.DropTable( + name: "monthly_recurrence" ); + + _ = migrationBuilder.DropTable( + name: "registration_bundles" ); + + _ = migrationBuilder.DropTable( + name: "saved_filters" ); + + _ = migrationBuilder.DropTable( + name: "schedule_audit_log" ); + + _ = migrationBuilder.DropTable( + name: "schedule_expiration" ); + + _ = migrationBuilder.DropTable( + name: "schedule_holiday_calendars" ); + + _ = migrationBuilder.DropTable( + name: "schedule_repeat_options" ); + + _ = migrationBuilder.DropTable( + name: "schedule_start_datetimeinfo" ); + + _ = migrationBuilder.DropTable( + name: "task_schedules" ); + + _ = migrationBuilder.DropTable( + name: "weekly_recurrence" ); + + _ = migrationBuilder.DropTable( + name: "workflow_run_variables" ); + + _ = migrationBuilder.DropTable( + name: "workflow_schedules" ); + + _ = migrationBuilder.DropTable( + name: "workflow_step_dependencies" ); + + _ = migrationBuilder.DropTable( + name: "workflow_step_executions" ); + + _ = migrationBuilder.DropTable( + name: "workflow_variables" ); + + _ = migrationBuilder.DropTable( + name: "holiday_rules" ); + + _ = migrationBuilder.DropTable( + name: "jobs" ); + + _ = migrationBuilder.DropTable( + name: "holiday_calendars" ); + + _ = migrationBuilder.DropTable( + name: "schedules" ); + + _ = migrationBuilder.DropTable( + name: "workflow_runs" ); + + _ = migrationBuilder.DropTable( + name: "workflow_steps" ); + + _ = migrationBuilder.DropTable( + name: "registered_connections" ); + + _ = migrationBuilder.DropTable( + name: "tasks" ); + + _ = migrationBuilder.DropTable( + name: "workflows" ); + } +} diff --git a/src/Werkr.Data/Migrations/Sqlite/SqliteWerkrDbContextModelSnapshot.cs b/src/Werkr.Data/Migrations/Sqlite/SqliteWerkrDbContextModelSnapshot.cs index 3bc1c7e..a16cfcd 100644 --- a/src/Werkr.Data/Migrations/Sqlite/SqliteWerkrDbContextModelSnapshot.cs +++ b/src/Werkr.Data/Migrations/Sqlite/SqliteWerkrDbContextModelSnapshot.cs @@ -729,6 +729,68 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("weekly_recurrence", (string)null); }); + modelBuilder.Entity("Werkr.Data.Entities.Settings.SavedFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Created") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("created"); + + b.Property("CriteriaJson") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("criteria_json"); + + b.Property("IsShared") + .HasColumnType("INTEGER") + .HasColumnName("is_shared"); + + b.Property("LastUpdated") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_updated"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("OwnerId") + .IsRequired() + .HasMaxLength(450) + .HasColumnType("TEXT") + .HasColumnName("owner_id"); + + b.Property("PageKey") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("page_key"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_saved_filters"); + + b.HasIndex("PageKey", "IsShared") + .HasDatabaseName("ix_saved_filters_page_key_is_shared"); + + b.HasIndex("PageKey", "OwnerId") + .HasDatabaseName("ix_saved_filters_page_key_owner_id"); + + b.ToTable("saved_filters", (string)null); + }); + modelBuilder.Entity("Werkr.Data.Entities.Tasks.TaskSchedule", b => { b.Property("TaskId") @@ -814,6 +876,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasColumnName("start_time"); + b.Property("StepId") + .HasColumnType("INTEGER") + .HasColumnName("step_id"); + b.Property("Success") .HasColumnType("INTEGER") .HasColumnName("success"); @@ -846,11 +912,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ScheduleId") .HasDatabaseName("ix_jobs_schedule_id"); + b.HasIndex("StepId") + .HasDatabaseName("ix_jobs_step_id"); + b.HasIndex("TaskId") .HasDatabaseName("ix_jobs_task_id"); - b.HasIndex("WorkflowRunId") - .HasDatabaseName("ix_jobs_workflow_run_id"); + b.HasIndex("WorkflowRunId", "StepId") + .HasDatabaseName("IX_jobs_WorkflowRunId_StepId"); b.ToTable("jobs", (string)null); }); @@ -959,6 +1028,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER") .HasColumnName("id"); + b.Property("Annotations") + .HasColumnType("TEXT") + .HasColumnName("annotations"); + b.Property("Created") .IsRequired() .HasColumnType("TEXT") @@ -1242,6 +1315,86 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("workflow_step_dependencies", (string)null); }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStepExecution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Attempt") + .HasColumnType("INTEGER") + .HasColumnName("attempt"); + + b.Property("Created") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("created"); + + b.Property("EndTime") + .HasColumnType("TEXT") + .HasColumnName("end_time"); + + b.Property("ErrorMessage") + .HasMaxLength(4000) + .HasColumnType("TEXT") + .HasColumnName("error_message"); + + b.Property("JobId") + .HasColumnType("TEXT") + .HasColumnName("job_id"); + + b.Property("LastUpdated") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_updated"); + + b.Property("SkipReason") + .HasMaxLength(2000) + .HasColumnType("TEXT") + .HasColumnName("skip_reason"); + + b.Property("StartTime") + .HasColumnType("TEXT") + .HasColumnName("start_time"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("status"); + + b.Property("StepId") + .HasColumnType("INTEGER") + .HasColumnName("step_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("INTEGER") + .HasColumnName("version"); + + b.Property("WorkflowRunId") + .HasColumnType("TEXT") + .HasColumnName("workflow_run_id"); + + b.HasKey("Id") + .HasName("pk_workflow_step_executions"); + + b.HasIndex("JobId") + .HasDatabaseName("ix_workflow_step_executions_job_id"); + + b.HasIndex("StepId") + .HasDatabaseName("ix_workflow_step_executions_step_id"); + + b.HasIndex("WorkflowRunId") + .HasDatabaseName("ix_workflow_step_executions_workflow_run_id"); + + b.HasIndex("WorkflowRunId", "StepId", "Attempt") + .IsUnique() + .HasDatabaseName("ix_workflow_step_executions_workflow_run_id_step_id_attempt"); + + b.ToTable("workflow_step_executions", (string)null); + }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowVariable", b => { b.Property("Id") @@ -1463,6 +1616,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("ScheduleId") .HasConstraintName("fk_jobs_schedules_schedule_id"); + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowStep", "Step") + .WithMany() + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_jobs_workflow_steps_step_id"); + b.HasOne("Werkr.Data.Entities.Tasks.WerkrTask", "Task") .WithMany() .HasForeignKey("TaskId") @@ -1479,6 +1638,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Schedule"); + b.Navigation("Step"); + b.Navigation("Task"); b.Navigation("WorkflowRun"); @@ -1604,6 +1765,35 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Step"); }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStepExecution", b => + { + b.HasOne("Werkr.Data.Entities.Tasks.WerkrJob", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_workflow_step_executions_jobs_job_id"); + + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowStep", "Step") + .WithMany() + .HasForeignKey("StepId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_workflow_step_executions_workflow_steps_step_id"); + + b.HasOne("Werkr.Data.Entities.Workflows.WorkflowRun", "WorkflowRun") + .WithMany("StepExecutions") + .HasForeignKey("WorkflowRunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_workflow_step_executions_workflow_runs_workflow_run_id"); + + b.Navigation("Job"); + + b.Navigation("Step"); + + b.Navigation("WorkflowRun"); + }); + modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowVariable", b => { b.HasOne("Werkr.Data.Entities.Workflows.Workflow", "Workflow") @@ -1674,6 +1864,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Jobs"); b.Navigation("RunVariables"); + + b.Navigation("StepExecutions"); }); modelBuilder.Entity("Werkr.Data.Entities.Workflows.WorkflowStep", b => diff --git a/src/Werkr.Data/WerkrDbContext.cs b/src/Werkr.Data/WerkrDbContext.cs index 56cfd2d..f89b559 100644 --- a/src/Werkr.Data/WerkrDbContext.cs +++ b/src/Werkr.Data/WerkrDbContext.cs @@ -9,6 +9,7 @@ using Werkr.Data.Entities; using Werkr.Data.Entities.Registration; using Werkr.Data.Entities.Schedule; +using Werkr.Data.Entities.Settings; using Werkr.Data.Entities.Tasks; using Werkr.Data.Entities.Workflows; @@ -99,6 +100,12 @@ protected WerkrDbContext( DbContextOptions options ) : base( options ) { } /// Workflow-to-schedule many-to-many join table. public DbSet WorkflowSchedules => Set( ); + /// Per-run-per-step execution tracking (supports retry attempts). + public DbSet WorkflowStepExecutions => Set( ); + + /// Named saved filter views (personal and shared). + public DbSet SavedFilters => Set( ); + /// protected override void OnModelCreating( ModelBuilder modelBuilder ) { base.OnModelCreating( modelBuilder ); @@ -402,6 +409,46 @@ protected override void OnModelCreating( ModelBuilder modelBuilder ) { .HasForeignKey( e => e.ProducedByJobId ) .OnDelete( DeleteBehavior.SetNull ); } ); + + // WerkrJob — StepId FK and index + _ = modelBuilder.Entity( entity => { + _ = entity.HasIndex( e => new { e.WorkflowRunId, e.StepId } ) + .HasDatabaseName( "IX_jobs_WorkflowRunId_StepId" ); + + _ = entity.HasOne( e => e.Step ) + .WithMany( ) + .HasForeignKey( e => e.StepId ) + .OnDelete( DeleteBehavior.SetNull ); + } ); + + // WorkflowStepExecution — per-run-per-step execution tracking + _ = modelBuilder.Entity( entity => { + _ = entity.HasIndex( e => new { e.WorkflowRunId, e.StepId, e.Attempt } ) + .IsUnique( ); + + _ = entity.HasIndex( e => e.WorkflowRunId ); + + _ = entity.HasOne( e => e.WorkflowRun ) + .WithMany( r => r.StepExecutions ) + .HasForeignKey( e => e.WorkflowRunId ) + .OnDelete( DeleteBehavior.Cascade ); + + _ = entity.HasOne( e => e.Step ) + .WithMany( ) + .HasForeignKey( e => e.StepId ) + .OnDelete( DeleteBehavior.Cascade ); + + _ = entity.HasOne( e => e.Job ) + .WithMany( ) + .HasForeignKey( e => e.JobId ) + .OnDelete( DeleteBehavior.SetNull ); + } ); + + // SavedFilter — named filter views per page per user + _ = modelBuilder.Entity( entity => { + _ = entity.HasIndex( e => new { e.PageKey, e.OwnerId } ); + _ = entity.HasIndex( e => new { e.PageKey, e.IsShared } ); + } ); } /// @@ -467,6 +514,10 @@ protected override void ConfigureConventions( ModelConfigurationBuilder configur // VariableSource ↔ string _ = configurationBuilder.Properties( ) .HaveConversion( ); + + // StepExecutionStatus ↔ string + _ = configurationBuilder.Properties( ) + .HaveConversion( ); } /// @@ -587,4 +638,9 @@ private sealed class VariableSourceStringConverter( ) : ValueConverter( v => v.ToString( ), v => Enum.Parse( v ) ); + + private sealed class StepExecutionStatusStringConverter( ) + : ValueConverter( + v => v.ToString( ), + v => Enum.Parse( v ) ); } diff --git a/src/Werkr.Data/packages.lock.json b/src/Werkr.Data/packages.lock.json index ec800d0..f581cba 100644 --- a/src/Werkr.Data/packages.lock.json +++ b/src/Werkr.Data/packages.lock.json @@ -15,60 +15,60 @@ }, "Microsoft.EntityFrameworkCore": { "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "kzTsfFK2GCytp6DDTfQOmxPU4gbGdrIlP7PxrxF3ESNLtfXrC8BoUVZENBN2WORlZPAD7CVX6AYIglgkpXQooA==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.4", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Design": { "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "FmiUU5xdu1chVxnmsu/mEpCKVQ5+lvIxdP0194lE7HfoU1jO4z/9qnWZpd0kSkVve4gOnRm1lE20kkhlMqJJIg==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "gm6f0cC2w/2tcd4GeZJqEMruTercpIJfO5sSAFLtqTqblDBHgAFk70xwshUIUVX4I6sZwdEUSd1YxoKFk1AL0w==", "dependencies": { "Humanizer.Core": "2.14.1", "Microsoft.Build.Framework": "18.0.2", "Microsoft.CodeAnalysis.CSharp": "5.0.0", "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", "Mono.TextTemplating": "3.0.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "Direct", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "cbc/Ave31CbPQ9E29dfaA4QjcsBoc8KokNlLC6Noj0uToHDQ9PPllD+k6HluVbaFpflsU8XGwrQxOoyvXlU97g==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", "SQLitePCLRaw.core": "2.1.11" } }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)", - "Npgsql": "10.0.0" + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" } }, "Humanizer.Core": { @@ -78,8 +78,8 @@ }, "Microsoft.AspNetCore.Metadata": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "levTTo69d5gYARtSzRV4wMvD1YUtkWvI/fOiTEG+k4v1WEEFBxrHLdUVEvxCfiuCb1Y1XuPAbbBsKQi9wNtU3w==" + "resolved": "10.0.5", + "contentHash": "nXVB1K4RzyhDHKYWLiq3+aJopJZKO5ojFqHV9PZ74fe4VWM/8itoouqsd2KIqSooIwQ13UDNlPQfN2rWr7hc2A==" }, "Microsoft.Build.Framework": { "type": "Transitive", @@ -153,184 +153,184 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "qDcJqCfN1XYyX0ID/Hd9/kQTRvlia8S+Yuwyl9uFhBIKnOCbl9WMdGQCzbZUKbkpkfvf3P9CDdXsnxHyE3O0Aw==" + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "pQeMHCyD3yTtCEGnHV4VsgKUvrESo3MR5mnh8sgQ1hWYmI1YFsUutDowBIxkobeWRtaRmBqQAtF7XQFW6FWuNA==" + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "DOTjTHy93W3TwpMLM4SCm0n57Sc0Jj3+m2S6LSTstKyBB34eT1UouaMS19mpWwvtj42+sRiEjA3+rOTNoNzXFQ==", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4" + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "8D3Kk7assWpi93DuicgucDqGoOsgEgLlZy8io0FUlSGG2b4wkRWkjXn4xFBX+BzxExjfcYvHhtcBpqkXhe2p0A==", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.4", - "Microsoft.EntityFrameworkCore.Relational": "10.0.4", - "Microsoft.Extensions.Caching.Memory": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyModel": "10.0.4", - "Microsoft.Extensions.Logging": "10.0.4", + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", "SQLitePCLRaw.core": "2.1.11" } }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "uDRooaV6N3WZ0kdlNPMB68/MdGn/in1Fs7Db7DnIm85RBTPy4P321WO+daAImiYpH5dekjNggDqy1N44WaIlMA==", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Caching.Memory": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "CLLussNUMdSbyJOu4VBF7sqskHGB/5N1EcFzrqG/HsPATN8fCRUcfp0qns1VwkxKHwxrtYCh5FKe+kM81Q1PHA==", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "601B3ha6XvOsOcu9GVd2dVd1KEDuqr49r46GUWhNJkeZDhZ/NI9EYTyoeQjZQEi8ZUvnrv++FbTfGmC8F0vgtg==", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "ilnL/kQn62Gx3OZCVT7SJrBNi0CRIhS8VEunmE6i/a9lp9l/eos+hpxMvCW4iX2aVc/NWeDhxuQusZL7zvmKIg==", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LD4T4s2uW2kZUkwGc4A9KK5o3wfkgySHKEiYqV0NXeNdeLN563NgNqDpi3DNXAdrt2TwU0rK7QMPdWLLIaMipA==", + "resolved": "10.0.5", + "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileProviders.Physical": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "NkvJ8aSr3AG30yabjv7ZWwTG/wq5OElNTlNq39Ok2HSEF3TIwAc1f1xnTJlR/GuoJmEgkfT7WBO9YbSXRk41+g==", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "SIe9zlVQJecnk/DTmevIcl6+aEDYhoVLc2eG2AKwVeNEC8CSyxHAbh4lf0xtHq9JUum0vVTEByGNTK+b6oihTQ==" + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "LiJXylfk8pk+2zsUsITkou3QTFMJ8RNJ0oKKY0Oyjt6HJctGJwPw//ZgoNO4J29zKaT+dR4/PI2jW/znRcspLg==" + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "R9W7AttMedwOwJ7wRqTGBoVbX2JmlyWA+LJQUhizmS7Be9f6EJUn/+lvaIYDrOYtA1UzAfrwU871hpvZSPyIkg==", + "resolved": "10.0.5", + "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.4", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "JH2RyevIwJ1E9mBsZRXR+12TnUauptKgzCdOghhk3sE+dqcxB16GoE7x+0IuqTbaixM1ESXTNoqEw/IBnhM7LQ==", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "3hLXFZ1E/Kj3obIcb9iMCC95MpW2e8EkWxpXKgUfgGBfm+yn507pHAjPaHoi2U3GlSHIm/21DPCDLumwlMowjw==", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "gVVHdOFwlnXmTtx41e2aGfcFXX+8+9DPkOzEqQuHN8rOv+6RQWs/wfeQLaosOt3CQLKNoCaFmHopTtGB9PB5fg==", + "resolved": "10.0.5", + "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4", - "Microsoft.Extensions.FileSystemGlobbing": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "eCEFVuuZL++SqMcdB5i4KA16GvcxCzdKKK+clapXYyGMkhd4BxwZi2/vGzo8s7a8Vi0BA78p5u/NScgOP1pzTg==" + "resolved": "10.0.5", + "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" }, "Microsoft.Extensions.Logging": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "S8+6fCuMOhJZGk8sGFtOy3VsF9mk9x4UOL59GM91REiA/fmCDjunKKIw4RmStG87qyXPfxelDJf2pXIbTuaBdw==", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "kRxa2Zjzhg/ohh7EklpqQpBIcyQnC3meWxCcpZBn+0QWy/fY1DmDd45JiW8Vyrpj2J1RDtau5yRHiLZS/AoxUw==", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "amQUITwSnkbMPxh/ngneNykz4UtytEOXo0M/pbwdBiQU57EAVvBV5PFI8r/dRastUj0yxHVwrH64N9ACR5SdGQ==", + "resolved": "10.0.5", + "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.Binder": "10.0.4", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4", - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "10.0.4", - "contentHash": "lABYqiRH9HgYJsjzO3W7+cucUwWXhEkiyrRylANdIubnzcESlkIsLowXpQ4E+sc7kjMLbk1hk5oxw4qTKowTEg==" + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", @@ -365,8 +365,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "10.0.0", - "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "10.0.0" } @@ -455,8 +455,8 @@ "type": "Project", "dependencies": { "Google.Protobuf": "[3.34.0, )", - "Microsoft.AspNetCore.Authorization": "[10.0.4, )", - "Microsoft.Extensions.Configuration.Json": "[10.0.4, )", + "Microsoft.AspNetCore.Authorization": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Json": "[10.0.5, )", "Microsoft.IdentityModel.Tokens": "[8.16.0, )", "Werkr.Common.Configuration": "[1.0.0, )" } @@ -472,21 +472,21 @@ }, "Microsoft.AspNetCore.Authorization": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "uCg18hZTfzMEft8uxgPTM4s0sXsETfTnAJ00yR0LD6/ABz6NeEq1RMPIOpkTqbipatw+eNWKqDOV4Gus5OGjAQ==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "NbFi4wN6fUvZK4AKmixpfx0IvqtVimKEn8ZX28LkzZBVo09YnLbyRrJ1001IVQDLbV+aYpS/cLhVJu5JD0rY5A==", "dependencies": { - "Microsoft.AspNetCore.Metadata": "10.0.4", - "Microsoft.Extensions.Diagnostics": "10.0.4", - "Microsoft.Extensions.Logging.Abstractions": "10.0.4", - "Microsoft.Extensions.Options": "10.0.4" + "Microsoft.AspNetCore.Metadata": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Data.Sqlite.Core": { "type": "CentralTransitive", "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "UkmpN2pDkrtVLh+ypRDCbBij9mhPqOPzvHI625rf+VeT3FHnBwBjAY7XgjK8rGDI74fDx7C1SSIf2OAaAX4g2A==", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", "dependencies": { "SQLitePCLRaw.core": "2.1.11" } @@ -494,31 +494,31 @@ "Microsoft.Extensions.Configuration.Abstractions": { "type": "CentralTransitive", "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "3x9X9SMAMdAoEwWxHfsT2a9dTBqEtfYfbEOFw+UPtBshEH2gHWJeazxrZ1FK1O18MoCbe1NxINg5qciB01pEcg==", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.4" + "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Json": { "type": "CentralTransitive", - "requested": "[10.0.4, )", - "resolved": "10.0.4", - "contentHash": "gn2Rf0dvIa6Sz/WJ5cNHhG/oUOT1yrHXd7Q0vCpXDlLsMuRqv9G5NBXFJbSh/ZRzSbvbOQWMV0amQS/3N0Fzzg==", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.4", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.4", - "Microsoft.Extensions.Configuration.FileExtensions": "10.0.4", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.4" + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "CentralTransitive", "requested": "[10.0.3, )", - "resolved": "10.0.4", - "contentHash": "PDMMt7fvBatv6hcxxyJtXIzSwn7Dy00W6I2vDAOTYrQqNM2dF5A2L9n0uMzdPz2IPoNZWkAmYjoOCEdDLq0i4w==", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.IdentityModel.Tokens": { diff --git a/src/Werkr.Server/Components/App.razor b/src/Werkr.Server/Components/App.razor index c05fcbd..c1a7176 100644 --- a/src/Werkr.Server/Components/App.razor +++ b/src/Werkr.Server/Components/App.razor @@ -6,6 +6,7 @@ + diff --git a/src/Werkr.Server/Components/Layout/MainLayout.razor.css b/src/Werkr.Server/Components/Layout/MainLayout.razor.css index 88473a7..437198d 100644 --- a/src/Werkr.Server/Components/Layout/MainLayout.razor.css +++ b/src/Werkr.Server/Components/Layout/MainLayout.razor.css @@ -6,6 +6,7 @@ main { flex: 1; + min-width: 0; } .sidebar { diff --git a/src/Werkr.Server/Components/Layout/NavMenu.razor b/src/Werkr.Server/Components/Layout/NavMenu.razor index 1395a6f..0274fb7 100644 --- a/src/Werkr.Server/Components/Layout/NavMenu.razor +++ b/src/Werkr.Server/Components/Layout/NavMenu.razor @@ -12,15 +12,15 @@ + } + } else if ( !_isLoading ) { } @@ -76,16 +226,25 @@ OnConfirmed="DeleteConfirmedAsync" /> @code { - private List? _workflows; + private WorkflowDashboardPageDto? _dashboard; + private FilterCriteria? _activeCriteria; private string? _errorMessage; private bool _isLoading; private bool _isRunning; + private string _viewMode = "cards"; + private int _page = 1; private ConfirmDialog _confirmDialog = default!; private long _pendingDeleteId; private string _deleteMessage = ""; + private static readonly FilterDefinition _filterDefinition = new( [ + new FilterField( "search", "Name", FilterFieldType.Text ), + new FilterField( "tag", "Tag", FilterFieldType.Text ), + new FilterField( "enabled", "Enabled", FilterFieldType.Dropdown, ["Yes", "No"] ), + new FilterField( "status", "Last Run Status", FilterFieldType.Dropdown, ["Running", "Completed", "Failed", "Cancelled"] ), + ] ); + private readonly List _breadcrumbs = [ - new( "Task List", "/" ), new( "Workflows" ) ]; @@ -98,7 +257,8 @@ _errorMessage = null; try { HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); - _workflows = await client.GetFromJsonAsync>( "/api/workflows" ); + string url = BuildDashboardUrl( ); + _dashboard = await client.GetFromJsonAsync( url ); } catch ( Exception ex ) { _errorMessage = $"Failed to load workflows: {ex.Message}"; } finally { @@ -106,9 +266,31 @@ } } - private void PromptDelete( WorkflowDto wf ) { - _pendingDeleteId = wf.Id; - _deleteMessage = $"Are you sure you want to delete workflow '{wf.Name}'? All steps and runs will be lost."; + private string BuildDashboardUrl( ) { + List queryParts = [$"page={_page}", "pageSize=25"]; + if ( _activeCriteria is not null ) { + string? search = _activeCriteria.Get( "search" ); + if ( !string.IsNullOrWhiteSpace( search ) ) + queryParts.Add( $"search={Uri.EscapeDataString( search )}" ); + + string? tagFilter = _activeCriteria.Get( "tag" ); + if ( !string.IsNullOrWhiteSpace( tagFilter ) ) + queryParts.Add( $"tag={Uri.EscapeDataString( tagFilter )}" ); + + string? enabledFilter = _activeCriteria.Get( "enabled" ); + if ( !string.IsNullOrWhiteSpace( enabledFilter ) ) + queryParts.Add( $"enabled={( enabledFilter == "Yes" ? "true" : "false" )}" ); + + string? statusFilter = _activeCriteria.Get( "status" ); + if ( !string.IsNullOrWhiteSpace( statusFilter ) ) + queryParts.Add( $"status={Uri.EscapeDataString( statusFilter )}" ); + } + return "/api/workflows/dashboard?" + string.Join( "&", queryParts ); + } + + private void PromptDelete( long id, string name ) { + _pendingDeleteId = id; + _deleteMessage = $"Are you sure you want to delete workflow '{name}'? All steps and runs will be lost."; _confirmDialog.Show( ); } @@ -136,6 +318,8 @@ if ( !response.IsSuccessStatusCode ) { string body = await response.Content.ReadAsStringAsync( ); _errorMessage = $"Run failed: {body}"; + } else { + await LoadAsync( ); } } catch ( Exception ex ) { _errorMessage = $"Run failed: {ex.Message}"; @@ -143,4 +327,37 @@ _isRunning = false; } } + + private async Task GoToPage( int pageNum ) { + _page = pageNum; + await LoadAsync( ); + } + + private async Task OnFilterAppliedAsync( FilterCriteria criteria ) { + _activeCriteria = criteria; + _page = 1; + await LoadAsync( ); + } + + private async Task OnFilterClearedAsync( ) { + _activeCriteria = null; + _page = 1; + await LoadAsync( ); + } + + private static IReadOnlyList MapSparkline( + IReadOnlyList runs ) => + runs.Select( r => new RunSparkline.RunSummary( + RunId: r.RunId, + Status: r.Status, + DurationSeconds: r.DurationSeconds ) ).ToList( ); + + private static string FormatRelativeTime( DateTime utcTime ) { + TimeSpan span = DateTime.UtcNow - utcTime; + if ( span.TotalMinutes < 1 ) return "just now"; + if ( span.TotalMinutes < 60 ) return $"{(int) span.TotalMinutes}m ago"; + if ( span.TotalHours < 24 ) return $"{(int) span.TotalHours}h ago"; + if ( span.TotalDays < 30 ) return $"{(int) span.TotalDays}d ago"; + return utcTime.ToString( "d" ); + } } diff --git a/src/Werkr.Server/Components/Pages/Workflows/RunDetail.razor b/src/Werkr.Server/Components/Pages/Workflows/RunDetail.razor index 1076423..c6db01b 100644 --- a/src/Werkr.Server/Components/Pages/Workflows/RunDetail.razor +++ b/src/Werkr.Server/Components/Pages/Workflows/RunDetail.razor @@ -1,14 +1,21 @@ @page "/workflows/runs/{RunId:guid}" +@using Microsoft.AspNetCore.SignalR.Client +@using Werkr.Common.Models +@using Werkr.Server.Hubs @using Werkr.Server.Services @rendermode InteractiveServer @attribute [Authorize] @implements IAsyncDisposable @inject IHttpClientFactory HttpClientFactory @inject ServerConfigCache ConfigCache +@inject NavigationManager Navigation Werkr - Workflow Run Detail -

Workflow Run Detail

+
+

Workflow Run Detail

+ +
@if ( !string.IsNullOrWhiteSpace( _errorMessage ) ) { @@ -18,6 +25,7 @@ @if ( _isLoading ) { } else if ( _detail is not null ) { + @* ── Run Summary Card ── *@
Run Summary
@@ -30,7 +38,7 @@
Status
- @_detail.Status + @_detail.Status
Started
@@ -39,90 +47,277 @@
Ended
@( _detail.EndTime?.ToString( "G" ) ?? "Still running" )
+ + @if ( _detail.Status == "Failed" ) { + StepExecutionDto? failedStep = _detail.StepExecutions + .FirstOrDefault( e => e.Status == "Failed" ); + if ( failedStep is not null ) { + + @if ( !string.IsNullOrWhiteSpace( _retryMessage ) ) { + @_retryMessage + } + } + }
-
-
-
Jobs (@_detail.Jobs.Count)
- @if ( _detail.Jobs.Count > 0 ) { -
- - - - - - - - - - - - - @foreach ( JobDto j in _detail.Jobs ) { + @* ── DAG with Execution Overlay ── *@ + @if ( _workflow is not null ) { +
+
+
DAG
+ +
+
+ } + + @* ── Tabbed Content Area ── *@ +
+ @* Left panel: tabs *@ +
+ + + @switch ( _activeTab ) { + case "compact": + @* ── Compact: Step Execution Table ── *@ +
+
Job IDTask IDStartedRuntimeResultExit Code
+ - - - - - - + + + + + + - } - -
@j.Id.ToString()[..8]…@j.TaskId@j.StartTime.ToString( "g" )@j.RuntimeSeconds.ToString( "F1" )s - - @( j.Success ? "Success" : "Failed" ) - - @( j.ExitCode?.ToString( ) ?? "—" )StepStatusStartedDurationJobExit Code
-
- } else { -

No jobs in this run.

+ + + @foreach ( StepExecutionDto se in _detail.StepExecutions.OrderBy( e => e.StartTime ?? DateTime.MaxValue ) ) { + string stepName = GetStepName( se.StepId ); + double? duration = se is { StartTime: not null, EndTime: not null } + ? ( se.EndTime.Value - se.StartTime.Value ).TotalSeconds + : null; + JobDto? job = se.JobId.HasValue + ? _detail.Jobs.FirstOrDefault( j => j.Id == se.JobId.Value ) + : null; + + + @stepName + + @se.Status + + @( se.StartTime?.ToString( "T" ) ?? "—" ) + @( duration?.ToString( "F1" ) ?? "—" )@( duration.HasValue ? "s" : "" ) + + @if ( job is not null ) { + @job.Id.ToString( )[..8]… + } else { + + } + + @( job?.ExitCode?.ToString( ) ?? "—" ) + + } + @if ( _detail.StepExecutions.Count == 0 ) { + No step executions recorded. + } + + +
+ break; + + case "timeline": + @* ── Timeline: Event Groups ── *@ + + break; + + case "gantt": + @* ── Gantt: vis-timeline Bars ── *@ + + break; + + case "log": + @* ── Log: Full Log Viewer ── *@ + + break; }
+ + @* Right panel: Step Detail *@ + @if ( _selectedStep is not null ) { +
+ +
+ } - ← Back to Runs + ← Back to Runs } @code { [Parameter] public Guid RunId { get; set; } private WorkflowRunDetailDto? _detail; + private WorkflowDto? _workflow; private string? _errorMessage; private bool _isLoading; private bool _disposed; + private string _activeTab = "compact"; + private StepExecutionDto? _selectedStep; + private GanttTab? _ganttTab; + + // Retry + private bool _retrying; + private string? _retryMessage; + private bool _retrySuccess; + + // SignalR + private HubConnection? _hub; + private string _connectionState = "Polling"; + + // Polling fallback private PeriodicTimer? _refreshTimer; private CancellationTokenSource? _cts; + // Real-time data + private Dictionary _stepStatuses = new( ); + private List _logLines = new( ); + private readonly List _breadcrumbs = [ - new( "Task List", "/" ), new( "Workflows", "/workflows" ), new( "Run Detail" ) ]; protected override async Task OnInitializedAsync( ) { await LoadAsync( ); + BuildStepStatuses( ); _cts = new CancellationTokenSource( ); + await StartSignalRAsync( ); + + // Start polling as fallback — disabled when hub connects _refreshTimer = new PeriodicTimer( TimeSpan.FromSeconds( ConfigCache.RunDetailPollingIntervalSeconds ) ); _ = AutoRefreshAsync( _cts.Token ); } + // ── SignalR Lifecycle ── + + private async Task StartSignalRAsync( ) { + try { + string hubUrl = Navigation.ToAbsoluteUri( "/hubs/workflow-run" ).AbsoluteUri; + _hub = new HubConnectionBuilder( ) + .WithUrl( hubUrl ) + .WithAutomaticReconnect( ) + .Build( ); + + _hub.Reconnecting += _ => { _connectionState = "Reconnecting"; return InvokeAsync( StateHasChanged ); }; + _hub.Reconnected += _ => { _connectionState = "Live"; return InvokeAsync( StateHasChanged ); }; + _hub.Closed += _ => { _connectionState = "Polling"; return InvokeAsync( StateHasChanged ); }; + + _hub.On( "StepStatusChanged", OnStepStatusChanged ); + _hub.On( "RunStatusChanged", OnRunStatusChanged ); + _hub.On( "LogAppended", OnLogAppended ); + + await _hub.StartAsync( ); + await _hub.InvokeAsync( "JoinRun", RunId ); + _connectionState = "Live"; + } catch { + _connectionState = "Polling"; + } + } + + private Task OnStepStatusChanged( StepStatusDto dto ) => InvokeAsync( async ( ) => { + if ( Enum.TryParse( dto.Status, out StepExecutionStatus status ) ) { + _stepStatuses[dto.StepId] = status; + } + + // Forward to Gantt tab for real-time bar updates + if ( _ganttTab is not null ) { + await _ganttTab.HandleStepStatusChangedAsync( dto ); + } + + // Update step execution in the detail DTO list + if ( _detail is not null ) { + List updated = _detail.StepExecutions.ToList( ); + int idx = updated.FindIndex( e => e.StepId == dto.StepId ); + StepExecutionDto newEntry = new( + idx >= 0 ? updated[idx].Id : 0, + dto.RunId, dto.StepId, idx >= 0 ? updated[idx].Attempt : 1, + dto.Status, dto.Timestamp, + dto.Status is "Completed" or "Failed" ? dto.Timestamp : null, + dto.JobId, dto.ErrorMessage, null ); + + if ( idx >= 0 ) updated[idx] = newEntry; + else updated.Add( newEntry ); + + _detail = _detail with { StepExecutions = updated }; + } + + StateHasChanged( ); + } ); + + private Task OnRunStatusChanged( RunStatusDto dto ) => InvokeAsync( ( ) => { + if ( _detail is not null ) { + _detail = _detail with { Status = dto.Status, EndTime = dto.Timestamp }; + } + StateHasChanged( ); + } ); + + private Task OnLogAppended( LogLineDto dto ) => InvokeAsync( ( ) => { + _logLines.Add( $"[{dto.Timestamp:HH:mm:ss}] Step {dto.StepId}: {dto.Line}" ); + StateHasChanged( ); + } ); + + // ── Polling Fallback ── + private async Task AutoRefreshAsync( CancellationToken ct ) { if ( _refreshTimer is null ) return; try { while ( await _refreshTimer.WaitForNextTickAsync( ct ) ) { if ( _disposed ) return; + if ( _connectionState == "Live" ) continue; // Skip polling when SignalR is active await LoadAsync( ); + BuildStepStatuses( ); await InvokeAsync( StateHasChanged ); } } catch ( OperationCanceledException ) { } catch ( ObjectDisposedException ) { } } + // ── Data Loading ── + private async Task LoadAsync( ) { - _isLoading = true; + _isLoading = _detail is null; _errorMessage = null; try { HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); @@ -130,6 +325,13 @@ $"/api/workflows/runs/{RunId}" ); if ( _detail is null ) { _errorMessage = "Run not found."; + return; + } + + // Load workflow definition for DAG rendering (only once) + if ( _workflow is null ) { + _workflow = await client.GetFromJsonAsync( + $"/api/workflows/{_detail.WorkflowId}" ); } } catch ( Exception ex ) { _errorMessage = $"Failed to load run: {ex.Message}"; @@ -138,13 +340,76 @@ } } - private static string RunStatusBadge( string status ) => status switch { - "Completed" => "bg-success", - "Running" => "bg-primary", - "Failed" => "bg-danger", - "Cancelled" => "bg-warning text-dark", - _ => "bg-secondary" - }; + private void BuildStepStatuses( ) { + if ( _detail is null ) return; + _stepStatuses.Clear( ); + foreach ( StepExecutionDto se in _detail.StepExecutions ) { + if ( Enum.TryParse( se.Status, out StepExecutionStatus status ) ) { + _stepStatuses[se.StepId] = status; + } + } + } + + // ── UI Helpers ── + + private string GetStepName( long stepId ) { + WorkflowStepDto? step = _workflow?.Steps.FirstOrDefault( s => s.Id == stepId ); + return step is not null ? $"Step #{step.Order} (Task {step.TaskId})" : $"Step {stepId}"; + } + + private void SelectStep( StepExecutionDto step ) => _selectedStep = step; + + private void SelectStepById( long stepId ) { + _selectedStep = _detail?.StepExecutions + .Where( e => e.StepId == stepId ) + .OrderByDescending( e => e.Attempt ) + .FirstOrDefault( ); + } + + private void OnDagStepSelected( WorkflowStepDto step ) => SelectStepById( step.Id ); + + private void ClearSelectedStep( ) => _selectedStep = null; + + private JobDto? GetJobForStep( StepExecutionDto step ) => + step.JobId.HasValue ? _detail?.Jobs.FirstOrDefault( j => j.Id == step.JobId.Value ) : null; + + private IReadOnlyList GetFilteredLogLines( ) { + if ( _selectedStep is null ) return _logLines; + string prefix = $"Step {_selectedStep.StepId}:"; + return _logLines.Where( l => l.Contains( prefix ) ).ToList( ); + } + + // ── Retry ── + + private async Task RetryFromFailedAsync( long stepId ) { + if ( _detail is null ) return; + _retrying = true; + _retryMessage = null; + try { + HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); + HttpResponseMessage response = await client.PostAsJsonAsync( + $"/api/workflows/{_detail.WorkflowId}/runs/{RunId}/retry-from/{stepId}", + new RetryFromFailedRequest( null ) ); + + if ( response.IsSuccessStatusCode ) { + _retrySuccess = true; + _retryMessage = "Retry scheduled. Run will resume shortly."; + await LoadAsync( ); + BuildStepStatuses( ); + } else { + _retrySuccess = false; + string body = await response.Content.ReadAsStringAsync( ); + _retryMessage = $"Retry failed: {body}"; + } + } catch ( Exception ex ) { + _retrySuccess = false; + _retryMessage = $"Retry error: {ex.Message}"; + } finally { + _retrying = false; + } + } + + // ── Disposal ── public async ValueTask DisposeAsync( ) { if ( _disposed ) return; @@ -154,5 +419,9 @@ _cts.Dispose( ); } _refreshTimer?.Dispose( ); + if ( _hub is not null ) { + try { await _hub.DisposeAsync( ); } + catch { /* best effort */ } + } } } diff --git a/src/Werkr.Server/Components/Pages/Workflows/Runs.razor b/src/Werkr.Server/Components/Pages/Workflows/Runs.razor index 9a19382..a726476 100644 --- a/src/Werkr.Server/Components/Pages/Workflows/Runs.razor +++ b/src/Werkr.Server/Components/Pages/Workflows/Runs.razor @@ -1,4 +1,5 @@ @page "/workflows/{WorkflowId:long}/runs" +@using Werkr.Common.Models @using Werkr.Server.Services @rendermode InteractiveServer @attribute [Authorize] @@ -23,13 +24,18 @@ + + @if ( !string.IsNullOrWhiteSpace( _errorMessage ) ) { } @if ( _isLoading && _runs is null ) { -} else if ( _runs is not null && _runs.Count > 0 ) { +} else if ( _runs is not null && FilteredRuns.Count > 0 ) {
@@ -42,13 +48,13 @@ - @foreach ( WorkflowRunDto run in _runs ) { + @foreach ( WorkflowRunDto run in FilteredRuns ) { @foreach ( RunGridEntry run in Runs ) { string status = GetCellStatus( run, step.Id ); - @foreach ( RunGridEntry run in Runs ) { string status = GetCellStatus( run, step.Id ); - } @@ -62,12 +63,6 @@ private static string GetCellStatus( RunGridEntry run, long stepId ) => run.StepStatuses.TryGetValue( stepId, out string? s ) ? s : "Pending"; - private async Task HandleCellKeyDown( KeyboardEventArgs e, Guid runId, long stepId ) { - if (e.Key is "Enter" or " ") { - await OnCellSelected.InvokeAsync( (runId, stepId) ); - } - } - private static string GetStatusIcon( string status ) => status switch { "Completed" => "✓", "Failed" => "✗", diff --git a/src/Werkr.Server/Components/Shared/RunSparkline.razor b/src/Werkr.Server/Components/Shared/RunSparkline.razor index 9aca9b9..5d700a5 100644 --- a/src/Werkr.Server/Components/Shared/RunSparkline.razor +++ b/src/Werkr.Server/Components/Shared/RunSparkline.razor @@ -36,6 +36,7 @@ "Completed" => "var(--werkr-success)", "Failed" => "var(--werkr-failed)", "Running" => "var(--werkr-running)", + "Skipped" => "var(--werkr-skipped)", _ => "var(--werkr-pending)", }; } From ac25c2302976344fc4cb0f51f99873f2ff65e78e Mon Sep 17 00:00:00 2001 From: Taylor Marvin Date: Sun, 15 Mar 2026 01:05:00 -0700 Subject: [PATCH 09/11] Address PR concerns x5 --- .../Components/Pages/Workflows/Runs.razor | 6 ++++-- src/Werkr.Server/Components/Shared/DagCanvas.razor | 12 +++++------- .../Components/Shared/RunGridView.razor.css | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Werkr.Server/Components/Pages/Workflows/Runs.razor b/src/Werkr.Server/Components/Pages/Workflows/Runs.razor index 0fbb219..adee4e3 100644 --- a/src/Werkr.Server/Components/Pages/Workflows/Runs.razor +++ b/src/Werkr.Server/Components/Pages/Workflows/Runs.razor @@ -33,9 +33,11 @@ } +@{ var filteredRuns = FilteredRuns; } + @if ( _isLoading && _runs is null ) { -} else if ( _runs is not null && FilteredRuns.Count > 0 ) { +} else if ( _runs is not null && filteredRuns.Count > 0 ) {
@run.Id.ToString()[..8]… @run.StartTime.ToString( "g" ) @( run.EndTime?.ToString( "g" ) ?? "—" ) - @run.Status + @run.Status Details @@ -66,6 +72,7 @@ [Parameter] public long WorkflowId { get; set; } private List? _runs; + private FilterCriteria? _activeCriteria; private string? _errorMessage; private bool _isLoading; private bool _disposed; @@ -74,9 +81,24 @@ private List _breadcrumbs = []; + private static readonly FilterDefinition _filterDefinition = new( [ + new FilterField( "status", "Status", FilterFieldType.Dropdown, ["Running", "Completed", "Failed", "Skipped"] ), + new FilterField( "since", "Since", FilterFieldType.Date ), + new FilterField( "until", "Until", FilterFieldType.Date ), + ] ); + + private List FilteredRuns => ( _runs ?? [] ) + .Where( r => + ( string.IsNullOrWhiteSpace( _activeCriteria?.Get( "status" ) ) + || r.Status.Equals( _activeCriteria.Get( "status" ), StringComparison.OrdinalIgnoreCase ) ) + && ( !DateTime.TryParse( _activeCriteria?.Get( "since" ), out DateTime since ) + || r.StartTime >= since ) + && ( !DateTime.TryParse( _activeCriteria?.Get( "until" ), out DateTime until ) + || r.StartTime <= until.AddDays( 1 ) ) ) + .ToList( ); + protected override async Task OnInitializedAsync( ) { _breadcrumbs = [ - new( "Task List", "/" ), new( "Workflows", "/workflows" ), new( $"Workflow {WorkflowId}", $"/workflows/{WorkflowId}" ), new( "Runs" ) @@ -114,14 +136,6 @@ } } - private static string RunStatusBadge( string status ) => status switch { - "Completed" => "bg-success", - "Running" => "bg-primary", - "Failed" => "bg-danger", - "Cancelled" => "bg-warning text-dark", - _ => "bg-secondary" - }; - public ValueTask DisposeAsync( ) { _disposed = true; _cts?.Cancel( ); @@ -129,4 +143,14 @@ _refreshTimer?.Dispose( ); return ValueTask.CompletedTask; } + + private async Task OnFilterAppliedAsync( FilterCriteria criteria ) { + _activeCriteria = criteria; + await LoadAsync( ); + } + + private async Task OnFilterClearedAsync( ) { + _activeCriteria = null; + await LoadAsync( ); + } } diff --git a/src/Werkr.Server/Components/Shared/ConnectionStatus.razor b/src/Werkr.Server/Components/Shared/ConnectionStatus.razor new file mode 100644 index 0000000..e185e7f --- /dev/null +++ b/src/Werkr.Server/Components/Shared/ConnectionStatus.razor @@ -0,0 +1,29 @@ +@* Connection status indicator for SignalR real-time updates. *@ + + + @GetLabel( ) + + +@code { + /// Current connection state: Live, Reconnecting, or Polling. + [Parameter] + public string State { get; set; } = "Polling"; + + private string GetStatusClass( ) => State switch { + "Live" => "bg-success", + "Reconnecting" => "bg-warning text-dark", + _ => "bg-secondary", + }; + + private string GetLabel( ) => State switch { + "Live" => "Live", + "Reconnecting" => "Reconnecting…", + _ => "Polling", + }; + + private string GetTooltip( ) => State switch { + "Live" => "Connected via SignalR — receiving real-time updates.", + "Reconnecting" => "SignalR connection lost. Attempting to reconnect…", + _ => "Falling back to periodic polling.", + }; +} diff --git a/src/Werkr.Server/Components/Shared/DagCanvas.razor b/src/Werkr.Server/Components/Shared/DagCanvas.razor new file mode 100644 index 0000000..31f3d68 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/DagCanvas.razor @@ -0,0 +1,220 @@ +@rendermode InteractiveServer +@using Werkr.Common.Models +@using Werkr.Server.Helpers +@implements IAsyncDisposable +@inject IJSRuntime JS +@inject ILogger Logger + +
+ @* ── Toolbar ── *@ +
+
+ + +
+ +
+ + + +
+ + @( _zoomLevel.ToString( "P0" ) ) + +
+ + @* ── Export ── *@ +
+ + +
+
+ + @* ── Canvas ── *@ +
+ + @* ── Minimap ── *@ +
+
+ +@code { + [Parameter] public long WorkflowId { get; set; } + [Parameter] public IReadOnlyList? Steps { get; set; } + [Parameter] public EventCallback OnStepSelected { get; set; } + [Parameter] public IReadOnlyDictionary? StepStatuses { get; set; } + [Parameter] public List? Annotations { get; set; } + + private static readonly System.Text.Json.JsonSerializerOptions s_jsonWeb = new( System.Text.Json.JsonSerializerDefaults.Web ); + + private readonly string _containerId = $"dag-{Guid.NewGuid( ):N}"; + private readonly string _minimapId = $"dag-mini-{Guid.NewGuid( ):N}"; + private DagJsInterop? _interop; + private bool _initialized; + private string _direction = "LR"; + private double _zoomLevel = 1.0; + private IReadOnlyList? _previousSteps; + private IReadOnlyDictionary? _previousStatuses; + + protected override async Task OnAfterRenderAsync( bool firstRender ) { + if ( !firstRender || _initialized ) return; + _initialized = true; + + try { + _interop = new DagJsInterop( JS ); + _interop.OnNodeClicked += OnNodeClickedAsync; + _interop.OnZoomChanged += OnZoomChangedAsync; + await _interop.InitAsync( _containerId, _minimapId ); + + if ( Steps is not null && Steps.Count > 0 ) { + await LoadGraphDataAsync( ); + } + } catch ( Exception ex ) { + Logger.LogError( ex, "Failed to initialize DAG canvas." ); + } + } + + protected override async Task OnParametersSetAsync( ) { + if ( !_initialized || _interop is null ) return; + + // If the steps reference changed, reload the entire graph + if ( !ReferenceEquals( Steps, _previousSteps ) && Steps is not null ) { + _previousSteps = Steps; + try { + await LoadGraphDataAsync( ); + } catch ( Exception ex ) { + Logger.LogError( ex, "Failed to reload DAG graph." ); + } + } + + // If only statuses changed, update the overlay + if ( !ReferenceEquals( StepStatuses, _previousStatuses ) ) { + _previousStatuses = StepStatuses; + try { + await ApplyStatusesAsync( ); + } catch ( Exception ex ) { + Logger.LogError( ex, "Failed to update DAG status overlay." ); + } + } + } + + private async Task LoadGraphDataAsync( ) { + if ( _interop is null || Steps is null ) return; + + DagNodeDto[] nodes = Steps.Select( step => new DagNodeDto( + StepId: step.Id, + StepLabel: $"Step #{step.Order}", + TaskName: step.TaskName ?? $"Task {step.TaskId}", + ControlStatement: step.ControlStatement, + Order: step.Order + ) ).ToArray( ); + + DagEdgeDto[] edges = Steps + .SelectMany( step => step.Dependencies.Select( dep => new DagEdgeDto( + SourceStepId: dep.DependsOnStepId, + TargetStepId: step.Id + ) ) ) + .ToArray( ); + + await _interop.LoadGraphAsync( nodes, edges ); + + if ( Annotations is { Count: > 0 } ) { + string annotationsJson = System.Text.Json.JsonSerializer.Serialize( Annotations, s_jsonWeb ); + await _interop.LoadAnnotationsAsync( annotationsJson ); + } + + if ( StepStatuses is not null && StepStatuses.Count > 0 ) { + await ApplyStatusesAsync( ); + } + } + + private async Task ApplyStatusesAsync( ) { + if ( _interop is null || StepStatuses is null || StepStatuses.Count == 0 ) { + if ( _interop is not null ) { + await _interop.ClearStatusesAsync( ); + } + return; + } + + Dictionary statusMap = StepStatuses.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.ToString( ) + ); + await _interop.ApplyAllStatusesAsync( statusMap ); + } + + private async Task OnNodeClickedAsync( long stepId ) { + WorkflowStepDto? step = Steps?.FirstOrDefault( s => s.Id == stepId ); + if ( step is not null ) { + await OnStepSelected.InvokeAsync( step ); + } + } + + private Task OnZoomChangedAsync( double zoom ) { + _zoomLevel = zoom; + InvokeAsync( StateHasChanged ); + return Task.CompletedTask; + } + + private async Task SetDirectionAsync( string direction ) { + _direction = direction; + if ( _interop is not null ) { + await _interop.SetLayoutDirectionAsync( direction ); + } + } + + private async Task ZoomInAsync( ) { + if ( _interop is not null ) { + await _interop.ZoomToAsync( _zoomLevel * 1.2 ); + } + } + + private async Task ZoomOutAsync( ) { + if ( _interop is not null ) { + await _interop.ZoomToAsync( _zoomLevel / 1.2 ); + } + } + + private async Task ZoomFitAsync( ) { + if ( _interop is not null ) { + await _interop.ZoomToFitAsync( ); + } + } + + private async Task ExportSvgAsync( ) { + if ( _interop is not null ) { + await _interop.ExportSvgAsync( ); + } + } + + private async Task ExportPngAsync( ) { + if ( _interop is not null ) { + await _interop.ExportPngAsync( ); + } + } + + public async ValueTask DisposeAsync( ) { + if ( _interop is not null ) { + _interop.OnNodeClicked -= OnNodeClickedAsync; + _interop.OnZoomChanged -= OnZoomChangedAsync; + await _interop.DisposeAsync( ); + } + } +} diff --git a/src/Werkr.Server/Components/Shared/DagCanvas.razor.css b/src/Werkr.Server/Components/Shared/DagCanvas.razor.css new file mode 100644 index 0000000..ec01a26 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/DagCanvas.razor.css @@ -0,0 +1,45 @@ +.dag-canvas-container { + position: relative; +} + +.dag-canvas { + width: 100%; + min-height: 400px; + border: 1px solid var(--bs-border-color); + border-radius: 0.375rem; + background-color: var(--bs-body-bg); +} + +.dag-minimap { + position: absolute; + bottom: 8px; + right: 8px; + border: 1px solid var(--bs-border-color); + border-radius: 4px; + background: var(--bs-body-bg); + opacity: 0.9; +} + +.dag-toolbar { + padding: 4px 0; +} + +/* X6 foreignObject node styles (must use ::deep to pierce shadow) */ +::deep .werkr-node { + font-family: inherit; + transition: box-shadow 0.15s ease; + cursor: pointer; +} + +::deep .werkr-node:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +::deep .werkr-pulse { + animation: werkr-pulse 1.5s ease-in-out infinite; +} + +@keyframes werkr-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} diff --git a/src/Werkr.Server/Components/Shared/DagEditorCanvas.razor b/src/Werkr.Server/Components/Shared/DagEditorCanvas.razor new file mode 100644 index 0000000..b1da2b8 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/DagEditorCanvas.razor @@ -0,0 +1,99 @@ +@rendermode InteractiveServer +@using Werkr.Common.Models +@using Werkr.Server.Helpers +@implements IAsyncDisposable +@inject IJSRuntime JS +@inject ILogger Logger + +
+ @* ── Canvas ── *@ +
+ + @* ── Minimap ── *@ +
+
+ +@code { + [Parameter] public long WorkflowId { get; set; } + [Parameter] public string UserId { get; set; } = ""; + [Parameter] public EventCallback OnNodeSelected { get; set; } + [Parameter] public EventCallback OnNodeDeselected { get; set; } + [Parameter] public EventCallback<(string ActionType, double X, double Y)> OnNodeDropped { get; set; } + [Parameter] public EventCallback OnSaveRequested { get; set; } + [Parameter] public EventCallback<(string SourceId, string TargetId)> OnCycleDetected { get; set; } + [Parameter] public EventCallback OnDirtyCountChanged { get; set; } + [Parameter] public EventCallback OnZoomChanged { get; set; } + + private readonly string _containerId = $"dag-edit-{Guid.NewGuid( ):N}"; + private readonly string _minimapId = $"dag-edit-mini-{Guid.NewGuid( ):N}"; + private DagEditorJsInterop? _interop; + private bool _initialized; + + /// Gets the JS interop instance for parent components to call editor methods. + public DagEditorJsInterop? Interop => _interop; + + protected override async Task OnAfterRenderAsync( bool firstRender ) { + if (!firstRender || _initialized) return; + _initialized = true; + + try { + _interop = new DagEditorJsInterop( JS ); + _interop.OnNodeSelected += HandleNodeSelectedAsync; + _interop.OnNodeDeselected += HandleNodeDeselectedAsync; + _interop.OnNodeDropped += HandleNodeDroppedAsync; + _interop.OnSaveRequested += HandleSaveRequestedAsync; + _interop.OnCycleDetected += HandleCycleDetectedAsync; + _interop.OnGraphDirtyChanged += HandleDirtyChangedAsync; + _interop.OnZoomChanged += HandleZoomChangedAsync; + await _interop.InitEditorAsync( _containerId, _minimapId, UserId, WorkflowId ); + } catch (Exception ex) { + Logger.LogError( ex, "Failed to initialize DAG editor canvas." ); + } + } + + private async Task HandleNodeSelectedAsync( long stepId ) { + await OnNodeSelected.InvokeAsync( stepId ); + await InvokeAsync( StateHasChanged ); + } + + private async Task HandleNodeDeselectedAsync( ) { + await OnNodeDeselected.InvokeAsync( ); + await InvokeAsync( StateHasChanged ); + } + + private async Task HandleNodeDroppedAsync( string actionType, double x, double y ) { + await OnNodeDropped.InvokeAsync( (actionType, x, y) ); + await InvokeAsync( StateHasChanged ); + } + + private async Task HandleSaveRequestedAsync( ) { + await OnSaveRequested.InvokeAsync( ); + } + + private async Task HandleCycleDetectedAsync( string sourceId, string targetId ) { + await OnCycleDetected.InvokeAsync( (sourceId, targetId) ); + } + + private async Task HandleDirtyChangedAsync( int dirtyCount ) { + await OnDirtyCountChanged.InvokeAsync( dirtyCount ); + await InvokeAsync( StateHasChanged ); + } + + private Task HandleZoomChangedAsync( double zoom ) { + OnZoomChanged.InvokeAsync( zoom ); + return Task.CompletedTask; + } + + public async ValueTask DisposeAsync( ) { + if (_interop is not null) { + _interop.OnNodeSelected -= HandleNodeSelectedAsync; + _interop.OnNodeDeselected -= HandleNodeDeselectedAsync; + _interop.OnNodeDropped -= HandleNodeDroppedAsync; + _interop.OnSaveRequested -= HandleSaveRequestedAsync; + _interop.OnCycleDetected -= HandleCycleDetectedAsync; + _interop.OnGraphDirtyChanged -= HandleDirtyChangedAsync; + _interop.OnZoomChanged -= HandleZoomChangedAsync; + await _interop.DisposeAsync( ); + } + } +} diff --git a/src/Werkr.Server/Components/Shared/DagEditorCanvas.razor.css b/src/Werkr.Server/Components/Shared/DagEditorCanvas.razor.css new file mode 100644 index 0000000..62903eb --- /dev/null +++ b/src/Werkr.Server/Components/Shared/DagEditorCanvas.razor.css @@ -0,0 +1,21 @@ +.dag-editor-canvas-container { + position: relative; +} + +.dag-editor-canvas { + width: 100%; + min-height: 500px; + border: 1px solid var(--bs-border-color); + border-radius: 0.375rem; + background-color: var(--bs-body-bg); +} + +.dag-editor-minimap { + position: absolute; + bottom: 8px; + right: 8px; + border: 1px solid var(--bs-border-color); + border-radius: 4px; + background: var(--bs-body-bg); + opacity: 0.9; +} diff --git a/src/Werkr.Server/Components/Shared/DagView.razor b/src/Werkr.Server/Components/Shared/DagView.razor deleted file mode 100644 index 84cf908..0000000 --- a/src/Werkr.Server/Components/Shared/DagView.razor +++ /dev/null @@ -1,195 +0,0 @@ -@using System.Text -@inject IHttpClientFactory HttpClientFactory - -
- @if ( Steps is null || Steps.Count == 0 ) { - - } else if ( _nodes.Count == 0 ) { - - } else { - - - - - - - - @foreach ( DagEdge edge in _edges ) { - - } - - @foreach ( DagNode node in _nodes ) { - - - - @Truncate( node.Label, 16 ) - - - Order @node.Step.Order | @node.Step.ControlStatement - - - } - - } -
- -@code { - [Parameter] - public long WorkflowId { get; set; } - - [Parameter] - public IReadOnlyList? Steps { get; set; } - - [Parameter] - public EventCallback OnStepSelected { get; set; } - - private const int NodeWidth = 160; - private const int NodeHeight = 52; - private const int HorizontalGap = 40; - private const int VerticalGap = 60; - private const int PaddingX = 20; - private const int PaddingY = 20; - - private List _nodes = []; - private List _edges = []; - private int _svgWidth; - private int _svgHeight; - - protected override void OnParametersSet( ) { - if ( Steps is not null ) { - BuildLayout( Steps ); - } - } - - private void BuildLayout( IReadOnlyList steps ) { - if ( steps.Count == 0 ) { - _nodes = []; - _edges = []; - _svgWidth = 0; - _svgHeight = 0; - return; - } - - // Build adjacency and in-degree for topological layering - Dictionary stepMap = steps.ToDictionary( s => s.Id ); - Dictionary> dependsOn = []; - Dictionary> dependedBy = []; - - foreach ( WorkflowStepDto step in steps ) { - dependsOn[step.Id] = []; - if ( !dependedBy.ContainsKey( step.Id ) ) { - dependedBy[step.Id] = []; - } - foreach ( StepDependencyDto dep in step.Dependencies ) { - dependsOn[step.Id].Add( dep.DependsOnStepId ); - if ( !dependedBy.ContainsKey( dep.DependsOnStepId ) ) { - dependedBy[dep.DependsOnStepId] = []; - } - dependedBy[dep.DependsOnStepId].Add( step.Id ); - } - } - - // Kahn's algorithm for topological layers - Dictionary inDegree = []; - foreach ( WorkflowStepDto step in steps ) { - inDegree[step.Id] = dependsOn[step.Id].Count; - } - - List> layers = []; - HashSet placed = []; - - while ( placed.Count < steps.Count ) { - List layer = []; - foreach ( WorkflowStepDto step in steps ) { - if ( !placed.Contains( step.Id ) && inDegree[step.Id] == 0 ) { - layer.Add( step.Id ); - } - } - - if ( layer.Count == 0 ) { - // Cycle detected — place all remaining - layer = steps.Where( s => !placed.Contains( s.Id ) ) - .Select( s => s.Id ) - .ToList( ); - } - - // Sort layer by step order for consistent rendering - layer.Sort( ( a, b ) => stepMap[a].Order.CompareTo( stepMap[b].Order ) ); - - foreach ( long id in layer ) { - placed.Add( id ); - if ( dependedBy.TryGetValue( id, out HashSet? children ) ) { - foreach ( long child in children ) { - inDegree[child]--; - } - } - } - - layers.Add( layer ); - } - - // Position nodes in layers (top to bottom, each layer is a row) - Dictionary nodePositions = []; - _nodes = []; - int maxLayerWidth = 0; - - for ( int layerIdx = 0; layerIdx < layers.Count; layerIdx++ ) { - List layer = layers[layerIdx]; - maxLayerWidth = Math.Max( maxLayerWidth, layer.Count ); - - for ( int colIdx = 0; colIdx < layer.Count; colIdx++ ) { - long stepId = layer[colIdx]; - WorkflowStepDto step = stepMap[stepId]; - int x = PaddingX + colIdx * ( NodeWidth + HorizontalGap ); - int y = PaddingY + layerIdx * ( NodeHeight + VerticalGap ); - string label = $"#{step.Id} T{step.TaskId}"; - DagNode node = new( step, label, x, y ); - _nodes.Add( node ); - nodePositions[stepId] = node; - } - } - - // Build edges - _edges = []; - foreach ( WorkflowStepDto step in steps ) { - if ( !nodePositions.TryGetValue( step.Id, out DagNode? target ) ) continue; - foreach ( StepDependencyDto dep in step.Dependencies ) { - if ( !nodePositions.TryGetValue( dep.DependsOnStepId, out DagNode? source ) ) continue; - _edges.Add( new DagEdge( - source.X + NodeWidth / 2, - source.Y + NodeHeight, - target.X + NodeWidth / 2, - target.Y ) ); - } - } - - _svgWidth = PaddingX * 2 + maxLayerWidth * ( NodeWidth + HorizontalGap ) - HorizontalGap; - _svgHeight = PaddingY * 2 + layers.Count * ( NodeHeight + VerticalGap ) - VerticalGap; - - if ( _svgWidth < 200 ) _svgWidth = 200; - if ( _svgHeight < 100 ) _svgHeight = 100; - } - - private static string GetNodeFill( WorkflowStepDto step ) => step.ControlStatement.ToLowerInvariant( ) switch { - "default" => "#0d6efd", // Blue — default execution - "if" or "elseif" => "#6f42c1", // Purple — conditional branch - "else" => "#9b59b6", // Light purple — fallback branch - "while" or "do" => "#fd7e14", // Orange — loop - _ => "#495057" // Gray — unknown - }; - - private static string Truncate( string text, int max ) => - text.Length <= max ? text : text[..max] + "…"; - - private sealed record DagNode( WorkflowStepDto Step, string Label, int X, int Y ); - private sealed record DagEdge( int X1, int Y1, int X2, int Y2 ); -} diff --git a/src/Werkr.Server/Components/Shared/DagView.razor.css b/src/Werkr.Server/Components/Shared/DagView.razor.css deleted file mode 100644 index c471f3d..0000000 --- a/src/Werkr.Server/Components/Shared/DagView.razor.css +++ /dev/null @@ -1,22 +0,0 @@ -.dag-container { - overflow-x: auto; - overflow-y: auto; - max-height: 500px; - border: 1px solid var(--bs-border-color); - border-radius: 0.375rem; - padding: 0.5rem; - background: var(--bs-body-bg); -} - -.dag-svg { - display: block; -} - -::deep .dag-node { - cursor: pointer; -} - -::deep .dag-node:hover rect { - stroke-width: 2.5; - stroke: var(--bs-primary); -} diff --git a/src/Werkr.Server/Components/Shared/EditorToolbar.razor b/src/Werkr.Server/Components/Shared/EditorToolbar.razor new file mode 100644 index 0000000..16481a5 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/EditorToolbar.razor @@ -0,0 +1,152 @@ +@rendermode InteractiveServer + +
+ @* ── Save ── *@ + + +
+ + @* ── Undo / Redo ── *@ +
+ + +
+ +
+ + @* ── Validate ── *@ + + + @* ── Auto-Layout ── *@ + + +
+ + @* ── Layout Direction ── *@ +
+ + +
+ +
+ + @* ── Zoom ── *@ +
+ + + +
+ + @( ZoomLevel.ToString( "P0" ) ) + +
+ + @* ── Export ── *@ +
+ + +
+ +
+ + @* ── Grid Toggle ── *@ + + + @* ── Add Annotation ── *@ + + + @* ── Spacer ── *@ +
+ + @* ── Dirty Indicator ── *@ + @if ( DirtyCount > 0 ) { + + @DirtyCount unsaved + + } else { + + Saved + + } +
+ +@code { + [Parameter] public int DirtyCount { get; set; } + [Parameter] public bool IsSaving { get; set; } + [Parameter] public bool IsValidating { get; set; } + [Parameter] public string Direction { get; set; } = "LR"; + [Parameter] public double ZoomLevel { get; set; } = 1.0; + + [Parameter] public EventCallback OnSave { get; set; } + [Parameter] public EventCallback OnUndo { get; set; } + [Parameter] public EventCallback OnRedo { get; set; } + [Parameter] public EventCallback OnValidate { get; set; } + [Parameter] public EventCallback OnAutoLayout { get; set; } + [Parameter] public EventCallback OnZoomIn { get; set; } + [Parameter] public EventCallback OnZoomOut { get; set; } + [Parameter] public EventCallback OnZoomFit { get; set; } + [Parameter] public EventCallback OnDirectionChanged { get; set; } + [Parameter] public EventCallback OnExportSvg { get; set; } + [Parameter] public EventCallback OnExportPng { get; set; } + [Parameter] public bool GridVisible { get; set; } + [Parameter] public EventCallback OnToggleGrid { get; set; } + [Parameter] public EventCallback OnAddAnnotation { get; set; } +} diff --git a/src/Werkr.Server/Components/Shared/FilterBar.razor b/src/Werkr.Server/Components/Shared/FilterBar.razor new file mode 100644 index 0000000..dd42304 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/FilterBar.razor @@ -0,0 +1,213 @@ +@using Werkr.Common.Models +@using Werkr.Server.Services +@inject SavedFilterService FilterService +@inject ILogger Logger + +
+
+ @foreach ( FilterField field in Definition.Fields ) { +
+ + @switch ( field.Type ) { + case FilterFieldType.Dropdown: + + break; + + case FilterFieldType.Date: + + break; + + case FilterFieldType.Text: + + break; + + case FilterFieldType.Number: + + break; + } +
+ } + +
+ + +
+
+ +
+ + + + + @if ( _savedFilters.Count > 0 ) { + + } +
+
+ +@if ( _showSaveDialog ) { +
+
+ + + +
+
+} + +@code { + [Parameter, EditorRequired] public string PageKey { get; set; } = ""; + [Parameter, EditorRequired] public FilterDefinition Definition { get; set; } = new( [] ); + [Parameter] public EventCallback OnFilterApplied { get; set; } + [Parameter] public EventCallback OnFilterCleared { get; set; } + + private FilterCriteria _criteria = new( ); + private List _savedFilters = []; + private string _selectedFilterId = ""; + private bool _showSaveDialog; + private string _saveFilterName = ""; + private bool _initialized; + + protected override async Task OnAfterRenderAsync( bool firstRender ) { + if ( !firstRender || _initialized ) return; + _initialized = true; + + try { + _savedFilters = [.. await FilterService.GetAllFiltersAsync( PageKey )]; + + SavedFilterView? defaultFilter = _savedFilters.FirstOrDefault( f => f.IsDefault ); + if ( defaultFilter is not null ) { + _criteria = defaultFilter.Criteria; + _selectedFilterId = defaultFilter.Id; + await OnFilterApplied.InvokeAsync( _criteria ); + } + + StateHasChanged( ); + } catch ( Exception ex ) { + Logger.LogWarning( ex, "Failed to load saved filters for page {PageKey}.", PageKey ); + } + } + + private string GetValue( string key ) => _criteria.Get( key ) ?? ""; + + private void SetValue( string key, string? value ) { + if ( string.IsNullOrWhiteSpace( value ) ) { + _criteria.Values.Remove( key ); + } else { + _criteria.Values[key] = value; + } + } + + private async Task ApplyAsync( ) { + _selectedFilterId = ""; + await OnFilterApplied.InvokeAsync( _criteria ); + } + + private async Task ClearAsync( ) { + _criteria = new FilterCriteria( ); + _selectedFilterId = ""; + await OnFilterCleared.InvokeAsync( ); + } + + private async Task OnSavedFilterSelectedAsync( ChangeEventArgs e ) { + string? filterId = e.Value?.ToString( ); + if ( string.IsNullOrWhiteSpace( filterId ) ) return; + + SavedFilterView? filter = _savedFilters.FirstOrDefault( f => f.Id == filterId ); + if ( filter is null ) return; + + _selectedFilterId = filterId; + _criteria = filter.Criteria; + await OnFilterApplied.InvokeAsync( _criteria ); + } + + private Task ShowSaveDialogAsync( ) { + _showSaveDialog = true; + _saveFilterName = ""; + return Task.CompletedTask; + } + + private async Task SaveCurrentFilterAsync( ) { + if ( string.IsNullOrWhiteSpace( _saveFilterName ) ) return; + + SavedFilterView filter = new( + Id: Guid.NewGuid( ).ToString( ), + Name: _saveFilterName.Trim( ), + IsDefault: false, + IsShared: false, + Criteria: _criteria + ); + + await FilterService.SaveFilterAsync( PageKey, filter ); + _savedFilters = [.. await FilterService.GetAllFiltersAsync( PageKey )]; + _showSaveDialog = false; + _selectedFilterId = filter.Id; + } + + private async Task SetDefaultAsync( string filterId ) { + await FilterService.SetDefaultAsync( PageKey, filterId ); + _savedFilters = [.. await FilterService.GetAllFiltersAsync( PageKey )]; + } + + private async Task DeleteFilterAsync( string filterId ) { + await FilterService.DeleteFilterAsync( PageKey, filterId ); + _savedFilters = [.. await FilterService.GetAllFiltersAsync( PageKey )]; + if ( _selectedFilterId == filterId ) { + _selectedFilterId = ""; + } + } +} diff --git a/src/Werkr.Server/Components/Shared/FilterBar.razor.css b/src/Werkr.Server/Components/Shared/FilterBar.razor.css new file mode 100644 index 0000000..ff9d0aa --- /dev/null +++ b/src/Werkr.Server/Components/Shared/FilterBar.razor.css @@ -0,0 +1,50 @@ +/* FilterBar — universal filter bar layout */ + +.filter-bar { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 0.75rem; + margin-bottom: 1rem; + padding: 0.75rem; + background: var(--bs-body-bg); + border: 1px solid var(--bs-border-color); + border-radius: 0.375rem; +} + +.filter-bar-controls { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 0.5rem; + flex: 1 1 auto; +} + +.filter-field { + display: flex; + flex-direction: column; + min-width: 120px; + max-width: 200px; +} + +.filter-actions { + display: flex; + gap: 0.25rem; + align-self: flex-end; +} + +.filter-bar-saved { + display: flex; + align-items: center; + gap: 0.25rem; + flex-shrink: 0; +} + +.saved-filter-select { + min-width: 160px; + max-width: 220px; +} + +.filter-save-dialog { + width: 100%; +} diff --git a/src/Werkr.Server/Components/Shared/GanttTab.razor b/src/Werkr.Server/Components/Shared/GanttTab.razor new file mode 100644 index 0000000..34d3c45 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/GanttTab.razor @@ -0,0 +1,166 @@ +@using Werkr.Common.Models +@using Werkr.Server.Helpers +@using Werkr.Server.Hubs +@implements IAsyncDisposable +@inject IJSRuntime JS +@inject ILogger Logger +@inject IHttpClientFactory HttpClientFactory + + + + + +@if ( !string.IsNullOrWhiteSpace( _errorMessage ) ) { + +} + +
+ +@code { + [Parameter] public IReadOnlyList StepExecutions { get; set; } = []; + [Parameter] public IReadOnlyList Steps { get; set; } = []; + [Parameter] public bool IsRunActive { get; set; } + [Parameter] public EventCallback OnStepSelected { get; set; } + + /// Optional RunId for self-fetching run detail data. When provided and StepExecutions is empty, fetches data from the API. + [Parameter] public Guid? RunId { get; set; } + + private readonly string _containerId = $"gantt-{Guid.NewGuid( ):N}"; + private TimelineJsInterop? _interop; + private string? _errorMessage; + private bool _initialized; + private IReadOnlyList _effectiveExecutions = []; + private IReadOnlyList _effectiveSteps = []; + + protected override async Task OnParametersSetAsync( ) { + if ( StepExecutions.Count > 0 || RunId is null ) { + _effectiveExecutions = StepExecutions; + _effectiveSteps = Steps; + return; + } + + try { + HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); + WorkflowRunDetailDto? detail = await client.GetFromJsonAsync( + $"/api/workflows/runs/{RunId}" ); + if ( detail is not null ) { + _effectiveExecutions = detail.StepExecutions; + // Fetch workflow steps if not provided + if ( Steps.Count == 0 ) { + WorkflowDto? wf = await client.GetFromJsonAsync( + $"/api/workflows/{detail.WorkflowId}" ); + _effectiveSteps = wf?.Steps ?? []; + } else { + _effectiveSteps = Steps; + } + } + } catch ( Exception ex ) { + Logger.LogWarning( ex, "Failed to fetch run detail for RunId {RunId}.", RunId ); + _errorMessage = "Failed to load Gantt data."; + } + } + + protected override async Task OnAfterRenderAsync( bool firstRender ) { + if ( !firstRender || _initialized ) return; + _initialized = true; + + try { + _interop = new TimelineJsInterop( JS ); + _interop.OnItemClicked += OnItemClickedAsync; + await _interop.InitAsync( _containerId, new { } ); + + GanttItemDto[] items = MapExecutionsToItems( ); + if ( items.Length > 0 ) { + await _interop.LoadItemsAsync( items ); + } + } catch ( Exception ex ) { + Logger.LogError( ex, "Failed to initialize Gantt timeline." ); + _errorMessage = "Timeline view could not be loaded. Try refreshing the page."; + StateHasChanged( ); + } + } + + /// Called by parent RunDetail.razor to forward SignalR step status events. + public async Task HandleStepStatusChangedAsync( StepStatusDto dto ) { + if ( _interop is null || !_initialized ) return; + + string stepName = GetStepName( dto.StepId ); + string className = StatusToClassName( dto.Status ); + string tooltip = BuildTooltip( stepName, dto.Status, dto.RuntimeSeconds ); + + GanttItemDto item = new( + Id: $"{dto.StepId}-1", + Content: stepName, + Start: dto.Timestamp.ToString( "o" ), + End: dto.Status is "Completed" or "Failed" or "Skipped" ? dto.Timestamp.ToString( "o" ) : null, + ClassName: className, + Title: tooltip + ); + + try { + await _interop.UpdateItemAsync( item ); + } catch ( Exception ex ) { + Logger.LogWarning( ex, "Failed to update Gantt item for step {StepId}.", dto.StepId ); + } + } + + private GanttItemDto[] MapExecutionsToItems( ) { + List items = new( ); + foreach ( StepExecutionDto se in _effectiveExecutions ) { + if ( se.StartTime is null ) continue; + + string stepName = GetStepName( se.StepId ); + string className = StatusToClassName( se.Status ); + double? duration = se is { StartTime: not null, EndTime: not null } + ? ( se.EndTime.Value - se.StartTime.Value ).TotalSeconds + : null; + string tooltip = BuildTooltip( stepName, se.Status, duration ); + + items.Add( new GanttItemDto( + Id: $"{se.StepId}-{se.Attempt}", + Content: stepName, + Start: se.StartTime!.Value.ToString( "o" ), + End: se.EndTime?.ToString( "o" ), + ClassName: className, + Title: tooltip + ) ); + } + return [.. items]; + } + + private string GetStepName( long stepId ) { + WorkflowStepDto? step = _effectiveSteps.FirstOrDefault( s => s.Id == stepId ); + return step is not null ? $"Step #{step.Order} (Task {step.TaskId})" : $"Step {stepId}"; + } + + private static string StatusToClassName( string status ) => status switch { + "Running" => "gantt-running", + "Completed" => "gantt-completed", + "Failed" => "gantt-failed", + "Skipped" => "gantt-skipped", + _ => "gantt-pending", + }; + + private static string BuildTooltip( string stepName, string status, double? runtimeSeconds ) { + string duration = runtimeSeconds.HasValue ? $" ({runtimeSeconds.Value:F1}s)" : ""; + return $"{stepName}{duration} — {status}"; + } + + private async Task OnItemClickedAsync( long stepId ) { + StepExecutionDto? step = _effectiveExecutions + .Where( e => e.StepId == stepId ) + .OrderByDescending( e => e.Attempt ) + .FirstOrDefault( ); + + if ( step is not null ) { + await OnStepSelected.InvokeAsync( step ); + } + } + + public async ValueTask DisposeAsync( ) { + if ( _interop is not null ) { + _interop.OnItemClicked -= OnItemClickedAsync; + await _interop.DisposeAsync( ); + } + } +} diff --git a/src/Werkr.Server/Components/Shared/GanttTab.razor.css b/src/Werkr.Server/Components/Shared/GanttTab.razor.css new file mode 100644 index 0000000..4611bb3 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/GanttTab.razor.css @@ -0,0 +1,43 @@ +/* Gantt tab — vis-timeline style scoping and status color overrides */ + +::deep .gantt-container .vis-item.gantt-completed { + background-color: var(--werkr-success); + border-color: var(--werkr-success); + color: #fff; +} + +::deep .gantt-container .vis-item.gantt-failed { + background-color: var(--werkr-failed); + border-color: var(--werkr-failed); + color: #fff; +} + +::deep .gantt-container .vis-item.gantt-running { + background-color: var(--werkr-running); + border-color: var(--werkr-running); + color: #212529; + animation: gantt-pulse 1.5s ease-in-out infinite; +} + +::deep .gantt-container .vis-item.gantt-skipped { + background-color: var(--werkr-skipped); + border-color: var(--werkr-skipped); + color: #fff; +} + +::deep .gantt-container .vis-item.gantt-pending { + background-color: var(--werkr-not-run); + border-color: var(--werkr-not-run); + color: #fff; +} + +@keyframes gantt-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +/* Ensure the timeline container respects dark theme backgrounds */ +::deep .gantt-container .vis-timeline { + border: 1px solid var(--werkr-grey-300, #dee2e6); + border-radius: 0.375rem; +} diff --git a/src/Werkr.Server/Components/Shared/LogViewer.razor b/src/Werkr.Server/Components/Shared/LogViewer.razor new file mode 100644 index 0000000..0e6e313 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/LogViewer.razor @@ -0,0 +1,20 @@ +@* Live log viewer for workflow step output with auto-scroll. *@ + +
+ @if ( Lines is null || Lines.Count == 0 ) { + No log output yet. + } else { + @for ( int i = 0; i < Lines.Count; i++ ) { +
+ @( i + 1 )@Lines[i] +
+ } + } +
+ +@code { + /// Log lines to display. Updated externally via SignalR or polling. + [Parameter] public IReadOnlyList? Lines { get; set; } + + private ElementReference _logContainer; +} diff --git a/src/Werkr.Server/Components/Shared/NodeConfigPanel.razor b/src/Werkr.Server/Components/Shared/NodeConfigPanel.razor new file mode 100644 index 0000000..1da920e --- /dev/null +++ b/src/Werkr.Server/Components/Shared/NodeConfigPanel.razor @@ -0,0 +1,217 @@ +@rendermode InteractiveServer +@using Werkr.Common.Models + +
+ @if ( SelectedStepId.HasValue ) { +
+
+ @if ( SelectedStepId.Value < 0 ) { + New + } + Step @( SelectedStepId.Value < 0 ? $"(temp {SelectedStepId.Value})" : $"#{SelectedStepId.Value}" ) +
+ +
+ +
+ @* ── Task ── *@ +
+ + @if ( SelectedStepId.Value < 0 ) { +
+ + +
+ } else { +
+ @_taskName + +
+ } +
+ + @* ── Control Statement ── *@ +
+ + +
+ + @* ── Condition ── *@ + @if ( _controlStatement is "If" or "ElseIf" or "While" or "Do" ) { +
+ + +
+ +
+ + +
+ } + + @* ── Dependency Mode ── *@ +
+ + +
+ + @* ── Variable Names ── *@ +
+ + +
+ +
+ + +
+ + @* ── Dependencies ── *@ + @if ( Dependencies is { Count: > 0 } ) { +
+ +
    + @foreach ( long depId in Dependencies ) { +
  • + Step @depId + +
  • + } +
+
+ } + + @* ── Delete ── *@ +
+ +
+
+ } +
+ +@code { + [Parameter] public long? SelectedStepId { get; set; } + [Parameter] public IReadOnlyList? Tasks { get; set; } + [Parameter] public IReadOnlyList? Dependencies { get; set; } + + // Initial values set from parent on selection + [Parameter] public long TaskId { get; set; } + [Parameter] public string? TaskName { get; set; } + [Parameter] public string ControlStatement { get; set; } = "Default"; + [Parameter] public string? ConditionExpression { get; set; } + [Parameter] public int MaxIterations { get; set; } = 100; + [Parameter] public string DependencyMode { get; set; } = "All"; + [Parameter] public string? InputVariableName { get; set; } + [Parameter] public string? OutputVariableName { get; set; } + + [Parameter] public EventCallback<(string Field, object? Value)> OnFieldChanged { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + [Parameter] public EventCallback OnDelete { get; set; } + [Parameter] public EventCallback OnEditTaskRequested { get; set; } + [Parameter] public EventCallback OnCreateTaskRequested { get; set; } + [Parameter] public EventCallback OnRemoveDependency { get; set; } + + private long _taskId; + private string _taskName = ""; + private string _controlStatement = "Default"; + private string? _conditionExpression; + private int _maxIterations = 100; + private string _dependencyMode = "All"; + private string? _inputVariableName; + private string? _outputVariableName; + + protected override void OnParametersSet( ) { + _taskId = TaskId; + _taskName = TaskName ?? ""; + _controlStatement = ControlStatement; + _conditionExpression = ConditionExpression; + _maxIterations = MaxIterations; + _dependencyMode = DependencyMode; + _inputVariableName = InputVariableName; + _outputVariableName = OutputVariableName; + } + + private async Task Close( ) => await OnClose.InvokeAsync( ); + private async Task OnDeleteStep( ) => await OnDelete.InvokeAsync( ); + private async Task OnEditTask( ) => await OnEditTaskRequested.InvokeAsync( ); + private async Task OnCreateTask( ) => await OnCreateTaskRequested.InvokeAsync( ); + + private async Task OnTaskIdChanged( ChangeEventArgs e ) { + if (long.TryParse( e.Value?.ToString( ), out long taskId )) { + _taskId = taskId; + await EmitFieldChange( "taskId", taskId ); + } + } + + private async Task OnControlChanged( ChangeEventArgs e ) { + _controlStatement = e.Value?.ToString( ) ?? "Default"; + await EmitFieldChange( "controlStatement", _controlStatement ); + } + + private async Task OnMaxIterationsChanged( ChangeEventArgs e ) { + if (int.TryParse( e.Value?.ToString( ), out int val )) { + _maxIterations = val; + await EmitFieldChange( "maxIterations", val ); + } + } + + private async Task OnDependencyModeChanged( ChangeEventArgs e ) { + _dependencyMode = e.Value?.ToString( ) ?? "All"; + await EmitFieldChange( "dependencyMode", _dependencyMode ); + } + + private async Task OnInputVariableChanged( ChangeEventArgs e ) { + _inputVariableName = e.Value?.ToString( ); + await EmitFieldChange( "inputVariableName", _inputVariableName ); + } + + private async Task OnOutputVariableChanged( ChangeEventArgs e ) { + _outputVariableName = e.Value?.ToString( ); + await EmitFieldChange( "outputVariableName", _outputVariableName ); + } + + private async Task EmitFieldChange( string field, object? value ) { + await OnFieldChanged.InvokeAsync( (field, value) ); + } +} diff --git a/src/Werkr.Server/Components/Shared/NodeConfigPanel.razor.css b/src/Werkr.Server/Components/Shared/NodeConfigPanel.razor.css new file mode 100644 index 0000000..3198a2e --- /dev/null +++ b/src/Werkr.Server/Components/Shared/NodeConfigPanel.razor.css @@ -0,0 +1,14 @@ +.node-config-panel { + width: 0; + min-width: 0; + overflow: hidden; + border-left: 0 solid var(--bs-border-color); + background-color: var(--bs-body-bg); + transition: width 0.2s ease, min-width 0.2s ease, border-left-width 0.2s ease; +} + +.node-config-panel.open { + width: 360px; + min-width: 360px; + border-left-width: 1px; +} diff --git a/src/Werkr.Server/Components/Shared/RunGridView.razor b/src/Werkr.Server/Components/Shared/RunGridView.razor new file mode 100644 index 0000000..a4ebc69 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/RunGridView.razor @@ -0,0 +1,69 @@ +@* Steps × Runs status matrix for cross-run pattern recognition. *@ +@using Werkr.Common.Models + +@if ( Steps is not null && Runs is not null && Steps.Count > 0 && Runs.Count > 0 ) { +
+ + + + + @foreach ( RunGridEntry run in Runs ) { + + } + + + + @foreach ( RunGridStep step in Steps ) { + + + @foreach ( RunGridEntry run in Runs ) { + string status = GetCellStatus( run, step.Id ); + + } + + } + +
Step + + @run.StartTime.ToString( "MMM dd HH:mm" ) + +
+ @run.Status +
+ @step.Name + + + @GetStatusIcon( status ) + +
+
+} else { +

No run data available for grid view.

+} + +@code { + [Parameter] public IReadOnlyList? Steps { get; set; } + [Parameter] public IReadOnlyList? Runs { get; set; } + [Parameter] public EventCallback<(Guid RunId, long StepId)> OnCellSelected { get; set; } + + /// Row header model — step identity. + public sealed record RunGridStep( long Id, string Name, int Order ); + + /// Column model — one run with per-step status map. + public sealed record RunGridEntry( + Guid RunId, DateTime StartTime, string Status, + IReadOnlyDictionary StepStatuses ); + + private static string GetCellStatus( RunGridEntry run, long stepId ) => + run.StepStatuses.TryGetValue( stepId, out string? s ) ? s : "Pending"; + + private static string GetStatusIcon( string status ) => status switch { + "Completed" => "✓", + "Failed" => "✗", + "Running" => "▶", + "Skipped" => "⏭", + _ => "·", + }; +} diff --git a/src/Werkr.Server/Components/Shared/RunGridView.razor.css b/src/Werkr.Server/Components/Shared/RunGridView.razor.css new file mode 100644 index 0000000..220ec27 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/RunGridView.razor.css @@ -0,0 +1,10 @@ +.run-grid-table .sticky-start { + position: sticky; + left: 0; + z-index: 1; +} + +.run-grid-table td[role="button"]:hover { + background-color: var(--bs-tertiary-bg); + cursor: pointer; +} diff --git a/src/Werkr.Server/Components/Shared/RunSparkline.razor b/src/Werkr.Server/Components/Shared/RunSparkline.razor new file mode 100644 index 0000000..0b3638b --- /dev/null +++ b/src/Werkr.Server/Components/Shared/RunSparkline.razor @@ -0,0 +1,41 @@ +@* SVG sparkline showing recent run status and relative duration. *@ + +@if ( Runs is not null && Runs.Count > 0 ) { + double maxDuration = Runs.Max( r => r.DurationSeconds ?? 1.0 ); + if ( maxDuration <= 0 ) maxDuration = 1; + int barWidth = 8; + int gap = 2; + int height = 32; + int totalWidth = Runs.Count * ( barWidth + gap ) - gap; + + + @for ( int i = 0; i < Runs.Count; i++ ) { + RunSummary run = Runs[i]; + double pct = ( run.DurationSeconds ?? 0 ) / maxDuration; + int barHeight = Math.Max( 2, (int)( pct * height ) ); + int x = i * ( barWidth + gap ); + int y = height - barHeight; + string fill = GetFill( run.Status ); + bool isRunning = run.Status == "Running"; + + + @run.Status — @( run.DurationSeconds?.ToString( "F1" ) ?? "?" )s + + } + +} + +@code { + [Parameter] public IReadOnlyList? Runs { get; set; } + + /// Lightweight run summary for sparkline rendering. + public sealed record RunSummary( Guid RunId, string Status, double? DurationSeconds ); + + private static string GetFill( string status ) => status switch { + "Completed" => "var(--werkr-success)", + "Failed" => "var(--werkr-failed)", + "Running" => "var(--werkr-running)", + _ => "var(--werkr-pending)", + }; +} diff --git a/src/Werkr.Server/Components/Shared/RunSparkline.razor.css b/src/Werkr.Server/Components/Shared/RunSparkline.razor.css new file mode 100644 index 0000000..e57bad2 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/RunSparkline.razor.css @@ -0,0 +1,8 @@ +.sparkline-running { + animation: sparkline-pulse 1.5s ease-in-out infinite; +} + +@keyframes sparkline-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} diff --git a/src/Werkr.Server/Components/Shared/ScheduleSetupModal.razor b/src/Werkr.Server/Components/Shared/ScheduleSetupModal.razor new file mode 100644 index 0000000..6f576de --- /dev/null +++ b/src/Werkr.Server/Components/Shared/ScheduleSetupModal.razor @@ -0,0 +1,617 @@ +@using System.ComponentModel.DataAnnotations +@using Werkr.Common.Models.Holidays +@rendermode InteractiveServer +@inject IHttpClientFactory HttpClientFactory + +@if ( _isVisible ) { + +} + +@code { + [Parameter] + public EventCallback OnScheduleCreated { get; set; } + + [Parameter] + public EventCallback OnScheduleUpdated { get; set; } + + private bool _isVisible; + private bool _isSaving; + private bool _isPreviewLoading; + private string? _errorMessage; + private ScheduleFormModel _model = new( ); + private Guid? _editScheduleId; + private List? _previewOccurrences; + + // Holiday calendar + private List? _availableCalendars; + private string _selectedCalendarId = ""; + private string _selectedCalendarMode = "Blocklist"; + private List? _auditLogs; + + private bool IsEditing => _editScheduleId is not null; + + private static readonly (string Label, int Flag)[] s_daysOfWeek = [ + ("Sun", 1), ("Mon", 2), ("Tue", 4), ("Wed", 8), ("Thu", 16), ("Fri", 32), ("Sat", 64) + ]; + + private static readonly (string Label, int Flag)[] s_months = [ + ("Jan", 1), ("Feb", 2), ("Mar", 4), ("Apr", 8), + ("May", 16), ("Jun", 32), ("Jul", 64), ("Aug", 128), + ("Sep", 256), ("Oct", 512), ("Nov", 1024), ("Dec", 2048) + ]; + + private static readonly (string Label, int Flag)[] s_weekNumbers = [ + ("1st", 1), ("2nd", 2), ("3rd", 4), ("4th", 8), ("5th", 16), ("6th", 32) + ]; + + private static readonly IReadOnlyList s_timeZones = + TimeZoneInfo.GetSystemTimeZones( ).OrderBy( tz => tz.BaseUtcOffset ).ToList( ); + + public async Task Show( ) { + _model = new ScheduleFormModel( ); + _editScheduleId = null; + _isVisible = true; + _isSaving = false; + _isPreviewLoading = false; + _errorMessage = null; + _previewOccurrences = null; + _selectedCalendarId = ""; + _selectedCalendarMode = "Blocklist"; + _auditLogs = null; + await LoadCalendarsAsync( ); + StateHasChanged( ); + } + + public async Task Show( ScheduleDto existingSchedule ) { + _model = ScheduleFormModel.FromDto( existingSchedule ); + _editScheduleId = existingSchedule.Id; + _isVisible = true; + _isSaving = false; + _isPreviewLoading = false; + _errorMessage = null; + _previewOccurrences = null; + _selectedCalendarId = ""; + _selectedCalendarMode = "Blocklist"; + _auditLogs = null; + await LoadCalendarsAsync( ); + await LoadHolidayCalendarLinkAsync( ); + StateHasChanged( ); + } + + private void Close( ) { + _isVisible = false; + StateHasChanged( ); + } + + private async Task LoadCalendarsAsync( ) { + try { + HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); + _availableCalendars = await client.GetFromJsonAsync>( "/api/holiday-calendars" ); + } catch { + // Non-critical: calendar list not loaded + } + } + + private async Task LoadHolidayCalendarLinkAsync( ) { + if ( _editScheduleId is null ) return; + try { + HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); + ScheduleHolidayCalendarDto? link = await client.GetFromJsonAsync( + $"/api/schedules/{_editScheduleId}/holiday-calendar" ); + if ( link is not null ) { + _selectedCalendarId = link.CalendarId.ToString( ); + _selectedCalendarMode = link.Mode; + await LoadAuditLogAsync( ); + } + } catch { + // No calendar attached — OK + } + } + + private async Task LoadAuditLogAsync( ) { + if ( _editScheduleId is null ) return; + try { + HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); + DateTime from = DateTime.UtcNow.AddDays( -30 ); + DateTime to = DateTime.UtcNow; + string url = $"/api/schedules/{_editScheduleId}/audit-log?from={from:O}&to={to:O}"; + _auditLogs = await client.GetFromJsonAsync>( url ) ?? []; + } catch { + _auditLogs = []; + } + } + + private async Task SubmitAsync( ) { + _isSaving = true; + _errorMessage = null; + try { + HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); + + if ( IsEditing ) { + ScheduleUpdateRequest request = _model.ToUpdateRequest( ); + HttpResponseMessage response = await client.PutAsJsonAsync( $"/api/schedules/{_editScheduleId}", request ); + if ( response.IsSuccessStatusCode ) { + await SaveHolidayCalendarAsync( client, _editScheduleId!.Value ); + ScheduleDto? updated = await client.GetFromJsonAsync( $"/api/schedules/{_editScheduleId}" ); + _isVisible = false; + if ( updated is not null ) { + await OnScheduleUpdated.InvokeAsync( updated ); + } + } else { + string body = await response.Content.ReadAsStringAsync( ); + _errorMessage = $"Failed to update schedule: {body}"; + } + } else { + ScheduleCreateRequest request = _model.ToCreateRequest( ); + HttpResponseMessage response = await client.PostAsJsonAsync( "/api/schedules", request ); + if ( response.IsSuccessStatusCode ) { + ScheduleDto? created = await response.Content.ReadFromJsonAsync( ); + if ( created is not null ) { + await SaveHolidayCalendarAsync( client, created.Id ); + } + _isVisible = false; + if ( created is not null ) { + await OnScheduleCreated.InvokeAsync( created ); + } + } else { + string body = await response.Content.ReadAsStringAsync( ); + _errorMessage = $"Failed to create schedule: {body}"; + } + } + } catch ( Exception ex ) { + _errorMessage = IsEditing + ? $"Failed to update schedule: {ex.Message}" + : $"Failed to create schedule: {ex.Message}"; + } finally { + _isSaving = false; + } + } + + private async Task SaveHolidayCalendarAsync( HttpClient client, Guid scheduleId ) { + if ( !string.IsNullOrEmpty( _selectedCalendarId ) && Guid.TryParse( _selectedCalendarId, out Guid calId ) ) { + AttachHolidayCalendarRequest attachReq = new( calId, _selectedCalendarMode ); + _ = await client.PutAsJsonAsync( $"/api/schedules/{scheduleId}/holiday-calendar", attachReq ); + } else if ( IsEditing ) { + _ = await client.DeleteAsync( $"/api/schedules/{scheduleId}/holiday-calendar" ); + } + } + + private void ComputePreviewAsync( ) { + _isPreviewLoading = true; + _previewOccurrences = null; + try { + _previewOccurrences = SchedulePreviewCalculator.Calculate( _model.ToCreateRequest( ), 10 ); + } catch ( Exception ex ) { + _errorMessage = $"Preview failed: {ex.Message}"; + } finally { + _isPreviewLoading = false; + } + } + + private sealed class ScheduleFormModel { + [Required( ErrorMessage = "Name is required." )] + [StringLength( 200, MinimumLength = 1, ErrorMessage = "Name must be between 1 and 200 characters." )] + public string Name { get; set; } = ""; + + [Range( 1, 1440, ErrorMessage = "Stop after must be between 1 and 1440 minutes." )] + public long StopTaskAfterMinutes { get; set; } = 60; + + [Required] + public DateOnly StartDate { get; set; } = DateOnly.FromDateTime( DateTime.Today ); + + [Required] + public TimeOnly StartTime { get; set; } = new( 8, 0 ); + + [Required( ErrorMessage = "Time zone is required." )] + public string TimeZoneId { get; set; } = TimeZoneInfo.Local.Id; + + public string RecurrenceType { get; set; } = "None"; + + [Range( 1, 365, ErrorMessage = "Day interval must be between 1 and 365." )] + public int DayInterval { get; set; } = 1; + + [Range( 1, 52, ErrorMessage = "Week interval must be between 1 and 52." )] + public int WeekInterval { get; set; } = 1; + + public int DaysOfWeek { get; set; } + public int MonthsOfYear { get; set; } + public string MonthlyMode { get; set; } = "DayNumbers"; + public string DayNumbers { get; set; } = ""; + public int MonthlyWeekNumber { get; set; } + public int MonthlyDaysOfWeek { get; set; } + public bool HasExpiration { get; set; } + public DateOnly ExpirationDate { get; set; } = DateOnly.FromDateTime( DateTime.Today.AddYears( 1 ) ); + public TimeOnly ExpirationTime { get; set; } = new( 23, 59 ); + public string ExpirationTimeZoneId { get; set; } = TimeZoneInfo.Local.Id; + public bool HasRepeat { get; set; } + + [Range( 1, 1440, ErrorMessage = "Repeat interval must be between 1 and 1440 minutes." )] + public int RepeatIntervalMinutes { get; set; } = 15; + + [Range( 1, 1440, ErrorMessage = "Repeat duration must be between 1 and 1440 minutes." )] + public int RepeatDurationMinutes { get; set; } = 60; + + public static ScheduleFormModel FromDto( ScheduleDto dto ) { + string recType = dto.DailyRecurrence is not null ? "Daily" + : dto.WeeklyRecurrence is not null ? "Weekly" + : dto.MonthlyRecurrence is not null ? "Monthly" + : "None"; + + return new ScheduleFormModel { + Name = dto.Name, + StopTaskAfterMinutes = dto.StopTaskAfterMinutes, + StartDate = dto.StartDateTime?.Date ?? DateOnly.FromDateTime( DateTime.Today ), + StartTime = dto.StartDateTime?.Time ?? new TimeOnly( 8, 0 ), + TimeZoneId = dto.StartDateTime?.TimeZoneId ?? TimeZoneInfo.Local.Id, + RecurrenceType = recType, + DayInterval = dto.DailyRecurrence?.DayInterval ?? 1, + WeekInterval = dto.WeeklyRecurrence?.WeekInterval ?? 1, + DaysOfWeek = dto.WeeklyRecurrence?.DaysOfWeek ?? 0, + MonthsOfYear = dto.MonthlyRecurrence?.MonthsOfYear ?? 0, + MonthlyMode = dto.MonthlyRecurrence?.WeekNumber is not null ? "WeekAndDay" : "DayNumbers", + DayNumbers = dto.MonthlyRecurrence?.DayNumbers is not null + ? string.Join( ", ", dto.MonthlyRecurrence.DayNumbers ) + : "", + MonthlyWeekNumber = dto.MonthlyRecurrence?.WeekNumber ?? 0, + MonthlyDaysOfWeek = dto.MonthlyRecurrence?.DaysOfWeek ?? 0, + HasExpiration = dto.Expiration is not null, + ExpirationDate = dto.Expiration?.Date ?? DateOnly.FromDateTime( DateTime.Today.AddYears( 1 ) ), + ExpirationTime = dto.Expiration?.Time ?? new TimeOnly( 23, 59 ), + ExpirationTimeZoneId = dto.Expiration?.TimeZoneId ?? TimeZoneInfo.Local.Id, + HasRepeat = dto.RepeatOptions is not null, + RepeatIntervalMinutes = dto.RepeatOptions?.RepeatIntervalMinutes ?? 15, + RepeatDurationMinutes = dto.RepeatOptions?.RepeatDurationMinutes ?? 60, + }; + } + + public ScheduleCreateRequest ToCreateRequest( ) { + return new ScheduleCreateRequest( + Name: Name, + StopTaskAfterMinutes: StopTaskAfterMinutes, + StartDateTime: new StartDateTimeDto( StartDate, StartTime, TimeZoneId ), + Expiration: HasExpiration + ? new ExpirationDateTimeDto( ExpirationDate, ExpirationTime, ExpirationTimeZoneId ) + : null, + DailyRecurrence: RecurrenceType == "Daily" ? new DailyRecurrenceDto( DayInterval ) : null, + WeeklyRecurrence: RecurrenceType == "Weekly" ? new WeeklyRecurrenceDto( WeekInterval, DaysOfWeek ) : null, + MonthlyRecurrence: RecurrenceType == "Monthly" + ? new MonthlyRecurrenceDto( + MonthlyMode == "DayNumbers" ? ParseDayNumbers( DayNumbers ) : null, + MonthsOfYear, + MonthlyMode == "WeekAndDay" ? MonthlyWeekNumber : null, + MonthlyMode == "WeekAndDay" ? MonthlyDaysOfWeek : null ) + : null, + RepeatOptions: HasRepeat + ? new RepeatOptionsDto( RepeatIntervalMinutes, RepeatDurationMinutes ) + : null ); + } + + public ScheduleUpdateRequest ToUpdateRequest( ) { + return new ScheduleUpdateRequest( + Name: Name, + StopTaskAfterMinutes: StopTaskAfterMinutes, + StartDateTime: new StartDateTimeDto( StartDate, StartTime, TimeZoneId ), + Expiration: HasExpiration + ? new ExpirationDateTimeDto( ExpirationDate, ExpirationTime, ExpirationTimeZoneId ) + : null, + DailyRecurrence: RecurrenceType == "Daily" ? new DailyRecurrenceDto( DayInterval ) : null, + WeeklyRecurrence: RecurrenceType == "Weekly" ? new WeeklyRecurrenceDto( WeekInterval, DaysOfWeek ) : null, + MonthlyRecurrence: RecurrenceType == "Monthly" + ? new MonthlyRecurrenceDto( + MonthlyMode == "DayNumbers" ? ParseDayNumbers( DayNumbers ) : null, + MonthsOfYear, + MonthlyMode == "WeekAndDay" ? MonthlyWeekNumber : null, + MonthlyMode == "WeekAndDay" ? MonthlyDaysOfWeek : null ) + : null, + RepeatOptions: HasRepeat + ? new RepeatOptionsDto( RepeatIntervalMinutes, RepeatDurationMinutes ) + : null ); + } + + private static int[]? ParseDayNumbers( string input ) { + if ( string.IsNullOrWhiteSpace( input ) ) return null; + return input.Split( ',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ) + .Select( int.Parse ) + .ToArray( ); + } + } +} diff --git a/src/Werkr.Server/Components/Shared/StepDetailPanel.razor b/src/Werkr.Server/Components/Shared/StepDetailPanel.razor new file mode 100644 index 0000000..647bef2 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/StepDetailPanel.razor @@ -0,0 +1,93 @@ +@* Step detail side panel — shows I/O, log preview, and variables for a selected step. *@ +@using Werkr.Server.Services + +@inject IHttpClientFactory HttpClientFactory + +@if ( Step is not null ) { +
+
+ Step #@Step.StepId — @Step.Status + +
+
+
+
Step ID
+
@Step.StepId
+ +
Attempt
+
@Step.Attempt
+ +
Status
+
+ @Step.Status +
+ + @if ( Step.StartTime.HasValue ) { +
Started
+
@Step.StartTime.Value.ToString( "G" )
+ } + + @if ( Step.EndTime.HasValue ) { +
Ended
+
@Step.EndTime.Value.ToString( "G" )
+ } + + @if ( Step.JobId.HasValue ) { +
Job
+
@Step.JobId.Value.ToString( )[..8]…
+ } + + @if ( !string.IsNullOrWhiteSpace( Step.ErrorMessage ) ) { +
Error
+
@Step.ErrorMessage
+ } + + @if ( !string.IsNullOrWhiteSpace( Step.SkipReason ) ) { +
Skip Reason
+
@Step.SkipReason
+ } +
+ + @if ( Job is not null && !string.IsNullOrWhiteSpace( Job.Output ) ) { +
Output Preview
+
@Job.Output
+ } + + @if ( Step.JobId.HasValue ) { + + @if ( _fullLog is not null ) { +
@_fullLog
+ } + } +
+
+} + +@code { + [Parameter] public StepExecutionDto? Step { get; set; } + [Parameter] public JobDto? Job { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + + private string? _fullLog; + private bool _fetchingLog; + + protected override void OnParametersSet( ) { + // Reset full log when step changes + _fullLog = null; + } + + private async Task FetchFullLogAsync( ) { + if ( Step?.JobId is null ) return; + _fetchingLog = true; + try { + HttpClient client = HttpClientFactory.CreateClient( "ApiService" ); + _fullLog = await client.GetStringAsync( $"/api/jobs/{Step.JobId}/output" ); + } catch { + _fullLog = "Failed to fetch log."; + } finally { + _fetchingLog = false; + } + } +} diff --git a/src/Werkr.Server/Components/Shared/StepPalette.razor b/src/Werkr.Server/Components/Shared/StepPalette.razor new file mode 100644 index 0000000..c9b73f7 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/StepPalette.razor @@ -0,0 +1,70 @@ +@rendermode InteractiveServer +@using Werkr.Common.Models.Actions + +
+
+
Step Palette
+ +
+ +
+ @foreach ( (string category, IReadOnlyList actions) in FilteredGroups ) { +
+ @category + @foreach ( ActionFormDescriptor action in actions ) { +
+ + @action.DisplayName +
+ } +
+ } +
+
+ +@code { + [Parameter] public EventCallback<(string ActionType, double ClientX, double ClientY)> OnPaletteDragStart { get; set; } + + private string _searchTerm = ""; + + private IEnumerable<(string Category, IReadOnlyList Actions)> FilteredGroups { + get { + if (string.IsNullOrWhiteSpace( _searchTerm )) { + return ActionRegistry.Grouped; + } + + string term = _searchTerm.Trim( ); + return ActionRegistry.Grouped + .Select( g => (g.Category, (IReadOnlyList) g.Actions + .Where( a => a.DisplayName.Contains( term, StringComparison.OrdinalIgnoreCase ) + || a.Category.Contains( term, StringComparison.OrdinalIgnoreCase ) ) + .ToList( )) ) + .Where( g => g.Item2.Count > 0 ); + } + } + + private async Task HandleDragStart( string actionType, MouseEventArgs e ) { + await OnPaletteDragStart.InvokeAsync( (actionType, e.ClientX, e.ClientY) ); + } + + private static string GetCategoryIcon( string category ) => category switch { + "File" => "bi-file-earmark", + "Network" => "bi-globe", + "Process" => "bi-terminal", + "Archive" => "bi-file-zip", + "Data" => "bi-database", + "Security" => "bi-shield-lock", + "System" => "bi-gear", + "Text" => "bi-file-text", + "Shell" => "bi-terminal-fill", + "PowerShell" => "bi-code-slash", + _ => "bi-puzzle" + }; +} diff --git a/src/Werkr.Server/Components/Shared/StepPalette.razor.css b/src/Werkr.Server/Components/Shared/StepPalette.razor.css new file mode 100644 index 0000000..2e37a97 --- /dev/null +++ b/src/Werkr.Server/Components/Shared/StepPalette.razor.css @@ -0,0 +1,21 @@ +.step-palette { + width: 240px; + min-width: 240px; + border-right: 1px solid var(--bs-border-color); + background-color: var(--bs-body-bg); +} + +.palette-item { + cursor: grab; + user-select: none; + transition: background-color 0.1s ease; +} + +.palette-item:hover { + background-color: var(--bs-tertiary-bg); +} + +.palette-item:active { + cursor: grabbing; + background-color: var(--bs-primary-bg-subtle); +} diff --git a/src/Werkr.Server/Components/Shared/TaskCreateModal.razor b/src/Werkr.Server/Components/Shared/TaskSetupModal.razor similarity index 66% rename from src/Werkr.Server/Components/Shared/TaskCreateModal.razor rename to src/Werkr.Server/Components/Shared/TaskSetupModal.razor index 0100cf1..04954bb 100644 --- a/src/Werkr.Server/Components/Shared/TaskCreateModal.razor +++ b/src/Werkr.Server/Components/Shared/TaskSetupModal.razor @@ -6,7 +6,7 @@
+ @onkeydown="( e ) => HandleCellKeyDown( e, run.RunId, step.Id )" + title="@status" + aria-label="@step.Name — @status"> @GetStatusIcon( status ) @@ -59,6 +61,12 @@ private static string GetCellStatus( RunGridEntry run, long stepId ) => run.StepStatuses.TryGetValue( stepId, out string? s ) ? s : "Pending"; + private async Task HandleCellKeyDown( KeyboardEventArgs e, Guid runId, long stepId ) { + if (e.Key is "Enter" or " ") { + await OnCellSelected.InvokeAsync( (runId, stepId) ); + } + } + private static string GetStatusIcon( string status ) => status switch { "Completed" => "✓", "Failed" => "✗", diff --git a/src/Werkr.Server/Program.cs b/src/Werkr.Server/Program.cs index 3c7db87..9bb29ee 100644 --- a/src/Werkr.Server/Program.cs +++ b/src/Werkr.Server/Program.cs @@ -141,9 +141,8 @@ public static async Task Main( string[] args ) { // Dedicated SSE client for the long-lived event stream consumed by // JobEventRelayService. The global ConfigureHttpClientDefaults in // ServiceDefaults adds a standard resilience pipeline (10s attempt timeout) - // to all clients. We must strip those handlers for the SSE client because - // they immediately kill long-lived event streams. The service manages its - // own reconnect loop with exponential backoff. + // to all clients. We strip those handlers for the SSE client and add back + // a correctly-configured pipeline with infinite timeouts. // Note: ResilienceHandler is a public type from Microsoft.Extensions.Http.Resilience. _ = builder.Services.AddHttpClient( "ApiServiceSse", client => { client.BaseAddress = new Uri( "https://api" ); @@ -151,12 +150,23 @@ public static async Task Main( string[] args ) { } ) .AddHttpMessageHandler( ) .ConfigureAdditionalHttpMessageHandlers( ( handlers, _ ) => { - // Strip all resilience handlers added by ConfigureHttpClientDefaults + // Remove the global default resilience pipeline (10s attempt timeout) + // added by ConfigureHttpClientDefaults — it kills long-lived SSE + // connections. We add back a correctly-configured pipeline below. + // IMPORTANT: This must run BEFORE AddStandardResilienceHandler so the + // strip removes the DEFAULT handler, not the one we're about to add. for (int i = handlers.Count - 1; i >= 0; i--) { if (handlers[i] is ResilienceHandler) { handlers.RemoveAt( i ); } } + } ) + .AddStandardResilienceHandler( options => { + // SSE streams are indefinitely long-lived; disable timeout-based + // cancellation. The JobEventRelayService manages its own reconnect + // loop with exponential backoff (1s → 30s) and exposes a health check. + options.AttemptTimeout.Timeout = Timeout.InfiniteTimeSpan; + options.TotalRequestTimeout.Timeout = Timeout.InfiniteTimeSpan; } ); // Background health monitor — keeps agent DB status in sync with actual reachability From 9b0e73446a87eefd082435f25dd5e7e44c3140cc Mon Sep 17 00:00:00 2001 From: Taylor Marvin Date: Sat, 14 Mar 2026 22:56:38 -0700 Subject: [PATCH 07/11] Address PR concerns x3 --- .../Unit/Endpoints/FilterEndpointTests.cs | 508 +++++++++--------- src/Werkr.Api/Endpoints/FilterEndpoints.cs | 498 ++++++++--------- .../Services/JobReportingGrpcService.cs | 16 +- .../Services/OutputStreamingGrpcService.cs | 37 +- src/Werkr.Api/Services/VariableGrpcService.cs | 6 +- .../Components/Pages/Jobs/Index.razor | 13 +- .../Components/Pages/Workflows/AllRuns.razor | 9 +- .../Components/Pages/Workflows/Runs.razor | 9 +- .../Components/Shared/EditorToolbar.razor | 21 +- .../Components/Shared/RunGridView.razor | 1 + src/Werkr.Server/Helpers/FilterHelper.cs | 34 ++ 11 files changed, 597 insertions(+), 555 deletions(-) create mode 100644 src/Werkr.Server/Helpers/FilterHelper.cs diff --git a/src/Test/Werkr.Tests.Data/Unit/Endpoints/FilterEndpointTests.cs b/src/Test/Werkr.Tests.Data/Unit/Endpoints/FilterEndpointTests.cs index 4add360..e7ffc0c 100644 --- a/src/Test/Werkr.Tests.Data/Unit/Endpoints/FilterEndpointTests.cs +++ b/src/Test/Werkr.Tests.Data/Unit/Endpoints/FilterEndpointTests.cs @@ -1,254 +1,254 @@ -using System.Reflection; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Werkr.Data; -using Werkr.Data.Entities.Settings; - -namespace Werkr.Tests.Data.Unit.Endpoints; - -/// -/// Unit tests for the FilterEndpoints class, validating page key allowlist completeness -/// and that filter CRUD operations enforce ownership and produce correct persistence results. -/// -[TestClass] -public class FilterEndpointTests { - /// - /// The in-memory SQLite connection used for database operations. - /// - private SqliteConnection _connection = null!; - /// - /// The SQLite-backed used for test data persistence. - /// - private SqliteWerkrDbContext _dbContext = null!; - - /// - /// Gets or sets the MSTest providing per-test cancellation tokens and metadata. - /// - public TestContext TestContext { get; set; } = null!; - - /// - /// The complete set of valid page keys that the filter endpoints must accept. - /// - private static readonly HashSet s_expectedPageKeys = [ - "runs", "workflows", "jobs", "agents", "schedules", "tasks", - "all-workflow-runs", "workflow-dashboard" - ]; - - /// - /// Creates an in-memory SQLite database and the schema for each test. - /// - [TestInitialize] - public void TestInit( ) { - _connection = new SqliteConnection( "DataSource=:memory:" ); - _connection.Open( ); - - DbContextOptions options = new DbContextOptionsBuilder( ) - .UseSqlite( _connection ) - .Options; - - _dbContext = new SqliteWerkrDbContext( options ); - _ = _dbContext.Database.EnsureCreated( ); - } - - /// - /// Disposes the database context and SQLite connection after each test. - /// - [TestCleanup] - public void TestCleanup( ) { - _dbContext?.Dispose( ); - _connection?.Dispose( ); - } - - /// - /// Verifies that the s_validPageKeys field on FilterEndpoints contains exactly the - /// expected set of page keys including all-workflow-runs and workflow-dashboard. - /// - [TestMethod] - public void ValidPageKeys_ContainsAllExpectedKeys( ) { - Assembly apiAssembly = Assembly.Load( "Werkr.Api" ); - Type? endpointsType = apiAssembly.GetType( "Werkr.Api.Endpoints.FilterEndpoints" ); - Assert.IsNotNull( endpointsType, "FilterEndpoints type not found in Werkr.Api assembly" ); - - FieldInfo? field = endpointsType.GetField( - "s_validPageKeys", - BindingFlags.NonPublic | BindingFlags.Static - ); - - Assert.IsNotNull( field, "s_validPageKeys field not found on FilterEndpoints" ); - - object? value = field.GetValue( null ); - _ = Assert.IsInstanceOfType>( value ); - - HashSet actualKeys = (HashSet)value; - - foreach (string expected in s_expectedPageKeys) { - Assert.Contains( - expected, actualKeys, - $"Missing page key: '{expected}'" - ); - } - - Assert.HasCount( - s_expectedPageKeys.Count, - actualKeys, - $"Page key count mismatch. Expected: {s_expectedPageKeys.Count}, Actual: {actualKeys.Count}" - ); - } - - /// - /// Verifies that creating a filter persists the entity with the correct owner and page key. - /// - [TestMethod] - public async Task CreateFilter_PersistsWithCorrectOwnerAndPageKey( ) { - CancellationToken ct = TestContext.CancellationToken; - string userId = "user-1"; - - SavedFilter entity = new( ) { - OwnerId = userId, - PageKey = "runs", - Name = "My Filter", - CriteriaJson = "{\"status\":\"Running\"}", - IsShared = false, - Created = DateTime.UtcNow, - LastUpdated = DateTime.UtcNow, - Version = 1, - }; - - _ = _dbContext.SavedFilters.Add( entity ); - _ = await _dbContext.SaveChangesAsync( ct ); - - SavedFilter? loaded = await _dbContext.SavedFilters - .FirstOrDefaultAsync( f => f.OwnerId == userId && f.PageKey == "runs", ct ); - - Assert.IsNotNull( loaded ); - Assert.AreEqual( "My Filter", loaded.Name ); - Assert.AreEqual( "{\"status\":\"Running\"}", loaded.CriteriaJson ); - Assert.IsFalse( loaded.IsShared ); - } - - /// - /// Verifies that filters can be queried by page key and include both owned and shared filters. - /// - [TestMethod] - public async Task QueryFilters_ReturnsBothOwnedAndShared( ) { - CancellationToken ct = TestContext.CancellationToken; - string userId = "user-1"; - - SavedFilter owned = new( ) { - OwnerId = userId, - PageKey = "runs", - Name = "My Filter", - CriteriaJson = "{}", - Created = DateTime.UtcNow, - LastUpdated = DateTime.UtcNow, - Version = 1, - }; - - SavedFilter shared = new( ) { - OwnerId = "other-user", - PageKey = "runs", - Name = "Shared Filter", - CriteriaJson = "{}", - IsShared = true, - Created = DateTime.UtcNow, - LastUpdated = DateTime.UtcNow, - Version = 1, - }; - - SavedFilter differentPage = new( ) { - OwnerId = userId, - PageKey = "jobs", - Name = "Jobs Filter", - CriteriaJson = "{}", - Created = DateTime.UtcNow, - LastUpdated = DateTime.UtcNow, - Version = 1, - }; - - _dbContext.SavedFilters.AddRange( owned, shared, differentPage ); - _ = await _dbContext.SaveChangesAsync( ct ); - - List results = await _dbContext.SavedFilters - .Where( f => f.PageKey == "runs" && (f.OwnerId == userId || f.IsShared) ) - .OrderBy( f => f.Name ) - .ToListAsync( ct ); - - Assert.HasCount( 2, results ); - Assert.AreEqual( "My Filter", results[0].Name ); - Assert.AreEqual( "Shared Filter", results[1].Name ); - } - - /// - /// Verifies that deleting a filter only removes the targeted entity. - /// - [TestMethod] - public async Task DeleteFilter_RemovesOnlyTargetEntity( ) { - CancellationToken ct = TestContext.CancellationToken; - - SavedFilter filter1 = new( ) { - OwnerId = "user-1", - PageKey = "runs", - Name = "Filter 1", - CriteriaJson = "{}", - Created = DateTime.UtcNow, - LastUpdated = DateTime.UtcNow, - Version = 1, - }; - - SavedFilter filter2 = new( ) { - OwnerId = "user-1", - PageKey = "runs", - Name = "Filter 2", - CriteriaJson = "{}", - Created = DateTime.UtcNow, - LastUpdated = DateTime.UtcNow, - Version = 1, - }; - - _dbContext.SavedFilters.AddRange( filter1, filter2 ); - _ = await _dbContext.SaveChangesAsync( ct ); - - _ = _dbContext.SavedFilters.Remove( filter1 ); - _ = await _dbContext.SaveChangesAsync( ct ); - - List remaining = await _dbContext.SavedFilters.ToListAsync( ct ); - - Assert.HasCount( 1, remaining ); - Assert.AreEqual( "Filter 2", remaining[0].Name ); - } - - /// - /// Verifies that updating a filter increments the version and persists the new values. - /// - [TestMethod] - public async Task UpdateFilter_IncrementsVersionAndPersists( ) { - CancellationToken ct = TestContext.CancellationToken; - - SavedFilter entity = new( ) { - OwnerId = "user-1", - PageKey = "runs", - Name = "Original", - CriteriaJson = "{}", - Created = DateTime.UtcNow, - LastUpdated = DateTime.UtcNow, - Version = 1, - }; - - _ = _dbContext.SavedFilters.Add( entity ); - _ = await _dbContext.SaveChangesAsync( ct ); - - entity.Name = "Updated"; - entity.CriteriaJson = "{\"status\":\"Failed\"}"; - entity.Version++; - entity.LastUpdated = DateTime.UtcNow; - _ = await _dbContext.SaveChangesAsync( ct ); - - SavedFilter? loaded = await _dbContext.SavedFilters - .AsNoTracking( ) - .FirstOrDefaultAsync( f => f.Id == entity.Id, ct ); - - Assert.IsNotNull( loaded ); - Assert.AreEqual( "Updated", loaded.Name ); - Assert.AreEqual( "{\"status\":\"Failed\"}", loaded.CriteriaJson ); - } -} +using System.Reflection; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Werkr.Data; +using Werkr.Data.Entities.Settings; + +namespace Werkr.Tests.Data.Unit.Endpoints; + +/// +/// Unit tests for the FilterEndpoints class, validating page key allowlist completeness +/// and that filter CRUD operations enforce ownership and produce correct persistence results. +/// +[TestClass] +public class FilterEndpointTests { + /// + /// The in-memory SQLite connection used for database operations. + /// + private SqliteConnection _connection = null!; + /// + /// The SQLite-backed used for test data persistence. + /// + private SqliteWerkrDbContext _dbContext = null!; + + /// + /// Gets or sets the MSTest providing per-test cancellation tokens and metadata. + /// + public TestContext TestContext { get; set; } = null!; + + /// + /// The complete set of valid page keys that the filter endpoints must accept. + /// + private static readonly HashSet s_expectedPageKeys = [ + "runs", "workflows", "jobs", "agents", "schedules", "tasks", + "all-workflow-runs", "workflow-dashboard" + ]; + + /// + /// Creates an in-memory SQLite database and the schema for each test. + /// + [TestInitialize] + public void TestInit( ) { + _connection = new SqliteConnection( "DataSource=:memory:" ); + _connection.Open( ); + + DbContextOptions options = new DbContextOptionsBuilder( ) + .UseSqlite( _connection ) + .Options; + + _dbContext = new SqliteWerkrDbContext( options ); + _ = _dbContext.Database.EnsureCreated( ); + } + + /// + /// Disposes the database context and SQLite connection after each test. + /// + [TestCleanup] + public void TestCleanup( ) { + _dbContext?.Dispose( ); + _connection?.Dispose( ); + } + + /// + /// Verifies that the s_validPageKeys field on FilterEndpoints contains exactly the + /// expected set of page keys including all-workflow-runs and workflow-dashboard. + /// + [TestMethod] + public void ValidPageKeys_ContainsAllExpectedKeys( ) { + Assembly apiAssembly = Assembly.Load( "Werkr.Api" ); + Type? endpointsType = apiAssembly.GetType( "Werkr.Api.Endpoints.FilterEndpoints" ); + Assert.IsNotNull( endpointsType, "FilterEndpoints type not found in Werkr.Api assembly" ); + + FieldInfo? field = endpointsType.GetField( + "s_validPageKeys", + BindingFlags.NonPublic | BindingFlags.Static + ); + + Assert.IsNotNull( field, "s_validPageKeys field not found on FilterEndpoints" ); + + object? value = field.GetValue( null ); + _ = Assert.IsInstanceOfType>( value ); + + HashSet actualKeys = (HashSet)value; + + foreach (string expected in s_expectedPageKeys) { + Assert.Contains( + expected, actualKeys, + $"Missing page key: '{expected}'" + ); + } + + Assert.HasCount( + s_expectedPageKeys.Count, + actualKeys, + $"Page key count mismatch. Expected: {s_expectedPageKeys.Count}, Actual: {actualKeys.Count}" + ); + } + + /// + /// Verifies that creating a filter persists the entity with the correct owner and page key. + /// + [TestMethod] + public async Task CreateFilter_PersistsWithCorrectOwnerAndPageKey( ) { + CancellationToken ct = TestContext.CancellationToken; + string userId = "user-1"; + + SavedFilter entity = new( ) { + OwnerId = userId, + PageKey = "runs", + Name = "My Filter", + CriteriaJson = "{\"status\":\"Running\"}", + IsShared = false, + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + _ = _dbContext.SavedFilters.Add( entity ); + _ = await _dbContext.SaveChangesAsync( ct ); + + SavedFilter? loaded = await _dbContext.SavedFilters + .FirstOrDefaultAsync( f => f.OwnerId == userId && f.PageKey == "runs", ct ); + + Assert.IsNotNull( loaded ); + Assert.AreEqual( "My Filter", loaded.Name ); + Assert.AreEqual( "{\"status\":\"Running\"}", loaded.CriteriaJson ); + Assert.IsFalse( loaded.IsShared ); + } + + /// + /// Verifies that filters can be queried by page key and include both owned and shared filters. + /// + [TestMethod] + public async Task QueryFilters_ReturnsBothOwnedAndShared( ) { + CancellationToken ct = TestContext.CancellationToken; + string userId = "user-1"; + + SavedFilter owned = new( ) { + OwnerId = userId, + PageKey = "runs", + Name = "My Filter", + CriteriaJson = "{}", + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + SavedFilter shared = new( ) { + OwnerId = "other-user", + PageKey = "runs", + Name = "Shared Filter", + CriteriaJson = "{}", + IsShared = true, + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + SavedFilter differentPage = new( ) { + OwnerId = userId, + PageKey = "jobs", + Name = "Jobs Filter", + CriteriaJson = "{}", + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + _dbContext.SavedFilters.AddRange( owned, shared, differentPage ); + _ = await _dbContext.SaveChangesAsync( ct ); + + List results = await _dbContext.SavedFilters + .Where( f => f.PageKey == "runs" && (f.OwnerId == userId || f.IsShared) ) + .OrderBy( f => f.Name ) + .ToListAsync( ct ); + + Assert.HasCount( 2, results ); + Assert.AreEqual( "My Filter", results[0].Name ); + Assert.AreEqual( "Shared Filter", results[1].Name ); + } + + /// + /// Verifies that deleting a filter only removes the targeted entity. + /// + [TestMethod] + public async Task DeleteFilter_RemovesOnlyTargetEntity( ) { + CancellationToken ct = TestContext.CancellationToken; + + SavedFilter filter1 = new( ) { + OwnerId = "user-1", + PageKey = "runs", + Name = "Filter 1", + CriteriaJson = "{}", + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + SavedFilter filter2 = new( ) { + OwnerId = "user-1", + PageKey = "runs", + Name = "Filter 2", + CriteriaJson = "{}", + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + _dbContext.SavedFilters.AddRange( filter1, filter2 ); + _ = await _dbContext.SaveChangesAsync( ct ); + + _ = _dbContext.SavedFilters.Remove( filter1 ); + _ = await _dbContext.SaveChangesAsync( ct ); + + List remaining = await _dbContext.SavedFilters.ToListAsync( ct ); + + Assert.HasCount( 1, remaining ); + Assert.AreEqual( "Filter 2", remaining[0].Name ); + } + + /// + /// Verifies that updating a filter increments the version and persists the new values. + /// + [TestMethod] + public async Task UpdateFilter_IncrementsVersionAndPersists( ) { + CancellationToken ct = TestContext.CancellationToken; + + SavedFilter entity = new( ) { + OwnerId = "user-1", + PageKey = "runs", + Name = "Original", + CriteriaJson = "{}", + Created = DateTime.UtcNow, + LastUpdated = DateTime.UtcNow, + Version = 1, + }; + + _ = _dbContext.SavedFilters.Add( entity ); + _ = await _dbContext.SaveChangesAsync( ct ); + + entity.Name = "Updated"; + entity.CriteriaJson = "{\"status\":\"Failed\"}"; + entity.Version++; + entity.LastUpdated = DateTime.UtcNow; + _ = await _dbContext.SaveChangesAsync( ct ); + + SavedFilter? loaded = await _dbContext.SavedFilters + .AsNoTracking( ) + .FirstOrDefaultAsync( f => f.Id == entity.Id, ct ); + + Assert.IsNotNull( loaded ); + Assert.AreEqual( "Updated", loaded.Name ); + Assert.AreEqual( "{\"status\":\"Failed\"}", loaded.CriteriaJson ); + } +} diff --git a/src/Werkr.Api/Endpoints/FilterEndpoints.cs b/src/Werkr.Api/Endpoints/FilterEndpoints.cs index f5664ef..044e74a 100644 --- a/src/Werkr.Api/Endpoints/FilterEndpoints.cs +++ b/src/Werkr.Api/Endpoints/FilterEndpoints.cs @@ -1,249 +1,249 @@ -using System.Security.Claims; -using System.Text.Json; -using Microsoft.EntityFrameworkCore; -using Werkr.Common.Auth; -using Werkr.Data; -using Werkr.Data.Entities.Settings; - -namespace Werkr.Api.Endpoints; - -/// Maps CRUD endpoints for server-synced saved filters. -internal static class FilterEndpoints { - - private static readonly HashSet s_validPageKeys = [ - "runs", "workflows", "jobs", "agents", "schedules", "tasks", - "all-workflow-runs", "workflow-dashboard" - ]; - - /// Maps the saved-filter endpoints at /api/filters/{pageKey}. - public static WebApplication MapFilterEndpoints( this WebApplication app ) { - - // GET /api/filters/{pageKey} — list own + shared filters - _ = app.MapGet( "/api/filters/{pageKey}", async ( - string pageKey, - HttpContext httpContext, - WerkrDbContext dbContext, - CancellationToken ct - ) => { - if (!s_validPageKeys.Contains( pageKey )) { - return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); - } - - string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; - if (string.IsNullOrWhiteSpace( userId )) { - return Results.Unauthorized( ); - } - - List filters = await dbContext.SavedFilters - .AsNoTracking( ) - .Where( f => f.PageKey == pageKey && ( f.OwnerId == userId || f.IsShared ) ) - .OrderBy( f => f.Name ) - .ToListAsync( ct ); - - List dtos = [.. filters.Select( f => new { - f.Id, - f.Name, - f.PageKey, - f.CriteriaJson, - f.IsShared, - IsOwner = f.OwnerId == userId - } )]; - - return Results.Ok( dtos ); - } ) - .RequireAuthorization( Policies.CanRead ); - - // POST /api/filters/{pageKey} — create a filter - _ = app.MapPost( "/api/filters/{pageKey}", async ( - string pageKey, - CreateFilterRequest request, - HttpContext httpContext, - WerkrDbContext dbContext, - CancellationToken ct - ) => { - if (!s_validPageKeys.Contains( pageKey )) { - return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); - } - - string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; - if (string.IsNullOrWhiteSpace( userId )) { - return Results.Unauthorized( ); - } - - if (string.IsNullOrWhiteSpace( request.Name )) { - return Results.BadRequest( new { message = "Name is required." } ); - } - - if (!IsValidJson( request.CriteriaJson )) { - return Results.BadRequest( new { message = "CriteriaJson must be valid JSON." } ); - } - - if (request.CriteriaJson.Length > 4096) { - return Results.BadRequest( new { message = "CriteriaJson must not exceed 4 KB." } ); - } - - DateTime now = DateTime.UtcNow; - SavedFilter entity = new( ) { - OwnerId = userId, - PageKey = pageKey, - Name = request.Name.Trim( ), - CriteriaJson = request.CriteriaJson, - IsShared = false, - Created = now, - LastUpdated = now, - Version = 1, - }; - - _ = dbContext.SavedFilters.Add( entity ); - _ = await dbContext.SaveChangesAsync( ct ); - - return Results.Created( $"/api/filters/{pageKey}/{entity.Id}", new { - entity.Id, - entity.Name, - entity.PageKey, - entity.CriteriaJson, - entity.IsShared, - IsOwner = true - } ); - } ) - .RequireAuthorization( Policies.CanCreate ); - - // PUT /api/filters/{pageKey}/{id} — update own filter - _ = app.MapPut( "/api/filters/{pageKey}/{id}", async ( - string pageKey, - long id, - UpdateFilterRequest request, - HttpContext httpContext, - WerkrDbContext dbContext, - CancellationToken ct - ) => { - if (!s_validPageKeys.Contains( pageKey )) { - return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); - } - - string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; - if (string.IsNullOrWhiteSpace( userId )) { - return Results.Unauthorized( ); - } - - SavedFilter? entity = await dbContext.SavedFilters - .FirstOrDefaultAsync( f => f.Id == id && f.PageKey == pageKey, ct ); - - if (entity is null) { - return Results.NotFound( ); - } - - if (entity.OwnerId != userId) { - return Results.Forbid( ); - } - - if (!string.IsNullOrWhiteSpace( request.Name )) { - entity.Name = request.Name.Trim( ); - } - - if (request.CriteriaJson is not null) { - if (!IsValidJson( request.CriteriaJson )) { - return Results.BadRequest( new { message = "CriteriaJson must be valid JSON." } ); - } - if (request.CriteriaJson.Length > 4096) { - return Results.BadRequest( new { message = "CriteriaJson must not exceed 4 KB." } ); - } - entity.CriteriaJson = request.CriteriaJson; - } - - entity.LastUpdated = DateTime.UtcNow; - entity.Version++; - _ = await dbContext.SaveChangesAsync( ct ); - - return Results.Ok( new { - entity.Id, - entity.Name, - entity.PageKey, - entity.CriteriaJson, - entity.IsShared, - IsOwner = true - } ); - } ) - .RequireAuthorization( Policies.CanUpdate ); - - // DELETE /api/filters/{pageKey}/{id} — delete own filter - _ = app.MapDelete( "/api/filters/{pageKey}/{id}", async ( - string pageKey, - long id, - HttpContext httpContext, - WerkrDbContext dbContext, - CancellationToken ct - ) => { - if (!s_validPageKeys.Contains( pageKey )) { - return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); - } - - string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; - if (string.IsNullOrWhiteSpace( userId )) { - return Results.Unauthorized( ); - } - - SavedFilter? entity = await dbContext.SavedFilters - .FirstOrDefaultAsync( f => f.Id == id && f.PageKey == pageKey, ct ); - - if (entity is null) { - return Results.NotFound( ); - } - - if (entity.OwnerId != userId) { - return Results.Forbid( ); - } - - _ = dbContext.SavedFilters.Remove( entity ); - _ = await dbContext.SaveChangesAsync( ct ); - - return Results.NoContent( ); - } ) - .RequireAuthorization( Policies.CanDelete ); - - // PUT /api/filters/{pageKey}/{id}/share — toggle shared visibility (admin only) - _ = app.MapPut( "/api/filters/{pageKey}/{id}/share", async ( - string pageKey, - long id, - HttpContext httpContext, - WerkrDbContext dbContext, - CancellationToken ct - ) => { - if (!s_validPageKeys.Contains( pageKey )) { - return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); - } - - SavedFilter? entity = await dbContext.SavedFilters - .FirstOrDefaultAsync( f => f.Id == id && f.PageKey == pageKey, ct ); - - if (entity is null) { - return Results.NotFound( ); - } - - entity.IsShared = !entity.IsShared; - entity.LastUpdated = DateTime.UtcNow; - entity.Version++; - _ = await dbContext.SaveChangesAsync( ct ); - - return Results.Ok( new { entity.Id, entity.IsShared } ); - } ) - .RequireAuthorization( Policies.IsAdmin ); - - return app; - } - - private static bool IsValidJson( string json ) { - try { - using JsonDocument doc = JsonDocument.Parse( json ); - return true; - } catch (JsonException) { - return false; - } - } -} - -/// Request body for creating a saved filter. -internal sealed record CreateFilterRequest( string Name, string CriteriaJson ); - -/// Request body for updating a saved filter. -internal sealed record UpdateFilterRequest( string? Name, string? CriteriaJson ); +using System.Security.Claims; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Werkr.Common.Auth; +using Werkr.Data; +using Werkr.Data.Entities.Settings; + +namespace Werkr.Api.Endpoints; + +/// Maps CRUD endpoints for server-synced saved filters. +internal static class FilterEndpoints { + + private static readonly HashSet s_validPageKeys = [ + "runs", "workflows", "jobs", "agents", "schedules", "tasks", + "all-workflow-runs", "workflow-dashboard" + ]; + + /// Maps the saved-filter endpoints at /api/filters/{pageKey}. + public static WebApplication MapFilterEndpoints( this WebApplication app ) { + + // GET /api/filters/{pageKey} — list own + shared filters + _ = app.MapGet( "/api/filters/{pageKey}", async ( + string pageKey, + HttpContext httpContext, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + if (!s_validPageKeys.Contains( pageKey )) { + return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); + } + + string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; + if (string.IsNullOrWhiteSpace( userId )) { + return Results.Unauthorized( ); + } + + List filters = await dbContext.SavedFilters + .AsNoTracking( ) + .Where( f => f.PageKey == pageKey && ( f.OwnerId == userId || f.IsShared ) ) + .OrderBy( f => f.Name ) + .ToListAsync( ct ); + + List dtos = [.. filters.Select( f => new { + f.Id, + f.Name, + f.PageKey, + f.CriteriaJson, + f.IsShared, + IsOwner = f.OwnerId == userId + } )]; + + return Results.Ok( dtos ); + } ) + .RequireAuthorization( Policies.CanRead ); + + // POST /api/filters/{pageKey} — create a filter + _ = app.MapPost( "/api/filters/{pageKey}", async ( + string pageKey, + CreateFilterRequest request, + HttpContext httpContext, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + if (!s_validPageKeys.Contains( pageKey )) { + return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); + } + + string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; + if (string.IsNullOrWhiteSpace( userId )) { + return Results.Unauthorized( ); + } + + if (string.IsNullOrWhiteSpace( request.Name )) { + return Results.BadRequest( new { message = "Name is required." } ); + } + + if (!IsValidJson( request.CriteriaJson )) { + return Results.BadRequest( new { message = "CriteriaJson must be valid JSON." } ); + } + + if (request.CriteriaJson.Length > 4096) { + return Results.BadRequest( new { message = "CriteriaJson must not exceed 4 KB." } ); + } + + DateTime now = DateTime.UtcNow; + SavedFilter entity = new( ) { + OwnerId = userId, + PageKey = pageKey, + Name = request.Name.Trim( ), + CriteriaJson = request.CriteriaJson, + IsShared = false, + Created = now, + LastUpdated = now, + Version = 1, + }; + + _ = dbContext.SavedFilters.Add( entity ); + _ = await dbContext.SaveChangesAsync( ct ); + + return Results.Created( $"/api/filters/{pageKey}/{entity.Id}", new { + entity.Id, + entity.Name, + entity.PageKey, + entity.CriteriaJson, + entity.IsShared, + IsOwner = true + } ); + } ) + .RequireAuthorization( Policies.CanCreate ); + + // PUT /api/filters/{pageKey}/{id} — update own filter + _ = app.MapPut( "/api/filters/{pageKey}/{id}", async ( + string pageKey, + long id, + UpdateFilterRequest request, + HttpContext httpContext, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + if (!s_validPageKeys.Contains( pageKey )) { + return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); + } + + string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; + if (string.IsNullOrWhiteSpace( userId )) { + return Results.Unauthorized( ); + } + + SavedFilter? entity = await dbContext.SavedFilters + .FirstOrDefaultAsync( f => f.Id == id && f.PageKey == pageKey, ct ); + + if (entity is null) { + return Results.NotFound( ); + } + + if (entity.OwnerId != userId) { + return Results.Forbid( ); + } + + if (!string.IsNullOrWhiteSpace( request.Name )) { + entity.Name = request.Name.Trim( ); + } + + if (request.CriteriaJson is not null) { + if (!IsValidJson( request.CriteriaJson )) { + return Results.BadRequest( new { message = "CriteriaJson must be valid JSON." } ); + } + if (request.CriteriaJson.Length > 4096) { + return Results.BadRequest( new { message = "CriteriaJson must not exceed 4 KB." } ); + } + entity.CriteriaJson = request.CriteriaJson; + } + + entity.LastUpdated = DateTime.UtcNow; + entity.Version++; + _ = await dbContext.SaveChangesAsync( ct ); + + return Results.Ok( new { + entity.Id, + entity.Name, + entity.PageKey, + entity.CriteriaJson, + entity.IsShared, + IsOwner = true + } ); + } ) + .RequireAuthorization( Policies.CanUpdate ); + + // DELETE /api/filters/{pageKey}/{id} — delete own filter + _ = app.MapDelete( "/api/filters/{pageKey}/{id}", async ( + string pageKey, + long id, + HttpContext httpContext, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + if (!s_validPageKeys.Contains( pageKey )) { + return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); + } + + string? userId = httpContext.User.FindFirst( ClaimTypes.NameIdentifier )?.Value; + if (string.IsNullOrWhiteSpace( userId )) { + return Results.Unauthorized( ); + } + + SavedFilter? entity = await dbContext.SavedFilters + .FirstOrDefaultAsync( f => f.Id == id && f.PageKey == pageKey, ct ); + + if (entity is null) { + return Results.NotFound( ); + } + + if (entity.OwnerId != userId) { + return Results.Forbid( ); + } + + _ = dbContext.SavedFilters.Remove( entity ); + _ = await dbContext.SaveChangesAsync( ct ); + + return Results.NoContent( ); + } ) + .RequireAuthorization( Policies.CanDelete ); + + // PUT /api/filters/{pageKey}/{id}/share — toggle shared visibility (admin only) + _ = app.MapPut( "/api/filters/{pageKey}/{id}/share", async ( + string pageKey, + long id, + HttpContext httpContext, + WerkrDbContext dbContext, + CancellationToken ct + ) => { + if (!s_validPageKeys.Contains( pageKey )) { + return Results.BadRequest( new { message = $"Invalid page key: {pageKey}" } ); + } + + SavedFilter? entity = await dbContext.SavedFilters + .FirstOrDefaultAsync( f => f.Id == id && f.PageKey == pageKey, ct ); + + if (entity is null) { + return Results.NotFound( ); + } + + entity.IsShared = !entity.IsShared; + entity.LastUpdated = DateTime.UtcNow; + entity.Version++; + _ = await dbContext.SaveChangesAsync( ct ); + + return Results.Ok( new { entity.Id, entity.IsShared } ); + } ) + .RequireAuthorization( Policies.IsAdmin ); + + return app; + } + + private static bool IsValidJson( string json ) { + try { + using JsonDocument doc = JsonDocument.Parse( json ); + return true; + } catch (JsonException) { + return false; + } + } +} + +/// Request body for creating a saved filter. +internal sealed record CreateFilterRequest( string Name, string CriteriaJson ); + +/// Request body for updating a saved filter. +internal sealed record UpdateFilterRequest( string? Name, string? CriteriaJson ); diff --git a/src/Werkr.Api/Services/JobReportingGrpcService.cs b/src/Werkr.Api/Services/JobReportingGrpcService.cs index 617573a..1df3da2 100644 --- a/src/Werkr.Api/Services/JobReportingGrpcService.cs +++ b/src/Werkr.Api/Services/JobReportingGrpcService.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Grpc.Core; using Microsoft.EntityFrameworkCore; using Werkr.Common.Models; @@ -58,11 +59,13 @@ ServerCallContext context : ErrorCategory.Unknown; // Parse timestamps - DateTime startTime = DateTime.TryParse( inner.StartTime, out DateTime st ) - ? DateTime.SpecifyKind( st, DateTimeKind.Utc ) + DateTime startTime = DateTime.TryParse(inner.StartTime, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime st) + ? st : DateTime.UtcNow; - DateTime? endTime = DateTime.TryParse( inner.EndTime, out DateTime et ) - ? DateTime.SpecifyKind( et, DateTimeKind.Utc ) + DateTime? endTime = DateTime.TryParse(inner.EndTime, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime et) + ? et : null; // Parse workflow run ID if provided @@ -240,8 +243,9 @@ ServerCallContext context throw new RpcException( new Status( StatusCode.InvalidArgument, "Invalid workflow_run_id." ) ); } - DateTime startTime = DateTime.TryParse( inner.StartTime, out DateTime st ) - ? DateTime.SpecifyKind( st, DateTimeKind.Utc ) + DateTime startTime = DateTime.TryParse(inner.StartTime, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime st) + ? st : DateTime.UtcNow; // Determine attempt number (previous max + 1 for retries) diff --git a/src/Werkr.Api/Services/OutputStreamingGrpcService.cs b/src/Werkr.Api/Services/OutputStreamingGrpcService.cs index 8f50511..1af59ab 100644 --- a/src/Werkr.Api/Services/OutputStreamingGrpcService.cs +++ b/src/Werkr.Api/Services/OutputStreamingGrpcService.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics; using System.Threading.Channels; using Grpc.Core; using Werkr.Common.Protos; @@ -51,25 +52,39 @@ private sealed class RunLogRateState { /// Maximum log events published per second per run. private const int MaxEventsPerSecond = 50; - private static readonly long s_tickInterval = TimeSpan.TicksPerSecond / MaxEventsPerSecond; + private static readonly long s_tickInterval = Stopwatch.Frequency / MaxEventsPerSecond; /// /// Attempts to acquire a publish permit. Returns true if within rate limit. /// On true after drops, returns the count of dropped lines for batching notice. + /// Uses a CAS loop with monotonic timestamps for + /// atomic, drift-free rate limiting under concurrency. /// public bool TryAcquire( out int droppedSinceLastPublish ) { - long nowTicks = DateTime.UtcNow.Ticks; - long last = Interlocked.Read( ref _lastPublishTicks ); + long nowTicks = Stopwatch.GetTimestamp(); - if (nowTicks - last >= s_tickInterval) { - _ = Interlocked.Exchange( ref _lastPublishTicks, nowTicks ); - droppedSinceLastPublish = Interlocked.Exchange( ref _droppedCount, 0 ); - return true; - } + while (true) + { + long last = Interlocked.Read(ref _lastPublishTicks); + + if (nowTicks - last < s_tickInterval) + { + _ = Interlocked.Increment(ref _droppedCount); + droppedSinceLastPublish = 0; + return false; + } + + long original = Interlocked.CompareExchange( + ref _lastPublishTicks, nowTicks, last); - _ = Interlocked.Increment( ref _droppedCount ); - droppedSinceLastPublish = 0; - return false; + if (original == last) + { + droppedSinceLastPublish = Interlocked.Exchange(ref _droppedCount, 0); + return true; + } + + nowTicks = Stopwatch.GetTimestamp(); + } } } diff --git a/src/Werkr.Api/Services/VariableGrpcService.cs b/src/Werkr.Api/Services/VariableGrpcService.cs index adc6791..26153b3 100644 --- a/src/Werkr.Api/Services/VariableGrpcService.cs +++ b/src/Werkr.Api/Services/VariableGrpcService.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Grpc.Core; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -259,8 +260,9 @@ ServerCallContext context throw new RpcException( new Status( StatusCode.InvalidArgument, "Invalid workflow_run_id." ) ); } - DateTime endTime = DateTime.TryParse( inner.EndTime, out DateTime et ) - ? DateTime.SpecifyKind( et, DateTimeKind.Utc ) + DateTime endTime = DateTime.TryParse(inner.EndTime, CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime et) + ? et : DateTime.UtcNow; WorkflowRun? run = await dbContext.WorkflowRuns diff --git a/src/Werkr.Server/Components/Pages/Jobs/Index.razor b/src/Werkr.Server/Components/Pages/Jobs/Index.razor index 55ef538..2249a46 100644 --- a/src/Werkr.Server/Components/Pages/Jobs/Index.razor +++ b/src/Werkr.Server/Components/Pages/Jobs/Index.razor @@ -1,6 +1,5 @@ @page "/jobs" @rendermode InteractiveServer -@using System.Globalization @using Werkr.Common.Models @attribute [Authorize] @inject IHttpClientFactory HttpClientFactory @@ -133,13 +132,13 @@ else if ( string.Equals( statusFilter, "Failed", StringComparison.OrdinalIgnoreCase ) ) queryParams.Add( "success=false" ); - string? sinceStr = _activeCriteria?.Get( "since" ); - if ( DateTime.TryParseExact( sinceStr, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime since ) ) - queryParams.Add( $"since={since:O}" ); + DateTime? since = FilterHelper.TryParseFilterDate( _activeCriteria?.Get( "since" ) ); + if ( since.HasValue ) + queryParams.Add( $"since={since.Value:O}" ); - string? untilStr = _activeCriteria?.Get( "until" ); - if ( DateTime.TryParseExact( untilStr, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime until ) ) - queryParams.Add( $"until={until:O}" ); + DateTime? until = FilterHelper.TryParseFilterDate( _activeCriteria?.Get( "until" ) ); + if ( until.HasValue ) + queryParams.Add( $"until={until.Value:O}" ); return $"/api/jobs?{string.Join( "&", queryParams )}"; } diff --git a/src/Werkr.Server/Components/Pages/Workflows/AllRuns.razor b/src/Werkr.Server/Components/Pages/Workflows/AllRuns.razor index 0c41875..0ea35b9 100644 --- a/src/Werkr.Server/Components/Pages/Workflows/AllRuns.razor +++ b/src/Werkr.Server/Components/Pages/Workflows/AllRuns.razor @@ -1,5 +1,4 @@ @page "/workflows/runs" -@using System.Globalization @using Werkr.Server.Services @rendermode InteractiveServer @attribute [Authorize] @@ -88,13 +87,7 @@ ] ); private List FilteredRuns => ( _runs ?? [] ) - .Where( r => - ( string.IsNullOrWhiteSpace( _activeCriteria?.Get( "status" ) ) - || r.Status.Equals( _activeCriteria.Get( "status" ), StringComparison.OrdinalIgnoreCase ) ) - && ( !DateTime.TryParseExact( _activeCriteria?.Get( "since" ), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime since ) - || r.StartTime >= since ) - && ( !DateTime.TryParseExact( _activeCriteria?.Get( "until" ), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime until ) - || r.StartTime < until.AddDays( 1 ) ) ) + .Where( r => FilterHelper.MatchesRunFilter( r, _activeCriteria ) ) .ToList( ); protected override async Task OnInitializedAsync( ) { diff --git a/src/Werkr.Server/Components/Pages/Workflows/Runs.razor b/src/Werkr.Server/Components/Pages/Workflows/Runs.razor index b2af1a8..0fbb219 100644 --- a/src/Werkr.Server/Components/Pages/Workflows/Runs.razor +++ b/src/Werkr.Server/Components/Pages/Workflows/Runs.razor @@ -1,5 +1,4 @@ @page "/workflows/{WorkflowId:long}/runs" -@using System.Globalization @using Werkr.Common.Models @using Werkr.Server.Services @rendermode InteractiveServer @@ -89,13 +88,7 @@ ] ); private List FilteredRuns => ( _runs ?? [] ) - .Where( r => - ( string.IsNullOrWhiteSpace( _activeCriteria?.Get( "status" ) ) - || r.Status.Equals( _activeCriteria.Get( "status" ), StringComparison.OrdinalIgnoreCase ) ) - && ( !DateTime.TryParseExact( _activeCriteria?.Get( "since" ), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime since ) - || r.StartTime >= since ) - && ( !DateTime.TryParseExact( _activeCriteria?.Get( "until" ), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime until ) - || r.StartTime < until.AddDays( 1 ) ) ) + .Where( r => FilterHelper.MatchesRunFilter( r, _activeCriteria ) ) .ToList( ); protected override async Task OnInitializedAsync( ) { diff --git a/src/Werkr.Server/Components/Shared/EditorToolbar.razor b/src/Werkr.Server/Components/Shared/EditorToolbar.razor index 8448a3c..a7080a9 100644 --- a/src/Werkr.Server/Components/Shared/EditorToolbar.razor +++ b/src/Werkr.Server/Components/Shared/EditorToolbar.razor @@ -22,10 +22,10 @@ @* ── Undo / Redo ── *@
- -
@@ -47,7 +47,7 @@ @* ── Auto-Layout ── *@ - @@ -73,13 +73,13 @@ @* ── Zoom ── *@
- - -
@@ -90,10 +90,10 @@ @* ── Export ── *@
- -
@@ -104,12 +104,13 @@ @* ── Add Annotation ── *@ - diff --git a/src/Werkr.Server/Components/Shared/RunGridView.razor b/src/Werkr.Server/Components/Shared/RunGridView.razor index d872866..8f619c3 100644 --- a/src/Werkr.Server/Components/Shared/RunGridView.razor +++ b/src/Werkr.Server/Components/Shared/RunGridView.razor @@ -29,6 +29,7 @@
diff --git a/src/Werkr.Server/Helpers/FilterHelper.cs b/src/Werkr.Server/Helpers/FilterHelper.cs new file mode 100644 index 0000000..f2c099e --- /dev/null +++ b/src/Werkr.Server/Helpers/FilterHelper.cs @@ -0,0 +1,34 @@ +using System.Globalization; +using Werkr.Common.Models; + +namespace Werkr.Server.Helpers; + +/// Shared filter-criteria parsing utilities for list pages. +public static class FilterHelper { + /// Parses a "yyyy-MM-dd" date string from filter criteria. + public static DateTime? TryParseFilterDate( string? dateText ) => + DateTime.TryParseExact( dateText, "yyyy-MM-dd", + CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime parsed ) + ? parsed : null; + + /// + /// Client-side predicate for WorkflowRunDto filtering by status + date range. + /// Used by Runs.razor and AllRuns.razor. + /// + public static bool MatchesRunFilter( WorkflowRunDto run, FilterCriteria? criteria ) { + if (criteria is null) return true; + + string? status = criteria.Get( "status" ); + if (!string.IsNullOrWhiteSpace( status ) + && !run.Status.Equals( status, StringComparison.OrdinalIgnoreCase )) + return false; + + DateTime? since = TryParseFilterDate( criteria.Get( "since" ) ); + if (since.HasValue && run.StartTime < since.Value) return false; + + DateTime? until = TryParseFilterDate( criteria.Get( "until" ) ); + if (until.HasValue && run.StartTime >= until.Value.AddDays( 1 )) return false; + + return true; + } +} From 7ecfaf85f8f5f281bc26ccbe15821d848fa62494 Mon Sep 17 00:00:00 2001 From: Taylor Marvin Date: Sat, 14 Mar 2026 23:59:06 -0700 Subject: [PATCH 08/11] Address PR concerns x4 --- .vscode/tasks.json | 889 +++++++++--------- .../ControlStatementConverterTests.cs | 1 + .../Components/ConditionBuilderTests.cs | 19 +- .../Services/OutputStreamingGrpcService.cs | 11 +- src/Werkr.Api/Services/VariableGrpcService.cs | 50 +- .../Communication/WorkflowEventBroadcaster.cs | 4 +- src/Werkr.Data/WerkrDbContext.cs | 1 + .../Components/Shared/ConditionBuilder.razor | 12 - .../Components/Shared/DagCanvas.razor | 6 +- .../Components/Shared/RunGridView.razor | 25 +- .../Components/Shared/RunSparkline.razor | 1 + 11 files changed, 511 insertions(+), 508 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5876818..4b7af21 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,442 +1,447 @@ -{ - "version": "2.0.0", - "inputs": [ - { - "id": "migrationName", - "type": "promptString", - "description": "Name for the EF Core migration", - "default": "migration" - } - ], - "tasks": [ - { - "label": "verify:restore", - "type": "process", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "dotnet", - "restore", - "Werkr.slnx" - ], - "group": "build", - "problemMatcher": "$msCompile", - "presentation": { - "reveal": "silent", - "revealProblems": "onProblem", - "showReuseMessage": false - } - }, - { - "label": "verify:build", - "type": "process", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "dotnet", - "build", - "Werkr.slnx" - ], - "group": "build", - "problemMatcher": "$msCompile", - "presentation": { - "reveal": "silent", - "revealProblems": "onProblem", - "showReuseMessage": false - } - }, - { - "label": "verify:format", - "type": "process", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "dotnet", - "format", - "Werkr.slnx" - ], - "problemMatcher": [], - "presentation": { - "reveal": "silent", - "revealProblems": "onProblem", - "showReuseMessage": false - } - }, - { - "label": "verify:test-unit", - "type": "process", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "dotnet", - "test", - "--project", - "src/Test/Werkr.Tests.Data/Werkr.Tests.Data.csproj" - ], - "group": "test", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}" - }, - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "verify:test-integration", - "type": "process", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "dotnet", - "test", - "--project", - "src/Test/Werkr.Tests.Server/Werkr.Tests.Server.csproj" - ], - "group": "test", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}" - }, - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "verify:test-e2e", - "type": "process", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "dotnet", - "test", - "--project", - "src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj" - ], - "group": "test", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}" - }, - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "verify:start-apphost", - "type": "process", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "dotnet", - "run", - "--project", - "src/Werkr.AppHost/Werkr.AppHost.csproj" - ], - "group": "test", - "problemMatcher": "$msCompile", - "options": { - "cwd": "${workspaceFolder}" - }, - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "verify:docker-check", - "type": "process", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "docker", - "compose", - "config" - ], - "options": { - "cwd": "${workspaceFolder}" - }, - "problemMatcher": [], - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "verify:docker-build", - "type": "process", - "command": "pwsh", - "args": [ - "--noprofile", - "-File", - "${workspaceFolder}/scripts/docker-build.ps1" - ], - "group": "build", - "problemMatcher": [], - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "docker:start", - "type": "process", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "docker", - "compose", - "up", - "-d" - ], - "options": { - "cwd": "${workspaceFolder}" - }, - "problemMatcher": [], - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "docker:stop", - "type": "process", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "docker", - "compose", - "down" - ], - "options": { - "cwd": "${workspaceFolder}" - }, - "problemMatcher": [], - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "docker:restart", - "dependsOn": [ - "docker:stop", - "docker:start" - ], - "dependsOrder": "sequence", - "problemMatcher": [], - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "ef:app:postgres", - "type": "shell", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "dotnet", - "ef", - "migrations", - "add", - "${input:migrationName}", - "--project", - "src/Werkr.Data", - "--startup-project", - "src/Werkr.Api", - "--context", - "PostgresWerkrDbContext", - "--output-dir", - "Migrations/Postgres" - ], - "options": { - "cwd": "${workspaceFolder}" - }, - "isBackground": false, - "problemMatcher": [], - "group": "build", - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "ef:app:sqlite", - "type": "shell", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "dotnet", - "ef", - "migrations", - "add", - "${input:migrationName}", - "--project", - "src/Werkr.Data", - "--startup-project", - "src/Werkr.Api", - "--context", - "SqliteWerkrDbContext", - "--output-dir", - "Migrations/Sqlite" - ], - "options": { - "cwd": "${workspaceFolder}" - }, - "isBackground": false, - "problemMatcher": [], - "group": "build", - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "ef:app:both", - "dependsOn": [ - "ef:app:postgres", - "ef:app:sqlite" - ], - "dependsOrder": "sequence", - "problemMatcher": [], - "group": "build", - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "ef:identity:postgres", - "type": "shell", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "dotnet", - "ef", - "migrations", - "add", - "${input:migrationName}", - "--project", - "src/Werkr.Data.Identity", - "--startup-project", - "src/Werkr.Server", - "--context", - "PostgresWerkrIdentityDbContext", - "--output-dir", - "Migrations/Postgres" - ], - "options": { - "cwd": "${workspaceFolder}" - }, - "isBackground": false, - "problemMatcher": [], - "group": "build", - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "ef:identity:sqlite", - "type": "shell", - "command": "pwsh", - "args": [ - "--noprofile", - "-c", - "dotnet", - "ef", - "migrations", - "add", - "${input:migrationName}", - "--project", - "src/Werkr.Data.Identity", - "--startup-project", - "src/Werkr.Server", - "--context", - "SqliteWerkrIdentityDbContext", - "--output-dir", - "Migrations/Sqlite" - ], - "options": { - "cwd": "${workspaceFolder}" - }, - "isBackground": false, - "problemMatcher": [], - "group": "build", - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "ef:identity:both", - "dependsOn": [ - "ef:identity:postgres", - "ef:identity:sqlite" - ], - "dependsOrder": "sequence", - "problemMatcher": [], - "group": "build", - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "ef:all", - "dependsOn": [ - "ef:app:both", - "ef:identity:both" - ], - "dependsOrder": "sequence", - "problemMatcher": [], - "group": "build", - "presentation": { - "reveal": "always", - "showReuseMessage": false - } - }, - { - "label": "verify:build-server", - "type": "shell", - "command": "pwsh --noprofile -c 'dotnet build src/Werkr.Server/Werkr.Server.csproj'" - }, - { - "label": "verify:test-e2e-verbose", - "type": "shell", - "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --verbosity normal 2>&1 | tail -50'" - }, - { - "label": "verify:test-e2e-failures", - "type": "shell", - "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj -- --report-trx 2>&1 | grep -i -E \"failed|error|FAIL\" | head -20'" - }, - { - "label": "verify:e2e-fail-detail", - "type": "shell", - "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --no-build 2>&1 | grep -i -E \"failed|FAIL\" | head -20'" - }, - { - "label": "verify:e2e-tail", - "type": "shell", - "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --no-build 2>&1 | tail -30'" - } - ] -} \ No newline at end of file +{ + "version": "2.0.0", + "inputs": [ + { + "id": "migrationName", + "type": "promptString", + "description": "Name for the EF Core migration", + "default": "migration" + } + ], + "tasks": [ + { + "label": "verify:restore", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "restore", + "Werkr.slnx" + ], + "group": "build", + "problemMatcher": "$msCompile", + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "showReuseMessage": false + } + }, + { + "label": "verify:build", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "build", + "Werkr.slnx" + ], + "group": "build", + "problemMatcher": "$msCompile", + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "showReuseMessage": false + } + }, + { + "label": "verify:format", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "format", + "Werkr.slnx" + ], + "problemMatcher": [], + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "showReuseMessage": false + } + }, + { + "label": "verify:test-unit", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "test", + "--project", + "src/Test/Werkr.Tests.Data/Werkr.Tests.Data.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:test-integration", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "test", + "--project", + "src/Test/Werkr.Tests.Server/Werkr.Tests.Server.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:test-e2e", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "test", + "--project", + "src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:start-apphost", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "run", + "--project", + "src/Werkr.AppHost/Werkr.AppHost.csproj" + ], + "group": "test", + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}" + }, + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:docker-check", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "docker", + "compose", + "config" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:docker-build", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-File", + "${workspaceFolder}/scripts/docker-build.ps1" + ], + "group": "build", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "docker:start", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "docker", + "compose", + "up", + "-d" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "docker:stop", + "type": "process", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "docker", + "compose", + "down" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "docker:restart", + "dependsOn": [ + "docker:stop", + "docker:start" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:app:postgres", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data", + "--startup-project", + "src/Werkr.Api", + "--context", + "PostgresWerkrDbContext", + "--output-dir", + "Migrations/Postgres" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:app:sqlite", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data", + "--startup-project", + "src/Werkr.Api", + "--context", + "SqliteWerkrDbContext", + "--output-dir", + "Migrations/Sqlite" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:app:both", + "dependsOn": [ + "ef:app:postgres", + "ef:app:sqlite" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:identity:postgres", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data.Identity", + "--startup-project", + "src/Werkr.Server", + "--context", + "PostgresWerkrIdentityDbContext", + "--output-dir", + "Migrations/Postgres" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:identity:sqlite", + "type": "shell", + "command": "pwsh", + "args": [ + "--noprofile", + "-c", + "dotnet", + "ef", + "migrations", + "add", + "${input:migrationName}", + "--project", + "src/Werkr.Data.Identity", + "--startup-project", + "src/Werkr.Server", + "--context", + "SqliteWerkrIdentityDbContext", + "--output-dir", + "Migrations/Sqlite" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "isBackground": false, + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:identity:both", + "dependsOn": [ + "ef:identity:postgres", + "ef:identity:sqlite" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "ef:all", + "dependsOn": [ + "ef:app:both", + "ef:identity:both" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "group": "build", + "presentation": { + "reveal": "always", + "showReuseMessage": false + } + }, + { + "label": "verify:build-server", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet build src/Werkr.Server/Werkr.Server.csproj'" + }, + { + "label": "verify:test-e2e-verbose", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --verbosity normal 2>&1 | tail -50'" + }, + { + "label": "verify:test-e2e-failures", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj -- --report-trx 2>&1 | grep -i -E \"failed|error|FAIL\" | head -20'" + }, + { + "label": "verify:e2e-fail-detail", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj --no-build 2>&1 | grep -i -E \"failed|FAIL\" | head -20'" + }, + { + "label": "verify:e2e-tail", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Agent/Werkr.Tests.Agent.csproj'" + }, + { + "label": "verify:test-server", + "type": "shell", + "command": "pwsh --noprofile -c 'dotnet test --project src/Test/Werkr.Tests.Server/Werkr.Tests.Server.csproj'" + } + ] +} diff --git a/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs index 0cc8d32..aa8d7e4 100644 --- a/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs +++ b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs @@ -168,6 +168,7 @@ public async Task LegacySequential_ReadsAs_Default( ) { [DataRow( "ConditionalElseIf", ControlStatement.ElseIf )] [DataRow( "ConditionalWhile", ControlStatement.While )] [DataRow( "ConditionalDo", ControlStatement.Do )] + [DataRow("ConditionalElse", ControlStatement.Else)] public async Task LegacyValues_ReadAs_CorrectEnum( string legacyString, ControlStatement expected ) { CancellationToken ct = TestContext.CancellationToken; diff --git a/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs b/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs index 8a6b506..b41795d 100644 --- a/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs +++ b/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs @@ -131,10 +131,12 @@ public void ApplyRaw_ValidExpression_FiresCallback( ) { } /// - /// Verifies that an invalid raw expression shows a validation error and does not fire the callback. + /// Verifies that a custom raw expression is accepted and fires the callback + /// (raw mode does not restrict to known patterns). /// [TestMethod] - public void ApplyRaw_InvalidExpression_ShowsError( ) { + public void ApplyRaw_CustomExpression_FiresCallback() + { string? captured = null; IRenderedComponent cut = Render( parameters => parameters.Add( p => p.Expression, null ) @@ -143,20 +145,15 @@ public void ApplyRaw_InvalidExpression_ShowsError( ) { // Switch to advanced mode cut.Find( "button.btn-outline-secondary" ).Click( ); - // Type an invalid expression - cut.Find( "input[type=text]" ).Input( "invalid garbage" ); + // Type a custom expression not matching known patterns + cut.Find("input[type=text]").Input("custom_var > 42"); // Click Apply IReadOnlyList buttons = cut.FindAll( "button.btn-outline-primary" ); buttons[^1].Click( ); - // Should show validation error - AngleSharp.Dom.IElement errorDiv = cut.Find( ".text-danger" ); - Assert.IsNotNull( errorDiv ); - Assert.IsFalse( string.IsNullOrWhiteSpace( errorDiv.TextContent ) ); - - // Callback should NOT have been fired (expression stays null) - Assert.IsNull( captured ); + // Callback should have been fired with the custom expression + Assert.AreEqual("custom_var > 42", captured); } /// diff --git a/src/Werkr.Api/Services/OutputStreamingGrpcService.cs b/src/Werkr.Api/Services/OutputStreamingGrpcService.cs index 1af59ab..5745b01 100644 --- a/src/Werkr.Api/Services/OutputStreamingGrpcService.cs +++ b/src/Werkr.Api/Services/OutputStreamingGrpcService.cs @@ -128,20 +128,15 @@ ServerCallContext context RunLogRateState rateState = _logRateState.GetOrAdd( workflowRunId, _ => new RunLogRateState( ) ); if (rateState.TryAcquire( out int dropped )) { + string lineText = message.Line.Text; if (dropped > 0) { - workflowBroadcaster.Publish( new LogAppendedEvent( - WorkflowRunId: workflowRunId, - StepId: message.StepId, - JobId: jobId, - Line: $"... {dropped} lines batched ...", - Timestamp: DateTime.UtcNow - ) ); + lineText = $"... {dropped} lines batched ...\n{lineText}"; } workflowBroadcaster.Publish( new LogAppendedEvent( WorkflowRunId: workflowRunId, StepId: message.StepId, JobId: jobId, - Line: message.Line.Text, + Line: lineText, Timestamp: DateTime.UtcNow ) ); } diff --git a/src/Werkr.Api/Services/VariableGrpcService.cs b/src/Werkr.Api/Services/VariableGrpcService.cs index 26153b3..79d12f4 100644 --- a/src/Werkr.Api/Services/VariableGrpcService.cs +++ b/src/Werkr.Api/Services/VariableGrpcService.cs @@ -320,23 +320,45 @@ ServerCallContext context throw new RpcException( new Status( StatusCode.InvalidArgument, "Invalid workflow_run_id." ) ); } - // Return the latest attempt per step - List executions = await dbContext.WorkflowStepExecutions - .Where( se => se.WorkflowRunId == runId ) - .OrderByDescending( se => se.Attempt ) - .ToListAsync( context.CancellationToken ); + // Return the latest attempt per step, selected in the database + List executions; + try + { + executions = await dbContext.WorkflowStepExecutions + .Where(se => se.WorkflowRunId == runId) + .GroupBy(se => se.StepId) + .Select(g => g.OrderByDescending(se => se.Attempt).First()) + .ToListAsync(context.CancellationToken); + } + catch (Exception ex) + { + // Fallback: EF provider (e.g. SQLite) may not support GroupBy+First + logger.LogWarning(ex, "GroupBy+First failed; falling back to in-memory grouping."); + List all = await dbContext.WorkflowStepExecutions + .Where(se => se.WorkflowRunId == runId) + .OrderByDescending(se => se.Attempt) + .ToListAsync(context.CancellationToken); + HashSet seen = []; + executions = []; + foreach (WorkflowStepExecution exec in all) + { + if (seen.Add(exec.StepId)) + { + executions.Add(exec); + } + } + } GetStepExecutionsResponse response = new( ); - HashSet seen = []; - foreach (WorkflowStepExecution exec in executions) { - if (seen.Add( exec.StepId )) { - response.Entries.Add( new StepExecutionEntry { - StepId = exec.StepId, - Attempt = exec.Attempt, - Status = exec.Status.ToString( ), - } ); - } + foreach (WorkflowStepExecution exec in executions) + { + response.Entries.Add(new StepExecutionEntry + { + StepId = exec.StepId, + Attempt = exec.Attempt, + Status = exec.Status.ToString(), + }); } return PayloadEncryptor.EncryptToEnvelope( response, connection.SharedKey, keyId ); diff --git a/src/Werkr.Core/Communication/WorkflowEventBroadcaster.cs b/src/Werkr.Core/Communication/WorkflowEventBroadcaster.cs index e77759b..edaabd4 100644 --- a/src/Werkr.Core/Communication/WorkflowEventBroadcaster.cs +++ b/src/Werkr.Core/Communication/WorkflowEventBroadcaster.cs @@ -17,8 +17,8 @@ public sealed partial class WorkflowEventBroadcaster( ILogger /// Creates a new subscription. The caller reads from the returned - /// and must call - /// (or dispose the ) when done. + /// and must dispose the returned + /// when done. /// public WorkflowEventSubscription Subscribe( ) { Channel channel = Channel.CreateBounded( diff --git a/src/Werkr.Data/WerkrDbContext.cs b/src/Werkr.Data/WerkrDbContext.cs index 9b2f453..a22c97f 100644 --- a/src/Werkr.Data/WerkrDbContext.cs +++ b/src/Werkr.Data/WerkrDbContext.cs @@ -613,6 +613,7 @@ private static ControlStatement ParseControlStatement( string v ) => "Sequential" or "Parallel" => ControlStatement.Default, "ConditionalIf" => ControlStatement.If, "ConditionalElseIf" => ControlStatement.ElseIf, + "ConditionalElse" => ControlStatement.Else, "ConditionalWhile" => ControlStatement.While, "ConditionalDo" => ControlStatement.Do, _ => Enum.TryParse( v, ignoreCase: true, out ControlStatement parsed ) diff --git a/src/Werkr.Server/Components/Shared/ConditionBuilder.razor b/src/Werkr.Server/Components/Shared/ConditionBuilder.razor index 7e1180d..e4f990b 100644 --- a/src/Werkr.Server/Components/Shared/ConditionBuilder.razor +++ b/src/Werkr.Server/Components/Shared/ConditionBuilder.razor @@ -4,9 +4,6 @@
- @if ( !string.IsNullOrWhiteSpace( _validationError ) ) { -
@_validationError
- }
- - @GetStatusIcon( status ) - + +
@@ -48,7 +50,7 @@ - @foreach ( WorkflowRunDto run in FilteredRuns ) { + @foreach ( WorkflowRunDto run in filteredRuns ) { diff --git a/src/Werkr.Server/Components/Shared/DagCanvas.razor b/src/Werkr.Server/Components/Shared/DagCanvas.razor index 4e0bca6..a3f16d9 100644 --- a/src/Werkr.Server/Components/Shared/DagCanvas.razor +++ b/src/Werkr.Server/Components/Shared/DagCanvas.razor @@ -103,13 +103,11 @@ } } - // Always (re)apply statuses when provided, regardless of reference identity - if ( StepStatuses is not null && StepStatuses.Count > 0 ) { - try { - await ApplyStatusesAsync( ); - } catch ( Exception ex ) { - Logger.LogError( ex, "Failed to update DAG status overlay." ); - } + // Always (re)apply or clear statuses when parameters change + try { + await ApplyStatusesAsync( ); + } catch ( Exception ex ) { + Logger.LogError( ex, "Failed to update DAG status overlay." ); } } diff --git a/src/Werkr.Server/Components/Shared/RunGridView.razor.css b/src/Werkr.Server/Components/Shared/RunGridView.razor.css index b9bc533..1c33ef2 100644 --- a/src/Werkr.Server/Components/Shared/RunGridView.razor.css +++ b/src/Werkr.Server/Components/Shared/RunGridView.razor.css @@ -4,7 +4,7 @@ z-index: 1; } -.run-grid-table td[role="button"]:hover { +.run-grid-table button:hover { background-color: var(--bs-tertiary-bg); cursor: pointer; } From c27c85e4516453654d35b791a08e9a1d594eb66d Mon Sep 17 00:00:00 2001 From: Taylor Marvin Date: Sun, 15 Mar 2026 01:14:52 -0700 Subject: [PATCH 10/11] Address PR concerns x6 --- src/Installer/Msi/Agent/packages.lock.json | 6 +++- src/Installer/Msi/Server/packages.lock.json | 6 +++- .../ControlStatementConverterTests.cs | 2 +- .../Components/ConditionBuilderTests.cs | 7 ++-- .../Services/OutputStreamingGrpcService.cs | 15 ++++---- src/Werkr.Api/Services/VariableGrpcService.cs | 35 ++++++++----------- src/Werkr.Server/Helpers/FilterHelper.cs | 15 ++++---- 7 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/Installer/Msi/Agent/packages.lock.json b/src/Installer/Msi/Agent/packages.lock.json index f432b8b..d8ec7ee 100644 --- a/src/Installer/Msi/Agent/packages.lock.json +++ b/src/Installer/Msi/Agent/packages.lock.json @@ -14,6 +14,10 @@ "resolved": "6.0.2", "contentHash": "plP64ub/0KjNbtLeaeiibVCPkKfr439WTKZmTwVSoQ4fznLHBZLsE0+wcyk6dA5cQuQsD5hlmnVGTKgPioiusQ==" } - } + }, + "native,Version=v0.0/win": {}, + "native,Version=v0.0/win-arm64": {}, + "native,Version=v0.0/win-x64": {}, + "native,Version=v0.0/win-x86": {} } } \ No newline at end of file diff --git a/src/Installer/Msi/Server/packages.lock.json b/src/Installer/Msi/Server/packages.lock.json index f432b8b..d8ec7ee 100644 --- a/src/Installer/Msi/Server/packages.lock.json +++ b/src/Installer/Msi/Server/packages.lock.json @@ -14,6 +14,10 @@ "resolved": "6.0.2", "contentHash": "plP64ub/0KjNbtLeaeiibVCPkKfr439WTKZmTwVSoQ4fznLHBZLsE0+wcyk6dA5cQuQsD5hlmnVGTKgPioiusQ==" } - } + }, + "native,Version=v0.0/win": {}, + "native,Version=v0.0/win-arm64": {}, + "native,Version=v0.0/win-x64": {}, + "native,Version=v0.0/win-x86": {} } } \ No newline at end of file diff --git a/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs index aa8d7e4..58f29ef 100644 --- a/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs +++ b/src/Test/Werkr.Tests.Data/Unit/Workflows/ControlStatementConverterTests.cs @@ -168,7 +168,7 @@ public async Task LegacySequential_ReadsAs_Default( ) { [DataRow( "ConditionalElseIf", ControlStatement.ElseIf )] [DataRow( "ConditionalWhile", ControlStatement.While )] [DataRow( "ConditionalDo", ControlStatement.Do )] - [DataRow("ConditionalElse", ControlStatement.Else)] + [DataRow( "ConditionalElse", ControlStatement.Else )] public async Task LegacyValues_ReadAs_CorrectEnum( string legacyString, ControlStatement expected ) { CancellationToken ct = TestContext.CancellationToken; diff --git a/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs b/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs index b41795d..62a4b75 100644 --- a/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs +++ b/src/Test/Werkr.Tests.Server/Components/ConditionBuilderTests.cs @@ -135,8 +135,7 @@ public void ApplyRaw_ValidExpression_FiresCallback( ) { /// (raw mode does not restrict to known patterns). /// [TestMethod] - public void ApplyRaw_CustomExpression_FiresCallback() - { + public void ApplyRaw_CustomExpression_FiresCallback( ) { string? captured = null; IRenderedComponent cut = Render( parameters => parameters.Add( p => p.Expression, null ) @@ -146,14 +145,14 @@ public void ApplyRaw_CustomExpression_FiresCallback() cut.Find( "button.btn-outline-secondary" ).Click( ); // Type a custom expression not matching known patterns - cut.Find("input[type=text]").Input("custom_var > 42"); + cut.Find( "input[type=text]" ).Input( "custom_var > 42" ); // Click Apply IReadOnlyList buttons = cut.FindAll( "button.btn-outline-primary" ); buttons[^1].Click( ); // Callback should have been fired with the custom expression - Assert.AreEqual("custom_var > 42", captured); + Assert.AreEqual( "custom_var > 42", captured ); } /// diff --git a/src/Werkr.Api/Services/OutputStreamingGrpcService.cs b/src/Werkr.Api/Services/OutputStreamingGrpcService.cs index 5745b01..f6e39e2 100644 --- a/src/Werkr.Api/Services/OutputStreamingGrpcService.cs +++ b/src/Werkr.Api/Services/OutputStreamingGrpcService.cs @@ -63,13 +63,11 @@ private sealed class RunLogRateState { public bool TryAcquire( out int droppedSinceLastPublish ) { long nowTicks = Stopwatch.GetTimestamp(); - while (true) - { + while (true) { long last = Interlocked.Read(ref _lastPublishTicks); - if (nowTicks - last < s_tickInterval) - { - _ = Interlocked.Increment(ref _droppedCount); + if (nowTicks - last < s_tickInterval) { + _ = Interlocked.Increment( ref _droppedCount ); droppedSinceLastPublish = 0; return false; } @@ -77,13 +75,12 @@ public bool TryAcquire( out int droppedSinceLastPublish ) { long original = Interlocked.CompareExchange( ref _lastPublishTicks, nowTicks, last); - if (original == last) - { - droppedSinceLastPublish = Interlocked.Exchange(ref _droppedCount, 0); + if (original == last) { + droppedSinceLastPublish = Interlocked.Exchange( ref _droppedCount, 0 ); return true; } - nowTicks = Stopwatch.GetTimestamp(); + nowTicks = Stopwatch.GetTimestamp( ); } } } diff --git a/src/Werkr.Api/Services/VariableGrpcService.cs b/src/Werkr.Api/Services/VariableGrpcService.cs index 79d12f4..94ec667 100644 --- a/src/Werkr.Api/Services/VariableGrpcService.cs +++ b/src/Werkr.Api/Services/VariableGrpcService.cs @@ -322,43 +322,36 @@ ServerCallContext context // Return the latest attempt per step, selected in the database List executions; - try - { + try { executions = await dbContext.WorkflowStepExecutions - .Where(se => se.WorkflowRunId == runId) - .GroupBy(se => se.StepId) - .Select(g => g.OrderByDescending(se => se.Attempt).First()) - .ToListAsync(context.CancellationToken); - } - catch (Exception ex) - { + .Where( se => se.WorkflowRunId == runId ) + .GroupBy( se => se.StepId ) + .Select( g => g.OrderByDescending( se => se.Attempt ).First( ) ) + .ToListAsync( context.CancellationToken ); + } catch (Exception ex) { // Fallback: EF provider (e.g. SQLite) may not support GroupBy+First - logger.LogWarning(ex, "GroupBy+First failed; falling back to in-memory grouping."); + logger.LogWarning( ex, "GroupBy+First failed; falling back to in-memory grouping." ); List all = await dbContext.WorkflowStepExecutions .Where(se => se.WorkflowRunId == runId) .OrderByDescending(se => se.Attempt) .ToListAsync(context.CancellationToken); HashSet seen = []; executions = []; - foreach (WorkflowStepExecution exec in all) - { - if (seen.Add(exec.StepId)) - { - executions.Add(exec); + foreach (WorkflowStepExecution exec in all) { + if (seen.Add( exec.StepId )) { + executions.Add( exec ); } } } GetStepExecutionsResponse response = new( ); - foreach (WorkflowStepExecution exec in executions) - { - response.Entries.Add(new StepExecutionEntry - { + foreach (WorkflowStepExecution exec in executions) { + response.Entries.Add( new StepExecutionEntry { StepId = exec.StepId, Attempt = exec.Attempt, - Status = exec.Status.ToString(), - }); + Status = exec.Status.ToString( ), + } ); } return PayloadEncryptor.EncryptToEnvelope( response, connection.SharedKey, keyId ); diff --git a/src/Werkr.Server/Helpers/FilterHelper.cs b/src/Werkr.Server/Helpers/FilterHelper.cs index f2c099e..ec0b9ee 100644 --- a/src/Werkr.Server/Helpers/FilterHelper.cs +++ b/src/Werkr.Server/Helpers/FilterHelper.cs @@ -16,19 +16,22 @@ public static class FilterHelper { /// Used by Runs.razor and AllRuns.razor. /// public static bool MatchesRunFilter( WorkflowRunDto run, FilterCriteria? criteria ) { - if (criteria is null) return true; + if (criteria is null) { + return true; + } string? status = criteria.Get( "status" ); if (!string.IsNullOrWhiteSpace( status ) - && !run.Status.Equals( status, StringComparison.OrdinalIgnoreCase )) + && !run.Status.Equals( status, StringComparison.OrdinalIgnoreCase )) { return false; + } DateTime? since = TryParseFilterDate( criteria.Get( "since" ) ); - if (since.HasValue && run.StartTime < since.Value) return false; + if (since.HasValue && run.StartTime < since.Value) { + return false; + } DateTime? until = TryParseFilterDate( criteria.Get( "until" ) ); - if (until.HasValue && run.StartTime >= until.Value.AddDays( 1 )) return false; - - return true; + return !until.HasValue || run.StartTime < until.Value.AddDays( 1 ); } } From 025d654eeb7bcad56c6119fe9694c3da671ce914 Mon Sep 17 00:00:00 2001 From: Taylor Marvin Date: Mon, 16 Mar 2026 00:27:53 -0700 Subject: [PATCH 11/11] Attempts at bugfixes and editor canvas improvements --- .aspire/settings.json | 3 + .../Identity/IdentitySeederTests.cs | 136 ++++++++++++++++++ src/Werkr.Server/Identity/IdentitySeeder.cs | 91 +++++++++++- src/Werkr.Server/Program.cs | 17 +-- src/Werkr.Server/appsettings.Development.json | 4 + .../graph-ui/src/dag/create-graph.ts | 1 + .../graph-ui/src/dag/dag-editor.ts | 3 +- .../graph-ui/src/dag/dnd-handler.ts | 2 + .../graph-ui/src/dag/editor-bindings.ts | 84 ++++++++--- 9 files changed, 310 insertions(+), 31 deletions(-) create mode 100644 .aspire/settings.json diff --git a/.aspire/settings.json b/.aspire/settings.json new file mode 100644 index 0000000..231f885 --- /dev/null +++ b/.aspire/settings.json @@ -0,0 +1,3 @@ +{ + "appHostPath": "..\\src\\Werkr.AppHost\\Werkr.AppHost.csproj" +} \ No newline at end of file diff --git a/src/Test/Werkr.Tests.Server/Identity/IdentitySeederTests.cs b/src/Test/Werkr.Tests.Server/Identity/IdentitySeederTests.cs index 2e325ae..99ab762 100644 --- a/src/Test/Werkr.Tests.Server/Identity/IdentitySeederTests.cs +++ b/src/Test/Werkr.Tests.Server/Identity/IdentitySeederTests.cs @@ -178,4 +178,140 @@ public async Task SeedAsync_SecondCallDoesNotCreateDuplicateRoles( ) { $"Role '{role}' should still exist after double-seed." ); } } + + /// + /// Verifies that when Werkr:SeedTestOperator is not configured (the default), no test operator account is + /// created by . This ensures production deployments are unaffected. + /// + [TestMethod] + public async Task SeedAsync_TestOperator_NotCreatedByDefault( ) { + await IdentitySeeder.SeedAsync( _provider ); + + using IServiceScope scope = _provider.CreateScope(); + UserManager userManager = scope.ServiceProvider + .GetRequiredService>(); + + WerkrUser? operatorUser = await userManager.FindByEmailAsync("operator@werkr.local"); + + Assert.IsNull( operatorUser, + "Test operator should not be created when Werkr:SeedTestOperator is not configured." ); + } + + /// + /// Verifies that when Werkr:SeedTestOperator is true and a valid password is provided via + /// Werkr:TestOperatorPassword, creates a test operator account with + /// the expected properties: display name "Test Operator", = , = , = , and = . + /// + [TestMethod] + public async Task SeedAsync_CreatesTestOperator_WhenConfigured( ) { + ServiceProvider provider = BuildProviderWithTestOperatorConfig(); + try { + await IdentitySeeder.SeedAsync( provider ); + + using IServiceScope scope = provider.CreateScope(); + UserManager userManager = scope.ServiceProvider + .GetRequiredService>(); + + WerkrUser? operatorUser = await userManager.FindByEmailAsync("operator@werkr.local"); + + Assert.IsNotNull( operatorUser, "Test operator should be created when configured." ); + Assert.AreEqual( "Test Operator", operatorUser.Name ); + Assert.IsTrue( operatorUser.Enabled, "Operator should be enabled." ); + Assert.IsFalse( operatorUser.ChangePassword, "Operator should not require password change." ); + Assert.IsFalse( operatorUser.Requires2FA, "Operator should not require 2FA." ); + Assert.IsTrue( operatorUser.EmailConfirmed, "Operator email should be confirmed." ); + } finally { + provider.Dispose( ); + } + } + + /// + /// Verifies that the test operator account created by is assigned to the + /// "Operator" role as defined in . + /// + [TestMethod] + public async Task SeedAsync_TestOperator_HasOperatorRole( ) { + ServiceProvider provider = BuildProviderWithTestOperatorConfig(); + try { + await IdentitySeeder.SeedAsync( provider ); + + using IServiceScope scope = provider.CreateScope(); + UserManager userManager = scope.ServiceProvider + .GetRequiredService>(); + + WerkrUser? operatorUser = await userManager.FindByEmailAsync("operator@werkr.local"); + Assert.IsNotNull( operatorUser ); + + IList roles = await userManager.GetRolesAsync(operatorUser); + + Assert.Contains( DefaultRoles.Operator.ToString( ), roles, + "Test operator should be in Operator role." ); + } finally { + provider.Dispose( ); + } + } + + /// + /// Verifies that calling twice with test operator configuration enabled + /// does not create a duplicate operator user, confirming the seeder's idempotency. + /// + [TestMethod] + public async Task SeedAsync_TestOperator_SecondCallDoesNotCreateDuplicate( ) { + ServiceProvider provider = BuildProviderWithTestOperatorConfig(); + try { + await IdentitySeeder.SeedAsync( provider ); + await IdentitySeeder.SeedAsync( provider ); + + using IServiceScope scope = provider.CreateScope(); + UserManager userManager = scope.ServiceProvider + .GetRequiredService>(); + + IList operators = await userManager.GetUsersInRoleAsync( + DefaultRoles.Operator.ToString()); + + Assert.HasCount( 1, operators, + "Should have exactly one operator after double-seed." ); + } finally { + provider.Dispose( ); + } + } + + /// + /// Builds a with Identity, EF Core, logging, and configuration services configured + /// to enable test operator seeding via Werkr:SeedTestOperator and Werkr:TestOperatorPassword. + /// + private static ServiceProvider BuildProviderWithTestOperatorConfig( ) { + ServiceCollection services = new(); + + string dbName = $"IdentitySeederTests_{Guid.NewGuid()}"; + _ = services.AddDbContext( options => + options.UseInMemoryDatabase( dbName ) ); + + _ = services.AddIdentity( + IdentityExtensions.ConfigureIdentityOptions + ) + .AddEntityFrameworkStores( ) + .AddDefaultTokenProviders( ); + + _ = services.AddLogging( b => b.AddProvider( NullLoggerProvider.Instance ) ); + + _ = services.AddSingleton( + new ConfigurationBuilder( ).AddInMemoryCollection( new Dictionary { + ["Werkr:SeedTestOperator"] = "true", + ["Werkr:TestOperatorPassword"] = "TestPassword!1Aa", + } ).Build( ) + ); + + _ = services.AddSingleton( ); + + ServiceProvider provider = services.BuildServiceProvider(); + + ServerConfigCache configCache = provider.GetRequiredService(); + configCache.InitializeAsync( CancellationToken.None ).GetAwaiter( ).GetResult( ); + + return provider; + } } diff --git a/src/Werkr.Server/Identity/IdentitySeeder.cs b/src/Werkr.Server/Identity/IdentitySeeder.cs index 7485b06..3f28bd6 100644 --- a/src/Werkr.Server/Identity/IdentitySeeder.cs +++ b/src/Werkr.Server/Identity/IdentitySeeder.cs @@ -9,7 +9,8 @@ namespace Werkr.Server.Identity; /// -/// Seeds default roles, role-permission mappings, and an initial admin account on application startup. +/// Seeds default roles, role-permission mappings, an initial admin account, and optionally a test +/// operator account on application startup. /// public static partial class IdentitySeeder { /// @@ -114,6 +115,82 @@ public static async Task SeedAsync( IServiceProvider services ) { } } + // Optionally seed a test operator account for automated browser testing. + // Gated by Werkr:SeedTestOperator = true. Never enable in production. + IConfiguration config = services.GetRequiredService(); + if (string.Equals( config["Werkr:SeedTestOperator"], "true", StringComparison.OrdinalIgnoreCase )) { + await SeedTestOperatorAsync( services, userManager, config ); + } + } + + /// + /// Seeds a test operator account with a known password and no forced password-change or 2FA gates. + /// Intended exclusively for automated browser testing (e.g., BrowserTester agent) in non-production + /// environments. The password is read from the Werkr:TestOperatorPassword configuration key. + /// + private static async Task SeedTestOperatorAsync( + IServiceProvider services, + UserManager userManager, + IConfiguration configuration + ) { + const string Email = "operator@werkr.local"; + + WerkrUser? existing = await userManager.FindByEmailAsync(Email); + if (existing is not null) { + return; + } + + string? password = configuration["Werkr:TestOperatorPassword"]; + if (string.IsNullOrWhiteSpace( password )) { + ILogger logger = services.GetRequiredService() + .CreateLogger("Werkr.Identity.Seeder"); + LogTestOperatorPasswordMissing( logger ); + return; + } + + WerkrUser operatorUser = new() + { + UserName = Email, + Email = Email, + Name = "Test Operator", + Enabled = true, + ChangePassword = false, + Requires2FA = false, + EmailConfirmed = true, + }; + + IdentityResult result = await userManager.CreateAsync(operatorUser, password); + if (result.Succeeded) { + _ = await userManager.AddToRoleAsync( operatorUser, DefaultRoles.Operator.ToString( ) ); + + ILogger logger = services.GetRequiredService() + .CreateLogger("Werkr.Identity.Seeder"); + LogTestOperatorCreated( logger ); + + Console.WriteLine( ); + Console.WriteLine( "╔══════════════════════════════════════════════════════╗" ); + Console.WriteLine( "║ TEST OPERATOR ACCOUNT CREATED ║" ); + Console.WriteLine( "║ Email: operator@werkr.local ║" ); + Console.WriteLine( "║ ⚠ This account is for automated testing only ║" ); + Console.WriteLine( "╚══════════════════════════════════════════════════════╝" ); + Console.WriteLine( ); + + string? passwordFilePath = configuration["Werkr:WriteTestOperatorPasswordToFile"]; + if (!string.IsNullOrWhiteSpace( passwordFilePath )) { + try { + await File.WriteAllTextAsync( passwordFilePath, password ); + LogPasswordWritten( logger ); + } catch (Exception ex) { + LogPasswordWriteFailed( logger, ex, passwordFilePath ); + } + } + } else { + ILogger logger = services.GetRequiredService() + .CreateLogger("Werkr.Identity.Seeder"); + foreach (IdentityError error in result.Errors) { + LogTestOperatorCreateFailed( logger, error.Code, error.Description ); + } + } } /// @@ -188,4 +265,16 @@ private static string GenerateDefaultAdminPassword( ) { [LoggerMessage( Level = LogLevel.Error, Message = "Failed to create default admin: {Code} — {Description}" )] private static partial void LogAdminCreateFailed( ILogger logger, string code, string description ); + + [LoggerMessage( Level = LogLevel.Warning, + Message = "Test operator account created: operator@werkr.local — for automated testing only." )] + private static partial void LogTestOperatorCreated( ILogger logger ); + + [LoggerMessage( Level = LogLevel.Warning, + Message = "Werkr:SeedTestOperator is true but Werkr:TestOperatorPassword is not set. Skipping test operator seed." )] + private static partial void LogTestOperatorPasswordMissing( ILogger logger ); + + [LoggerMessage( Level = LogLevel.Error, + Message = "Failed to create test operator: {Code} — {Description}" )] + private static partial void LogTestOperatorCreateFailed( ILogger logger, string code, string description ); } diff --git a/src/Werkr.Server/Program.cs b/src/Werkr.Server/Program.cs index 9bb29ee..fbb812d 100644 --- a/src/Werkr.Server/Program.cs +++ b/src/Werkr.Server/Program.cs @@ -141,8 +141,10 @@ public static async Task Main( string[] args ) { // Dedicated SSE client for the long-lived event stream consumed by // JobEventRelayService. The global ConfigureHttpClientDefaults in // ServiceDefaults adds a standard resilience pipeline (10s attempt timeout) - // to all clients. We strip those handlers for the SSE client and add back - // a correctly-configured pipeline with infinite timeouts. + // to all clients. We strip those handlers for the SSE client because SSE + // streams are indefinitely long-lived and the resilience timeouts would + // kill the connection. JobEventRelayService manages its own reconnect loop + // with exponential backoff (1s → 30s) and exposes a health check. // Note: ResilienceHandler is a public type from Microsoft.Extensions.Http.Resilience. _ = builder.Services.AddHttpClient( "ApiServiceSse", client => { client.BaseAddress = new Uri( "https://api" ); @@ -152,21 +154,12 @@ public static async Task Main( string[] args ) { .ConfigureAdditionalHttpMessageHandlers( ( handlers, _ ) => { // Remove the global default resilience pipeline (10s attempt timeout) // added by ConfigureHttpClientDefaults — it kills long-lived SSE - // connections. We add back a correctly-configured pipeline below. - // IMPORTANT: This must run BEFORE AddStandardResilienceHandler so the - // strip removes the DEFAULT handler, not the one we're about to add. + // connections. for (int i = handlers.Count - 1; i >= 0; i--) { if (handlers[i] is ResilienceHandler) { handlers.RemoveAt( i ); } } - } ) - .AddStandardResilienceHandler( options => { - // SSE streams are indefinitely long-lived; disable timeout-based - // cancellation. The JobEventRelayService manages its own reconnect - // loop with exponential backoff (1s → 30s) and exposes a health check. - options.AttemptTimeout.Timeout = Timeout.InfiniteTimeSpan; - options.TotalRequestTimeout.Timeout = Timeout.InfiniteTimeSpan; } ); // Background health monitor — keeps agent DB status in sync with actual reachability diff --git a/src/Werkr.Server/appsettings.Development.json b/src/Werkr.Server/appsettings.Development.json index a41b13e..cb5708b 100644 --- a/src/Werkr.Server/appsettings.Development.json +++ b/src/Werkr.Server/appsettings.Development.json @@ -13,5 +13,9 @@ "SigningKey": "werkr-dev-signing-key-do-not-use-in-production-min32chars!", "Issuer": "werkr-api", "Audience": "werkr" + }, + "Werkr": { + "SeedTestOperator": true, + "TestOperatorPassword": "Operator!Dev2026#" } } diff --git a/src/Werkr.Server/graph-ui/src/dag/create-graph.ts b/src/Werkr.Server/graph-ui/src/dag/create-graph.ts index 099a8a4..ac5a253 100644 --- a/src/Werkr.Server/graph-ui/src/dag/create-graph.ts +++ b/src/Werkr.Server/graph-ui/src/dag/create-graph.ts @@ -77,6 +77,7 @@ export function createGraph( options: GraphOptions ): GraphResult { modifiers: "ctrl", rubberband: false, showNodeSelectionBox: true, + showEdgeSelectionBox: true, } ) ); // Alignment snaplines (visual aid only) diff --git a/src/Werkr.Server/graph-ui/src/dag/dag-editor.ts b/src/Werkr.Server/graph-ui/src/dag/dag-editor.ts index b82e13d..271430f 100644 --- a/src/Werkr.Server/graph-ui/src/dag/dag-editor.ts +++ b/src/Werkr.Server/graph-ui/src/dag/dag-editor.ts @@ -2,7 +2,7 @@ import type { Graph, Dnd } from "@antv/x6"; import type { DotNetObjectReference } from "../types/dotnet-interop"; import type { DagEdgeDto, EditorDagNodeDto, EditorNodeData, LayoutConfig } from "./dag-types"; import { createGraph } from "./create-graph"; -import { bindEditorEvents, bindKeyboardShortcuts } from "./editor-bindings"; +import { bindEditorEvents, bindKeyboardShortcuts, requestSuppressBlankClick } from "./editor-bindings"; import { setupDnd } from "./dnd-handler"; import { Changeset } from "./changeset"; import { saveDraft, loadDraft, clearDraft, hasDraft } from "./draft-storage"; @@ -190,6 +190,7 @@ export function addNode( ): void { if ( !graph ) return; console.log( "[werkr-dag] addNode stepId=", stepId, "x=", x, "y=", y ); + requestSuppressBlankClick(); const node = graph.addNode( { id: `step-${stepId}`, diff --git a/src/Werkr.Server/graph-ui/src/dag/dnd-handler.ts b/src/Werkr.Server/graph-ui/src/dag/dnd-handler.ts index e85616a..9b1c4e3 100644 --- a/src/Werkr.Server/graph-ui/src/dag/dnd-handler.ts +++ b/src/Werkr.Server/graph-ui/src/dag/dnd-handler.ts @@ -1,5 +1,6 @@ import type { Graph, Dnd } from "@antv/x6"; import type { DotNetObjectReference } from "../types/dotnet-interop"; +import { requestSuppressBlankClick } from "./editor-bindings"; /** @@ -47,6 +48,7 @@ export function setupDnd( if ( data?.actionType && !data.hasOwnProperty( "stepId" ) ) { const pos = node.getPosition(); console.log( "[werkr-dag] node:added (dnd drop)", data.actionType, pos.x, pos.y ); + requestSuppressBlankClick(); // Remove the placeholder — .NET will call addNode with proper data graph.removeNode( node.id ); dotNetRef.invokeMethodAsync( "OnNodeDroppedCallback", data.actionType, pos.x, pos.y ); diff --git a/src/Werkr.Server/graph-ui/src/dag/editor-bindings.ts b/src/Werkr.Server/graph-ui/src/dag/editor-bindings.ts index a281bed..27b8f79 100644 --- a/src/Werkr.Server/graph-ui/src/dag/editor-bindings.ts +++ b/src/Werkr.Server/graph-ui/src/dag/editor-bindings.ts @@ -4,20 +4,38 @@ import type { EditorNodeData } from "./dag-types"; import { wouldCreateCycle } from "./cycle-detection"; import { Changeset } from "./changeset"; import { copySelection, pasteSelection } from "./clipboard-handler"; +import { werkrEdgeDefaults } from "./werkr-edge"; let zoomDebounceTimer: ReturnType | null = null; // ── Blank-click suppression ── -// Shape.HTML nodes render inside in SVG. Safari (and sometimes -// Chrome) fires BOTH node:click AND blank:click for the same physical click on -// a Shape.HTML node, because the foreignObject click propagates to the SVG -// background. Timing-based approaches fail because the blank:click can be -// delayed by hundreds of milliseconds (Blazor Server round-trip + DOM patching). +// Shape.HTML nodes render inside in SVG. Chromium and Safari +// fire BOTH node:click AND blank:click for the same physical click, because +// the foreignObject click propagates to the SVG background. DnD drops also +// generate spurious blank:click events. // -// Strategy: When blank:click fires, we geometrically check whether the click -// coordinates fall inside any existing node's bounding box. If they do, the -// blank:click is a ghost event from foreignObject propagation and is suppressed. -// This is deterministic and has zero timing dependencies. +// Strategy: Flag-based suppression. When an interaction that should NOT be +// followed by a deselect occurs (node:click, addNode via DnD), we set a flag. +// The flag is auto-cleared via setTimeout(0) at the end of the current macro- +// task — long enough to catch any ghost blank:click fired synchronously from +// the same DOM event, but short enough to never suppress a genuine user click. +let suppressBlankClick = false; +let suppressClearTimer: ReturnType | null = null; + +/** + * Request that the next blank:click event be suppressed. + * Call this from any code path that will trigger a ghost blank:click + * (e.g. node:click, addNode, DnD drop). + * Auto-clears after the current macrotask via setTimeout(0). + */ +export function requestSuppressBlankClick(): void { + suppressBlankClick = true; + if (suppressClearTimer !== null) clearTimeout(suppressClearTimer); + suppressClearTimer = setTimeout(() => { + suppressBlankClick = false; + suppressClearTimer = null; + }, 0); +} /** * Bind editor-specific X6 graph events to .NET callbacks and changeset tracking. @@ -33,6 +51,7 @@ export function bindEditorEvents( const data = node.getData<{ stepId?: number; isLane?: boolean }>(); if ( data?.isLane ) return; if ( data?.stepId != null ) { + requestSuppressBlankClick(); console.log( "[werkr-dag] node:click stepId=", data.stepId ); graph.cleanSelection(); graph.select( node ); @@ -47,11 +66,16 @@ export function bindEditorEvents( container?.focus(); } ); - // ── Click on blank canvas → deselect (unless click is inside a node bbox) ── - // Ghost blank:click events from foreignObject propagation will have coordinates - // that fall inside the clicked node's bounding box. Genuine blank clicks will - // have coordinates that are outside all nodes. + // ── Click on blank canvas → deselect (unless suppressed by a recent interaction) ── graph.on( "blank:click", ( { x, y }: { e: MouseEvent; x: number; y: number } ) => { + // Flag guard — suppress ghost events from node:click / addNode / DnD + if (suppressBlankClick) { + suppressBlankClick = false; + if (suppressClearTimer !== null) { clearTimeout(suppressClearTimer); suppressClearTimer = null; } + console.log("[werkr-dag] blank:click SUPPRESSED — flag set by node interaction"); + return; + } + // Bbox fallback — suppress if click falls inside any node's bounding box for ( const node of graph.getNodes() ) { const bbox = node.getBBox(); if ( bbox.containsPoint( { x, y } ) ) { @@ -64,6 +88,25 @@ export function bindEditorEvents( dotNetRef.invokeMethodAsync( "OnNodeDeselectedCallback" ); } ); + // ── Edge click → select edge (so Delete/Backspace can remove it) ── + graph.on("edge:click", ({ edge }) => { + requestSuppressBlankClick(); + console.log("[werkr-dag] edge:click id=", edge.id); + graph.cleanSelection(); + graph.select(edge); + // Deselect any node in the config panel + dotNetRef.invokeMethodAsync("OnNodeDeselectedCallback"); + // Focus container for keyboard shortcuts + const container = graph.container; + if (container) { + if (!container.getAttribute("tabindex")) { + container.setAttribute("tabindex", "-1"); + container.style.outline = "none"; + } + container.focus(); + } + }); + // ── Zoom changed → C# callback (debounced 100ms) ── graph.on( "scale", ( { sx } ) => { if ( zoomDebounceTimer ) { @@ -104,16 +147,23 @@ export function bindEditorEvents( return; } - // Cycle check (exclude the just-added edge for the check since it's already in graph) - // We temporarily remove it, check, then re-add if valid + // Capture source/target info before removal (toJSON() fails on detached edges) + const edgeSource = edge.getSource(); + const edgeTarget = edge.getTarget(); + + // Cycle check: temporarily remove the edge so it doesn't appear in the graph graph.removeEdge( edge.id ); if ( wouldCreateCycle( graph, sourceCell.id, targetCell.id ) ) { dotNetRef.invokeMethodAsync( "OnCycleDetectedCallback", sourceData.stepId, targetData.stepId ); return; } - // Re-add the valid edge - graph.addEdge( edge.toJSON() ); + // Re-add the valid edge using captured source/target and standard defaults + graph.addEdge({ + source: edgeSource, + target: edgeTarget, + ...werkrEdgeDefaults, + }); changeset.addDependency( targetData.stepId, sourceData.stepId ); notifyDirty( dotNetRef, changeset ); } );
@run.Id.ToString()[..8]… @run.StartTime.ToString( "g" )