mirror of
https://github.com/bitwarden/server
synced 2025-12-15 15:53:59 +00:00
Refactor recipies into scenes
This commit is contained in:
@@ -36,7 +36,7 @@ public class EmergencyAccessUpdateRequestModel
|
||||
existingEmergencyAccess.KeyEncrypted = KeyEncrypted;
|
||||
}
|
||||
existingEmergencyAccess.Type = Type;
|
||||
existingEmergencyAccess.WaitTimeDays = WaitTimeDays;
|
||||
existingEmergencyAccess.WaitTimeDays = (short)WaitTimeDays;
|
||||
return existingEmergencyAccess;
|
||||
}
|
||||
}
|
||||
|
||||
15
util/Seeder/IScene.cs
Normal file
15
util/Seeder/IScene.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Bit.Seeder;
|
||||
|
||||
public interface IScene
|
||||
{
|
||||
Type GetRequestType();
|
||||
RecipeResult Seed(object request);
|
||||
}
|
||||
|
||||
public interface IScene<TRequest> : IScene where TRequest : class
|
||||
{
|
||||
RecipeResult Seed(TRequest request);
|
||||
|
||||
Type IScene.GetRequestType() => typeof(TRequest);
|
||||
RecipeResult IScene.Seed(object request) => Seed((TRequest)request);
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
using Bit.Core.Enums;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Seeder.Factories;
|
||||
|
||||
namespace Bit.Seeder.Recipes;
|
||||
namespace Bit.Seeder.Scenes;
|
||||
|
||||
public class SingleUserRecipe(DatabaseContext db, UserSeeder userSeeder)
|
||||
public class SingleUserScene(DatabaseContext db, UserSeeder userSeeder) : IScene<SingleUserScene.Request>
|
||||
{
|
||||
public RecipeResult Seed(string email, bool emailVerified = false, bool premium = false)
|
||||
public class Request
|
||||
{
|
||||
var user = userSeeder.CreateUser(email, emailVerified, premium);
|
||||
[Required]
|
||||
public required string Email { get; set; }
|
||||
public bool EmailVerified { get; set; } = false;
|
||||
public bool Premium { get; set; } = false;
|
||||
}
|
||||
|
||||
public RecipeResult Seed(Request request)
|
||||
{
|
||||
var user = userSeeder.CreateUser(request.Email, request.EmailVerified, request.Premium);
|
||||
|
||||
db.Add(user);
|
||||
db.SaveChanges();
|
||||
@@ -17,7 +26,7 @@ public class SingleUserRecipe(DatabaseContext db, UserSeeder userSeeder)
|
||||
{
|
||||
Result = userSeeder.GetMangleMap(user, new UserData
|
||||
{
|
||||
Email = email,
|
||||
Email = request.Email,
|
||||
Id = Guid.Parse("00000000-0000-0000-0000-000000000001"),
|
||||
Key = "seeded_key",
|
||||
PublicKey = "seeded_public_key",
|
||||
@@ -39,7 +39,7 @@ public class SeedController(ILogger<SeedController> logger, IRecipeService recip
|
||||
}
|
||||
catch (RecipeExecutionException ex)
|
||||
{
|
||||
logger.LogError(ex, "Error executing recipe: {Template}", request.Template);
|
||||
logger.LogError(ex, "Error executing scene: {Template}", request.Template);
|
||||
return BadRequest(new
|
||||
{
|
||||
Error = ex.Message,
|
||||
|
||||
29
util/SeederApi/Extensions/ServiceCollectionExtensions.cs
Normal file
29
util/SeederApi/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Reflection;
|
||||
using Bit.Seeder;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Bit.SeederApi.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Dynamically registers all scene types that implement IScene<TRequest> from the Seeder assembly.
|
||||
/// Scenes are registered as keyed scoped services using their class name as the key.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddScenes(this IServiceCollection services)
|
||||
{
|
||||
var seederAssembly = Assembly.Load("Seeder");
|
||||
var sceneTypes = seederAssembly.GetTypes()
|
||||
.Where(t => t is { IsClass: true, IsAbstract: false } &&
|
||||
t.GetInterfaces().Any(i => i.IsGenericType &&
|
||||
i.GetGenericTypeDefinition().Name == "IScene`1"));
|
||||
|
||||
foreach (var sceneType in sceneTypes)
|
||||
{
|
||||
services.TryAddScoped(sceneType);
|
||||
services.TryAddKeyedScoped(typeof(IScene), sceneType.Name, (sp, key) => sp.GetRequiredService(sceneType));
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Seeder;
|
||||
using Bit.SeederApi.Extensions;
|
||||
using Bit.SeederApi.Services;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
||||
@@ -20,6 +21,7 @@ builder.Services.AddSingleton<Bit.RustSDK.RustSdkService>();
|
||||
builder.Services.AddScoped<Bit.Seeder.Factories.UserSeeder>();
|
||||
builder.Services.AddScoped<IRecipeService, RecipeService>();
|
||||
builder.Services.AddScoped<MangleId>(_ => new MangleId());
|
||||
builder.Services.AddScenes();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@@ -6,17 +6,17 @@ namespace Bit.SeederApi.Services;
|
||||
public interface IRecipeService
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes a recipe with the given template name and arguments.
|
||||
/// Executes a scene with the given template name and arguments.
|
||||
/// </summary>
|
||||
/// <param name="templateName">The name of the recipe template (e.g., "OrganizationWithUsersRecipe")</param>
|
||||
/// <param name="arguments">Optional JSON arguments to pass to the recipe's Seed method</param>
|
||||
/// <param name="templateName">The name of the scene template (e.g., "SingleUserScene")</param>
|
||||
/// <param name="arguments">Optional JSON arguments to pass to the scene's Seed method</param>
|
||||
/// <returns>A tuple containing the result and optional seed ID for tracked entities</returns>
|
||||
/// <exception cref="RecipeNotFoundException">Thrown when the recipe template is not found</exception>
|
||||
/// <exception cref="RecipeExecutionException">Thrown when there's an error executing the recipe</exception>
|
||||
/// <exception cref="RecipeNotFoundException">Thrown when the scene template is not found</exception>
|
||||
/// <exception cref="RecipeExecutionException">Thrown when there's an error executing the scene</exception>
|
||||
(object? Result, Guid? SeedId) ExecuteRecipe(string templateName, JsonElement? arguments);
|
||||
|
||||
/// <summary>
|
||||
/// Destroys data created by a recipe using the seeded data ID.
|
||||
/// Destroys data created by a scene using the seeded data ID.
|
||||
/// </summary>
|
||||
/// <param name="seedId">The ID of the seeded data to destroy</param>
|
||||
/// <returns>The result of the destroy operation</returns>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
@@ -15,6 +14,12 @@ public class RecipeService(
|
||||
IOrganizationRepository organizationRepository)
|
||||
: IRecipeService
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public List<SeededData> GetAllSeededData()
|
||||
{
|
||||
return databaseContext.SeededData.ToList();
|
||||
@@ -45,7 +50,7 @@ public class RecipeService(
|
||||
databaseContext.Add(seededData);
|
||||
databaseContext.SaveChanges();
|
||||
|
||||
logger.LogInformation("Saved seeded data with ID {SeedId} for recipe {RecipeName}",
|
||||
logger.LogInformation("Saved seeded data with ID {SeedId} for scene {RecipeName}",
|
||||
seededData.Id, templateName);
|
||||
|
||||
return (Result: recipeResult.Result, SeedId: seededData.Id);
|
||||
@@ -99,118 +104,70 @@ public class RecipeService(
|
||||
databaseContext.Remove(seededData);
|
||||
databaseContext.SaveChanges();
|
||||
|
||||
logger.LogInformation("Successfully destroyed seeded data with ID {SeedId} for recipe {RecipeName}",
|
||||
logger.LogInformation("Successfully destroyed seeded data with ID {SeedId} for scene {RecipeName}",
|
||||
seedId, seededData.RecipeName);
|
||||
|
||||
return new { SeedId = seedId, RecipeName = seededData.RecipeName };
|
||||
}
|
||||
|
||||
private object? ExecuteRecipeMethod(string templateName, JsonElement? arguments, string methodName)
|
||||
private RecipeResult? ExecuteRecipeMethod(string templateName, JsonElement? arguments, string methodName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var recipeType = LoadRecipeType(templateName);
|
||||
var method = GetRecipeMethod(recipeType, templateName, methodName);
|
||||
var recipeInstance = CreateRecipeInstance(recipeType);
|
||||
var scene = serviceProvider.GetKeyedService<IScene>(templateName)
|
||||
?? throw new RecipeNotFoundException(templateName);
|
||||
|
||||
var methodArguments = ParseMethodArguments(method, arguments);
|
||||
var result = method.Invoke(recipeInstance, methodArguments);
|
||||
var requestType = scene.GetRequestType();
|
||||
|
||||
logger.LogInformation("Successfully executed {MethodName} on recipe: {TemplateName}", methodName, templateName);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex) when (ex is not RecipeNotFoundException and not RecipeExecutionException)
|
||||
{
|
||||
logger.LogError(ex, "Unexpected error executing {MethodName} on recipe: {TemplateName}", methodName, templateName);
|
||||
throw new RecipeExecutionException(
|
||||
$"An unexpected error occurred while executing {methodName} on recipe '{templateName}'",
|
||||
ex.InnerException ?? ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Type LoadRecipeType(string templateName)
|
||||
{
|
||||
var recipeTypeName = $"Bit.Seeder.Recipes.{templateName}";
|
||||
var recipeType = Assembly.Load("Seeder")
|
||||
.GetTypes()
|
||||
.FirstOrDefault(t => t.FullName == recipeTypeName);
|
||||
|
||||
return recipeType ?? throw new RecipeNotFoundException(templateName);
|
||||
}
|
||||
|
||||
private static MethodInfo GetRecipeMethod(Type recipeType, string templateName, string methodName)
|
||||
{
|
||||
var method = recipeType.GetMethod(methodName);
|
||||
return method ?? throw new RecipeExecutionException($"{methodName} method not found in recipe '{templateName}'");
|
||||
}
|
||||
|
||||
private object CreateRecipeInstance(Type recipeType)
|
||||
{
|
||||
var constructors = recipeType.GetConstructors();
|
||||
if (constructors.Length == 0)
|
||||
{
|
||||
throw new RecipeExecutionException($"No public constructors found for recipe type '{recipeType.Name}'");
|
||||
}
|
||||
|
||||
var constructor = constructors[0];
|
||||
var parameters = constructor.GetParameters();
|
||||
var constructorArgs = new object[parameters.Length];
|
||||
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var parameter = parameters[i];
|
||||
var service = serviceProvider.GetService(parameter.ParameterType);
|
||||
|
||||
if (service == null)
|
||||
// Deserialize the arguments into the request model
|
||||
object? requestModel;
|
||||
if (arguments == null)
|
||||
{
|
||||
throw new RecipeExecutionException(
|
||||
$"Unable to resolve service of type '{parameter.ParameterType.Name}' for recipe constructor");
|
||||
// Try to create an instance with default values
|
||||
try
|
||||
{
|
||||
requestModel = Activator.CreateInstance(requestType);
|
||||
if (requestModel == null)
|
||||
{
|
||||
throw new RecipeExecutionException(
|
||||
$"Arguments are required for scene '{templateName}'");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new RecipeExecutionException(
|
||||
$"Arguments are required for scene '{templateName}'");
|
||||
}
|
||||
}
|
||||
|
||||
constructorArgs[i] = service;
|
||||
}
|
||||
|
||||
return Activator.CreateInstance(recipeType, constructorArgs)!;
|
||||
}
|
||||
|
||||
private static object?[] ParseMethodArguments(MethodInfo seedMethod, JsonElement? arguments)
|
||||
{
|
||||
var parameters = seedMethod.GetParameters();
|
||||
|
||||
if (arguments == null && parameters.Length > 0)
|
||||
{
|
||||
throw new RecipeExecutionException("Arguments are required for this recipe");
|
||||
}
|
||||
|
||||
var methodArguments = new object?[parameters.Length];
|
||||
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
var parameter = parameters[i];
|
||||
var parameterName = parameter.Name!;
|
||||
|
||||
if (arguments?.TryGetProperty(parameterName, out var value) == true)
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
methodArguments[i] = JsonSerializer.Deserialize(value.GetRawText(), parameter.ParameterType);
|
||||
requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, _jsonOptions);
|
||||
if (requestModel == null)
|
||||
{
|
||||
throw new RecipeExecutionException(
|
||||
$"Failed to deserialize request model for scene '{templateName}'");
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new RecipeExecutionException(
|
||||
$"Failed to deserialize parameter '{parameterName}': {ex.Message}", ex);
|
||||
$"Failed to deserialize request model for scene '{templateName}': {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
else if (!parameter.HasDefaultValue)
|
||||
{
|
||||
throw new RecipeExecutionException($"Missing required parameter: {parameterName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
methodArguments[i] = parameter.DefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return methodArguments;
|
||||
var result = scene.Seed(requestModel);
|
||||
|
||||
logger.LogInformation("Successfully executed {MethodName} on scene: {TemplateName}", methodName, templateName);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex) when (ex is not RecipeNotFoundException and not RecipeExecutionException)
|
||||
{
|
||||
logger.LogError(ex, "Unexpected error executing {MethodName} on scene: {TemplateName}", methodName, templateName);
|
||||
throw new RecipeExecutionException(
|
||||
$"An unexpected error occurred while executing {methodName} on scene '{templateName}'",
|
||||
ex.InnerException ?? ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user