From 3f22adcbf295fb1accc1a07db4d7eea001b2d0cd Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 30 Oct 2025 09:08:36 -0700 Subject: [PATCH] Split scene service and query service rename instances of `recipe` to `scene` --- .vscode/launch.json | 22 ++++++ .vscode/tasks.json | 16 ++++ util/SeederApi/Controllers/QueryController.cs | 10 +-- util/SeederApi/Controllers/SeedController.cs | 18 ++--- util/SeederApi/Program.cs | 2 +- util/SeederApi/Services/IQueryService.cs | 17 ++++ util/SeederApi/Services/IRecipeService.cs | 38 --------- util/SeederApi/Services/ISceneService.cs | 27 +++++++ util/SeederApi/Services/QeuryExceptions.cs | 10 +++ util/SeederApi/Services/QueryService.cs | 77 +++++++++++++++++++ util/SeederApi/Services/RecipeExceptions.cs | 10 --- util/SeederApi/Services/SceneExceptions.cs | 10 +++ util/SeederApi/Services/SceneService.cs | 46 +++++------ 13 files changed, 217 insertions(+), 86 deletions(-) create mode 100644 util/SeederApi/Services/IQueryService.cs delete mode 100644 util/SeederApi/Services/IRecipeService.cs create mode 100644 util/SeederApi/Services/ISceneService.cs create mode 100644 util/SeederApi/Services/QeuryExceptions.cs create mode 100644 util/SeederApi/Services/QueryService.cs delete mode 100644 util/SeederApi/Services/RecipeExceptions.cs create mode 100644 util/SeederApi/Services/SceneExceptions.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index ce9e0a20d3..74115dcc86 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -69,6 +69,28 @@ "preLaunchTask": "buildFullServer", "stopAll": true }, + { + "name": "Full Server with Seeder API", + "configurations": [ + "run-Admin", + "run-API", + "run-Events", + "run-EventsProcessor", + "run-Identity", + "run-Sso", + "run-Icons", + "run-Billing", + "run-Notifications", + "run-SeederAPI" + ], + "presentation": { + "hidden": false, + "group": "AA_compounds", + "order": 6 + }, + "preLaunchTask": "buildFullServerWithSeederApi", + "stopAll": true + }, { "name": "Self Host: Bit", "configurations": [ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 64824ca22d..1df1c0d4a9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -54,6 +54,22 @@ "buildNotifications" ], }, + { + "label": "buildFullServerWithSeederApi", + "hide": true, + "dependsOrder": "sequence", + "dependsOn": [ + "buildAdmin", + "buildAPI", + "buildEventsProcessor", + "buildIdentity", + "buildSso", + "buildIcons", + "buildBilling", + "buildNotifications", + "buildSeederAPI" + ], + }, { "label": "buildSelfHostBit", "hide": true, diff --git a/util/SeederApi/Controllers/QueryController.cs b/util/SeederApi/Controllers/QueryController.cs index 592109d28d..d6a0dc1ccd 100644 --- a/util/SeederApi/Controllers/QueryController.cs +++ b/util/SeederApi/Controllers/QueryController.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Mvc; namespace Bit.SeederApi.Controllers; [Route("query")] -public class QueryController(ILogger logger, ISeedService recipeService) +public class QueryController(ILogger logger, IQueryService queryService) : Controller { [HttpPost] @@ -15,15 +15,15 @@ public class QueryController(ILogger logger, ISeedService recip try { - var result = recipeService.ExecuteQuery(request.Template, request.Arguments); + var result = queryService.ExecuteQuery(request.Template, request.Arguments); - return Json(new { Result = result }); + return Json(result); } - catch (RecipeNotFoundException ex) + catch (SceneNotFoundException ex) { return NotFound(new { Error = ex.Message }); } - catch (RecipeExecutionException ex) + catch (SceneExecutionException ex) { logger.LogError(ex, "Error executing query: {Query}", request.Template); return BadRequest(new diff --git a/util/SeederApi/Controllers/SeedController.cs b/util/SeederApi/Controllers/SeedController.cs index 619a92003f..c33d8d4b1a 100644 --- a/util/SeederApi/Controllers/SeedController.cs +++ b/util/SeederApi/Controllers/SeedController.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc; namespace Bit.SeederApi.Controllers; [Route("seed")] -public class SeedController(ILogger logger, ISeedService recipeService) +public class SeedController(ILogger logger, ISceneService sceneService) : Controller { [HttpPost] @@ -16,15 +16,15 @@ public class SeedController(ILogger logger, ISeedService recipeS try { - SceneResponseModel response = recipeService.ExecuteScene(request.Template, request.Arguments); + SceneResponseModel response = sceneService.ExecuteScene(request.Template, request.Arguments); return Json(response); } - catch (RecipeNotFoundException ex) + catch (SceneNotFoundException ex) { return NotFound(new { Error = ex.Message }); } - catch (RecipeExecutionException ex) + catch (SceneExecutionException ex) { logger.LogError(ex, "Error executing scene: {Template}", request.Template); return BadRequest(new @@ -48,7 +48,7 @@ public class SeedController(ILogger logger, ISeedService recipeS { try { - await recipeService.DestroyRecipe(seedId); + await sceneService.DestroyScene(seedId); } catch (Exception ex) { @@ -79,11 +79,11 @@ public class SeedController(ILogger logger, ISeedService recipeS try { - var result = await recipeService.DestroyRecipe(seedId); + var result = await sceneService.DestroyScene(seedId); return Json(result); } - catch (RecipeExecutionException ex) + catch (SceneExecutionException ex) { logger.LogError(ex, "Error deleting seeded data: {SeedId}", seedId); return BadRequest(new @@ -101,7 +101,7 @@ public class SeedController(ILogger logger, ISeedService recipeS logger.LogInformation("Deleting all seeded data"); // Pull all Seeded Data ids - var seededData = recipeService.GetAllSeededData(); + var seededData = sceneService.GetAllSeededData(); var aggregateException = new AggregateException(); @@ -111,7 +111,7 @@ public class SeedController(ILogger logger, ISeedService recipeS { try { - await recipeService.DestroyRecipe(sd.Id); + await sceneService.DestroyScene(sd.Id); } catch (Exception ex) { diff --git a/util/SeederApi/Program.cs b/util/SeederApi/Program.cs index af0d901c8d..44666e615f 100644 --- a/util/SeederApi/Program.cs +++ b/util/SeederApi/Program.cs @@ -19,7 +19,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(_ => new MangleId()); builder.Services.AddScenes(); builder.Services.AddQueries(); diff --git a/util/SeederApi/Services/IQueryService.cs b/util/SeederApi/Services/IQueryService.cs new file mode 100644 index 0000000000..6ac41da960 --- /dev/null +++ b/util/SeederApi/Services/IQueryService.cs @@ -0,0 +1,17 @@ +using System.Text.Json; + +namespace Bit.SeederApi.Services; + +public interface IQueryService +{ + /// + /// Executes a query with the given query name and arguments. + /// Queries are read-only and do not track entities or create seed IDs. + /// + /// The name of the query (e.g., "EmergencyAccessInviteQuery") + /// Optional JSON arguments to pass to the query's Execute method + /// The result of the query execution + /// Thrown when the query is not found + /// Thrown when there's an error executing the query + object ExecuteQuery(string queryName, JsonElement? arguments); +} diff --git a/util/SeederApi/Services/IRecipeService.cs b/util/SeederApi/Services/IRecipeService.cs deleted file mode 100644 index 6e4e101f35..0000000000 --- a/util/SeederApi/Services/IRecipeService.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Text.Json; -using Bit.Infrastructure.EntityFramework.Models; -using Bit.SeederApi.Models.Response; - -namespace Bit.SeederApi.Services; - -public interface ISeedService -{ - /// - /// Executes a scene with the given template name and arguments. - /// - /// The name of the scene template (e.g., "SingleUserScene") - /// Optional JSON arguments to pass to the scene's Seed method - /// A tuple containing the result and optional seed ID for tracked entities - /// Thrown when the scene template is not found - /// Thrown when there's an error executing the scene - SceneResponseModel ExecuteScene(string templateName, JsonElement? arguments); - - /// - /// Destroys data created by a scene using the seeded data ID. - /// - /// The ID of the seeded data to destroy - /// The result of the destroy operation - /// Thrown when there's an error destroying the seeded data - Task DestroyRecipe(Guid seedId); - List GetAllSeededData(); - - /// - /// Executes a query with the given query name and arguments. - /// Queries are read-only and do not track entities or create seed IDs. - /// - /// The name of the query (e.g., "EmergencyAccessInviteQuery") - /// Optional JSON arguments to pass to the query's Execute method - /// The result of the query execution - /// Thrown when the query is not found - /// Thrown when there's an error executing the query - object ExecuteQuery(string queryName, JsonElement? arguments); -} diff --git a/util/SeederApi/Services/ISceneService.cs b/util/SeederApi/Services/ISceneService.cs new file mode 100644 index 0000000000..b393f3b53d --- /dev/null +++ b/util/SeederApi/Services/ISceneService.cs @@ -0,0 +1,27 @@ +using System.Text.Json; +using Bit.Infrastructure.EntityFramework.Models; +using Bit.SeederApi.Models.Response; + +namespace Bit.SeederApi.Services; + +public interface ISceneService +{ + /// + /// Executes a scene with the given template name and arguments. + /// + /// The name of the scene template (e.g., "SingleUserScene") + /// Optional JSON arguments to pass to the scene's Seed method + /// A tuple containing the result and optional seed ID for tracked entities + /// Thrown when the scene template is not found + /// Thrown when there's an error executing the scene + SceneResponseModel ExecuteScene(string templateName, JsonElement? arguments); + + /// + /// Destroys data created by a scene using the seeded data ID. + /// + /// The ID of the seeded data to destroy + /// The result of the destroy operation + /// Thrown when there's an error destroying the seeded data + Task DestroyScene(Guid seedId); + List GetAllSeededData(); +} diff --git a/util/SeederApi/Services/QeuryExceptions.cs b/util/SeederApi/Services/QeuryExceptions.cs new file mode 100644 index 0000000000..beb0625cbb --- /dev/null +++ b/util/SeederApi/Services/QeuryExceptions.cs @@ -0,0 +1,10 @@ +namespace Bit.SeederApi.Services; + +public class QueryNotFoundException(string query) : Exception($"Query '{query}' not found"); + +public class QueryExecutionException : Exception +{ + public QueryExecutionException(string message) : base(message) { } + public QueryExecutionException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/util/SeederApi/Services/QueryService.cs b/util/SeederApi/Services/QueryService.cs new file mode 100644 index 0000000000..11e4564349 --- /dev/null +++ b/util/SeederApi/Services/QueryService.cs @@ -0,0 +1,77 @@ +using System.Text.Json; +using Bit.Seeder; + +namespace Bit.SeederApi.Services; + +public class QueryService( + ILogger logger, + IServiceProvider serviceProvider) + : IQueryService +{ + private static readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public object ExecuteQuery(string queryName, JsonElement? arguments) + { + try + { + var query = serviceProvider.GetKeyedService(queryName) + ?? throw new QueryNotFoundException(queryName); + + var requestType = query.GetRequestType(); + + // Deserialize the arguments into the request model + object? requestModel; + if (arguments == null) + { + // Try to create an instance with default values + try + { + requestModel = Activator.CreateInstance(requestType); + if (requestModel == null) + { + throw new QueryExecutionException( + $"Arguments are required for query '{queryName}'"); + } + } + catch + { + throw new QueryExecutionException( + $"Arguments are required for query '{queryName}'"); + } + } + else + { + try + { + requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, _jsonOptions); + if (requestModel == null) + { + throw new QueryExecutionException( + $"Failed to deserialize request model for query '{queryName}'"); + } + } + catch (JsonException ex) + { + throw new QueryExecutionException( + $"Failed to deserialize request model for query '{queryName}': {ex.Message}", ex); + } + } + + var result = query.Execute(requestModel); + + logger.LogInformation("Successfully executed query: {QueryName}", queryName); + return result; + } + catch (Exception ex) when (ex is not QueryNotFoundException and not QueryExecutionException) + { + logger.LogError(ex, "Unexpected error executing query: {QueryName}", queryName); + throw new QueryExecutionException( + $"An unexpected error occurred while executing query '{queryName}'", + ex.InnerException ?? ex); + } + } +} diff --git a/util/SeederApi/Services/RecipeExceptions.cs b/util/SeederApi/Services/RecipeExceptions.cs deleted file mode 100644 index 21c95589d3..0000000000 --- a/util/SeederApi/Services/RecipeExceptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Bit.SeederApi.Services; - -public class RecipeNotFoundException(string recipe) : Exception($"Recipe '{recipe}' not found"); - -public class RecipeExecutionException : Exception -{ - public RecipeExecutionException(string message) : base(message) { } - public RecipeExecutionException(string message, Exception innerException) - : base(message, innerException) { } -} diff --git a/util/SeederApi/Services/SceneExceptions.cs b/util/SeederApi/Services/SceneExceptions.cs new file mode 100644 index 0000000000..2d8da19629 --- /dev/null +++ b/util/SeederApi/Services/SceneExceptions.cs @@ -0,0 +1,10 @@ +namespace Bit.SeederApi.Services; + +public class SceneNotFoundException(string scene) : Exception($"Scene '{scene}' not found"); + +public class SceneExecutionException : Exception +{ + public SceneExecutionException(string message) : base(message) { } + public SceneExecutionException(string message, Exception innerException) + : base(message, innerException) { } +} diff --git a/util/SeederApi/Services/SceneService.cs b/util/SeederApi/Services/SceneService.cs index 96c4f2a527..d25a604f2c 100644 --- a/util/SeederApi/Services/SceneService.cs +++ b/util/SeederApi/Services/SceneService.cs @@ -7,13 +7,13 @@ using Bit.SeederApi.Models.Response; namespace Bit.SeederApi.Services; -public class SeedService( +public class SceneService( DatabaseContext databaseContext, - ILogger logger, + ILogger logger, IServiceProvider serviceProvider, IUserRepository userRepository, IOrganizationRepository organizationRepository) - : ISeedService + : ISceneService { private static readonly JsonSerializerOptions _jsonOptions = new() { @@ -46,7 +46,7 @@ public class SeedService( databaseContext.Add(seededData); databaseContext.SaveChanges(); - logger.LogInformation("Saved seeded data with ID {SeedId} for scene {RecipeName}", + logger.LogInformation("Saved seeded data with ID {SeedId} for scene {SceneName}", seededData.Id, templateName); return SceneResponseModel.FromSceneResult(result, seededData.Id); @@ -57,7 +57,7 @@ public class SeedService( try { var query = serviceProvider.GetKeyedService(queryName) - ?? throw new RecipeNotFoundException(queryName); + ?? throw new SceneNotFoundException(queryName); var requestType = query.GetRequestType(); @@ -71,13 +71,13 @@ public class SeedService( requestModel = Activator.CreateInstance(requestType); if (requestModel == null) { - throw new RecipeExecutionException( + throw new SceneExecutionException( $"Arguments are required for query '{queryName}'"); } } catch { - throw new RecipeExecutionException( + throw new SceneExecutionException( $"Arguments are required for query '{queryName}'"); } } @@ -88,13 +88,13 @@ public class SeedService( requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, _jsonOptions); if (requestModel == null) { - throw new RecipeExecutionException( + throw new SceneExecutionException( $"Failed to deserialize request model for query '{queryName}'"); } } catch (JsonException ex) { - throw new RecipeExecutionException( + throw new SceneExecutionException( $"Failed to deserialize request model for query '{queryName}': {ex.Message}", ex); } } @@ -104,16 +104,16 @@ public class SeedService( logger.LogInformation("Successfully executed query: {QueryName}", queryName); return result; } - catch (Exception ex) when (ex is not RecipeNotFoundException and not RecipeExecutionException) + catch (Exception ex) when (ex is not SceneNotFoundException and not SceneExecutionException) { logger.LogError(ex, "Unexpected error executing query: {QueryName}", queryName); - throw new RecipeExecutionException( + throw new SceneExecutionException( $"An unexpected error occurred while executing query '{queryName}'", ex.InnerException ?? ex); } } - public async Task DestroyRecipe(Guid seedId) + public async Task DestroyScene(Guid seedId) { var seededData = databaseContext.SeededData.FirstOrDefault(s => s.Id == seedId); if (seededData == null) @@ -125,7 +125,7 @@ public class SeedService( var trackedEntities = JsonSerializer.Deserialize>>(seededData.Data); if (trackedEntities == null) { - throw new RecipeExecutionException($"Failed to deserialize tracked entities for seed ID {seedId}"); + throw new SceneExecutionException($"Failed to deserialize tracked entities for seed ID {seedId}"); } // Delete in reverse order to respect foreign key constraints @@ -152,7 +152,7 @@ public class SeedService( } if (aggregateException.InnerExceptions.Count > 0) { - throw new RecipeExecutionException( + throw new SceneExecutionException( $"One or more errors occurred while deleting organizations for seed ID {seedId}", aggregateException); } @@ -161,10 +161,10 @@ public class SeedService( databaseContext.Remove(seededData); databaseContext.SaveChanges(); - logger.LogInformation("Successfully destroyed seeded data with ID {SeedId} for scene {RecipeName}", + logger.LogInformation("Successfully destroyed seeded data with ID {SeedId} for scene {SceneName}", seedId, seededData.RecipeName); - return new { SeedId = seedId, RecipeName = seededData.RecipeName }; + return new { SeedId = seedId, SceneName = seededData.RecipeName }; } private SceneResult ExecuteSceneMethod(string templateName, JsonElement? arguments, string methodName) @@ -172,7 +172,7 @@ public class SeedService( try { var scene = serviceProvider.GetKeyedService(templateName) - ?? throw new RecipeNotFoundException(templateName); + ?? throw new SceneNotFoundException(templateName); var requestType = scene.GetRequestType(); @@ -186,13 +186,13 @@ public class SeedService( requestModel = Activator.CreateInstance(requestType); if (requestModel == null) { - throw new RecipeExecutionException( + throw new SceneExecutionException( $"Arguments are required for scene '{templateName}'"); } } catch { - throw new RecipeExecutionException( + throw new SceneExecutionException( $"Arguments are required for scene '{templateName}'"); } } @@ -203,13 +203,13 @@ public class SeedService( requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, _jsonOptions); if (requestModel == null) { - throw new RecipeExecutionException( + throw new SceneExecutionException( $"Failed to deserialize request model for scene '{templateName}'"); } } catch (JsonException ex) { - throw new RecipeExecutionException( + throw new SceneExecutionException( $"Failed to deserialize request model for scene '{templateName}': {ex.Message}", ex); } } @@ -219,10 +219,10 @@ public class SeedService( logger.LogInformation("Successfully executed {MethodName} on scene: {TemplateName}", methodName, templateName); return result; } - catch (Exception ex) when (ex is not RecipeNotFoundException and not RecipeExecutionException) + catch (Exception ex) when (ex is not SceneNotFoundException and not SceneExecutionException) { logger.LogError(ex, "Unexpected error executing {MethodName} on scene: {TemplateName}", methodName, templateName); - throw new RecipeExecutionException( + throw new SceneExecutionException( $"An unexpected error occurred while executing {methodName} on scene '{templateName}'", ex.InnerException ?? ex); }