mirror of
https://github.com/bitwarden/server
synced 2025-12-14 15:23:42 +00:00
Add queries, rename seed to scene
This commit is contained in:
15
util/Seeder/IQuery.cs
Normal file
15
util/Seeder/IQuery.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Bit.Seeder;
|
||||||
|
|
||||||
|
public interface IQuery
|
||||||
|
{
|
||||||
|
Type GetRequestType();
|
||||||
|
object Execute(object request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IQuery<TRequest> : IQuery where TRequest : class
|
||||||
|
{
|
||||||
|
object Execute(TRequest request);
|
||||||
|
|
||||||
|
Type IQuery.GetRequestType() => typeof(TRequest);
|
||||||
|
object IQuery.Execute(object request) => Execute((TRequest)request);
|
||||||
|
}
|
||||||
@@ -3,13 +3,13 @@ namespace Bit.Seeder;
|
|||||||
public interface IScene
|
public interface IScene
|
||||||
{
|
{
|
||||||
Type GetRequestType();
|
Type GetRequestType();
|
||||||
RecipeResult Seed(object request);
|
SceneResult Seed(object request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IScene<TRequest> : IScene where TRequest : class
|
public interface IScene<TRequest> : IScene where TRequest : class
|
||||||
{
|
{
|
||||||
RecipeResult Seed(TRequest request);
|
SceneResult Seed(TRequest request);
|
||||||
|
|
||||||
Type IScene.GetRequestType() => typeof(TRequest);
|
Type IScene.GetRequestType() => typeof(TRequest);
|
||||||
RecipeResult IScene.Seed(object request) => Seed((TRequest)request);
|
SceneResult IScene.Seed(object request) => Seed((TRequest)request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
using Bit.Core.Auth.Enums;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
|
||||||
namespace Bit.Seeder.Recipes;
|
namespace Bit.Seeder.Queries;
|
||||||
|
|
||||||
public class EmergencyAccessInviteRecipe(
|
public class EmergencyAccessInviteQuery(
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer)
|
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer)
|
||||||
|
: IQuery<EmergencyAccessInviteQuery.Request>
|
||||||
{
|
{
|
||||||
public RecipeResult Seed(string email)
|
public class Request
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public required string Email { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Execute(Request request)
|
||||||
{
|
{
|
||||||
var invites = db.EmergencyAccesses
|
var invites = db.EmergencyAccesses
|
||||||
.Where(ea => ea.Email == email).ToList().Select(ea =>
|
.Where(ea => ea.Email == request.Email).ToList().Select(ea =>
|
||||||
{
|
{
|
||||||
var token = dataProtectorTokenizer.Protect(
|
var token = dataProtectorTokenizer.Protect(
|
||||||
new EmergencyAccessInviteTokenable(ea, hoursTillExpiration: 1)
|
new EmergencyAccessInviteTokenable(ea, hoursTillExpiration: 1)
|
||||||
@@ -20,9 +28,6 @@ public class EmergencyAccessInviteRecipe(
|
|||||||
return $"/accept-emergency?id={ea.Id}&name=Dummy&email={ea.Email}&token={token}";
|
return $"/accept-emergency?id={ea.Id}&name=Dummy&email={ea.Email}&token={token}";
|
||||||
});
|
});
|
||||||
|
|
||||||
return new RecipeResult
|
return invites;
|
||||||
{
|
|
||||||
Result = invites,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,8 @@ namespace Bit.Seeder.Recipes;
|
|||||||
|
|
||||||
public class OrganizationWithUsersRecipe(DatabaseContext db)
|
public class OrganizationWithUsersRecipe(DatabaseContext db)
|
||||||
{
|
{
|
||||||
public RecipeResult Seed(string name, int users, string domain)
|
public Guid Seed(string name, int users, string domain)
|
||||||
{
|
{
|
||||||
var mangleId = Guid.NewGuid();
|
|
||||||
var organization = OrganizationSeeder.CreateEnterprise(name, domain, users);
|
var organization = OrganizationSeeder.CreateEnterprise(name, domain, users);
|
||||||
var user = UserSeeder.CreateUserNoMangle($"admin@{domain}");
|
var user = UserSeeder.CreateUserNoMangle($"admin@{domain}");
|
||||||
var orgUser = organization.CreateOrganizationUser(user);
|
var orgUser = organization.CreateOrganizationUser(user);
|
||||||
@@ -33,14 +32,6 @@ public class OrganizationWithUsersRecipe(DatabaseContext db)
|
|||||||
db.BulkCopy(additionalUsers);
|
db.BulkCopy(additionalUsers);
|
||||||
db.BulkCopy(additionalOrgUsers);
|
db.BulkCopy(additionalOrgUsers);
|
||||||
|
|
||||||
return new RecipeResult
|
return organization.Id;
|
||||||
{
|
|
||||||
Result = organization.Id,
|
|
||||||
TrackedEntities = new Dictionary<string, List<Guid>>
|
|
||||||
{
|
|
||||||
["Organization"] = [organization.Id],
|
|
||||||
["User"] = [user.Id, .. additionalUsers.Select(u => u.Id)]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Bit.Seeder;
|
namespace Bit.Seeder;
|
||||||
|
|
||||||
public class RecipeResult
|
public class SceneResult
|
||||||
{
|
{
|
||||||
public required object Result { get; init; }
|
public required object Result { get; init; }
|
||||||
public Dictionary<string, List<Guid>> TrackedEntities { get; init; } = new();
|
public Dictionary<string, List<Guid>> TrackedEntities { get; init; } = new();
|
||||||
@@ -15,14 +15,14 @@ public class SingleUserScene(DatabaseContext db, UserSeeder userSeeder) : IScene
|
|||||||
public bool Premium { get; set; } = false;
|
public bool Premium { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecipeResult Seed(Request request)
|
public SceneResult Seed(Request request)
|
||||||
{
|
{
|
||||||
var user = userSeeder.CreateUser(request.Email, request.EmailVerified, request.Premium);
|
var user = userSeeder.CreateUser(request.Email, request.EmailVerified, request.Premium);
|
||||||
|
|
||||||
db.Add(user);
|
db.Add(user);
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
|
|
||||||
return new RecipeResult
|
return new SceneResult
|
||||||
{
|
{
|
||||||
Result = userSeeder.GetMangleMap(user, new UserData
|
Result = userSeeder.GetMangleMap(user, new UserData
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,172 +1,165 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using Bit.SeederApi.Models.Requests;
|
||||||
using System.Text.Json;
|
using Bit.SeederApi.Models.Response;
|
||||||
using Bit.SeederApi.Services;
|
using Bit.SeederApi.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Bit.SeederApi.Controllers;
|
namespace Bit.SeederApi.Controllers
|
||||||
|
|
||||||
public class SeedRequestModel
|
|
||||||
{
|
{
|
||||||
[Required]
|
[Route("")]
|
||||||
public required string Template { get; set; }
|
public class SeedController(ILogger<SeedController> logger, IRecipeService recipeService)
|
||||||
public JsonElement? Arguments { get; set; }
|
: Controller
|
||||||
}
|
|
||||||
|
|
||||||
[Route("")]
|
|
||||||
public class SeedController(ILogger<SeedController> logger, IRecipeService recipeService)
|
|
||||||
: Controller
|
|
||||||
{
|
|
||||||
[HttpPost("/seed")]
|
|
||||||
public IActionResult Seed([FromBody] SeedRequestModel request)
|
|
||||||
{
|
{
|
||||||
logger.LogInformation("Seeding with template: {Template}", request.Template);
|
[HttpPost("/query")]
|
||||||
|
public IActionResult Query([FromBody] SeedRequestModel request)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var (result, seedId) = recipeService.ExecuteRecipe(request.Template, request.Arguments);
|
logger.LogInformation("Executing query: {Query}", request.Template);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = recipeService.ExecuteQuery(request.Template, request.Arguments);
|
||||||
|
|
||||||
|
return Json(new { Result = result });
|
||||||
|
}
|
||||||
|
catch (RecipeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return NotFound(new { Error = ex.Message });
|
||||||
|
}
|
||||||
|
catch (RecipeExecutionException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error executing query: {Query}", request.Template);
|
||||||
|
return BadRequest(new
|
||||||
|
{
|
||||||
|
Error = ex.Message,
|
||||||
|
Details = ex.InnerException?.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("/seed")]
|
||||||
|
public IActionResult Seed([FromBody] SeedRequestModel request)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Seeding with template: {Template}", request.Template);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (result, seedId) = recipeService.ExecuteRecipe(request.Template, request.Arguments);
|
||||||
|
|
||||||
|
return Json(new SeedResponseModel
|
||||||
|
{
|
||||||
|
SeedId = seedId,
|
||||||
|
Result = result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (RecipeNotFoundException ex)
|
||||||
|
{
|
||||||
|
return NotFound(new { Error = ex.Message });
|
||||||
|
}
|
||||||
|
catch (RecipeExecutionException ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Error executing scene: {Template}", request.Template);
|
||||||
|
return BadRequest(new
|
||||||
|
{
|
||||||
|
Error = ex.Message,
|
||||||
|
Details = ex.InnerException?.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("/seed/batch")]
|
||||||
|
public async Task<IActionResult> DeleteBatch([FromBody] List<Guid> seedIds)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Deleting batch of seeded data with IDs: {SeedIds}", string.Join(", ", seedIds));
|
||||||
|
|
||||||
|
var aggregateException = new AggregateException();
|
||||||
|
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
foreach (var seedId in seedIds)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await recipeService.DestroyRecipe(seedId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
aggregateException = new AggregateException(aggregateException, ex);
|
||||||
|
logger.LogError(ex, "Error deleting seeded data: {SeedId}", seedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (aggregateException.InnerExceptions.Count > 0)
|
||||||
|
{
|
||||||
|
return BadRequest(new
|
||||||
|
{
|
||||||
|
Error = "One or more errors occurred while deleting seeded data",
|
||||||
|
Details = aggregateException.InnerExceptions.Select(e => e.Message).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
Message = "Seed completed successfully",
|
Message = "Batch delete completed successfully"
|
||||||
request.Template,
|
|
||||||
Result = result,
|
|
||||||
SeedId = seedId
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (RecipeNotFoundException ex)
|
|
||||||
{
|
|
||||||
return NotFound(new { Error = ex.Message });
|
|
||||||
}
|
|
||||||
catch (RecipeExecutionException ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error executing scene: {Template}", request.Template);
|
|
||||||
return BadRequest(new
|
|
||||||
{
|
|
||||||
Error = ex.Message,
|
|
||||||
Details = ex.InnerException?.Message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Unexpected error seeding with template: {Template}", request.Template);
|
|
||||||
return StatusCode(500, new
|
|
||||||
{
|
|
||||||
Error = "An unexpected error occurred while seeding",
|
|
||||||
Details = ex.Message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("/seed/batch")]
|
[HttpDelete("/seed/{seedId}")]
|
||||||
public async Task<IActionResult> DeleteBatch([FromBody] List<Guid> seedIds)
|
public async Task<IActionResult> Delete([FromRoute] Guid seedId)
|
||||||
{
|
|
||||||
logger.LogInformation("Deleting batch of seeded data with IDs: {SeedIds}", string.Join(", ", seedIds));
|
|
||||||
|
|
||||||
var aggregateException = new AggregateException();
|
|
||||||
|
|
||||||
await Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
foreach (var seedId in seedIds)
|
logger.LogInformation("Deleting seeded data with ID: {SeedId}", seedId);
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
try
|
var result = await recipeService.DestroyRecipe(seedId);
|
||||||
{
|
|
||||||
await recipeService.DestroyRecipe(seedId);
|
return Json(result);
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
aggregateException = new AggregateException(aggregateException, ex);
|
|
||||||
logger.LogError(ex, "Error deleting seeded data: {SeedId}", seedId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
catch (RecipeExecutionException ex)
|
||||||
|
|
||||||
if (aggregateException.InnerExceptions.Count > 0)
|
|
||||||
{
|
|
||||||
return BadRequest(new
|
|
||||||
{
|
{
|
||||||
Error = "One or more errors occurred while deleting seeded data",
|
logger.LogError(ex, "Error deleting seeded data: {SeedId}", seedId);
|
||||||
Details = aggregateException.InnerExceptions.Select(e => e.Message).ToList()
|
return BadRequest(new
|
||||||
});
|
|
||||||
}
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
Message = "Batch delete completed successfully"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("/seed/{seedId}")]
|
|
||||||
public async Task<IActionResult> Delete([FromRoute] Guid seedId)
|
|
||||||
{
|
|
||||||
logger.LogInformation("Deleting seeded data with ID: {SeedId}", seedId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await recipeService.DestroyRecipe(seedId);
|
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
Message = "Delete completed successfully",
|
|
||||||
Result = result
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (RecipeExecutionException ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Error deleting seeded data: {SeedId}", seedId);
|
|
||||||
return BadRequest(new
|
|
||||||
{
|
|
||||||
Error = ex.Message,
|
|
||||||
Details = ex.InnerException?.Message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Unexpected error deleting seeded data: {SeedId}", seedId);
|
|
||||||
return StatusCode(500, new
|
|
||||||
{
|
|
||||||
Error = "An unexpected error occurred while deleting",
|
|
||||||
Details = ex.Message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[HttpDelete("/seed")]
|
|
||||||
public async Task<IActionResult> DeleteAll()
|
|
||||||
{
|
|
||||||
logger.LogInformation("Deleting all seeded data");
|
|
||||||
|
|
||||||
// Pull all Seeded Data ids
|
|
||||||
var seededData = recipeService.GetAllSeededData();
|
|
||||||
|
|
||||||
var aggregateException = new AggregateException();
|
|
||||||
|
|
||||||
await Task.Run(async () =>
|
|
||||||
{
|
|
||||||
foreach (var sd in seededData)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
await recipeService.DestroyRecipe(sd.Id);
|
Error = ex.Message,
|
||||||
}
|
Details = ex.InnerException?.Message
|
||||||
catch (Exception ex)
|
});
|
||||||
{
|
|
||||||
aggregateException = new AggregateException(aggregateException, ex);
|
|
||||||
logger.LogError(ex, "Error deleting seeded data: {SeedId}", sd.Id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (aggregateException.InnerExceptions.Count > 0)
|
|
||||||
{
|
|
||||||
return BadRequest(new
|
|
||||||
{
|
|
||||||
Error = "One or more errors occurred while deleting seeded data",
|
|
||||||
Details = aggregateException.InnerExceptions.Select(e => e.Message).ToList()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return Ok(new
|
|
||||||
|
|
||||||
|
[HttpDelete("/seed")]
|
||||||
|
public async Task<IActionResult> DeleteAll()
|
||||||
{
|
{
|
||||||
Message = "All seeded data deleted successfully"
|
logger.LogInformation("Deleting all seeded data");
|
||||||
});
|
|
||||||
|
// Pull all Seeded Data ids
|
||||||
|
var seededData = recipeService.GetAllSeededData();
|
||||||
|
|
||||||
|
var aggregateException = new AggregateException();
|
||||||
|
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
foreach (var sd in seededData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await recipeService.DestroyRecipe(sd.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
aggregateException = new AggregateException(aggregateException, ex);
|
||||||
|
logger.LogError(ex, "Error deleting seeded data: {SeedId}", sd.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (aggregateException.InnerExceptions.Count > 0)
|
||||||
|
{
|
||||||
|
return BadRequest(new
|
||||||
|
{
|
||||||
|
Error = "One or more errors occurred while deleting seeded data",
|
||||||
|
Details = aggregateException.InnerExceptions.Select(e => e.Message).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,28 @@ public static class ServiceCollectionExtensions
|
|||||||
foreach (var sceneType in sceneTypes)
|
foreach (var sceneType in sceneTypes)
|
||||||
{
|
{
|
||||||
services.TryAddScoped(sceneType);
|
services.TryAddScoped(sceneType);
|
||||||
services.TryAddKeyedScoped(typeof(IScene), sceneType.Name, (sp, key) => sp.GetRequiredService(sceneType));
|
services.TryAddKeyedScoped(typeof(IScene), sceneType.Name, (sp, _) => sp.GetRequiredService(sceneType));
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dynamically registers all query types that implement IQuery<TRequest> from the Seeder assembly.
|
||||||
|
/// Queries are registered as keyed scoped services using their class name as the key.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddQueries(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
var seederAssembly = Assembly.Load("Seeder");
|
||||||
|
var queryTypes = seederAssembly.GetTypes()
|
||||||
|
.Where(t => t is { IsClass: true, IsAbstract: false } &&
|
||||||
|
t.GetInterfaces().Any(i => i.IsGenericType &&
|
||||||
|
i.GetGenericTypeDefinition().Name == "IQuery`1"));
|
||||||
|
|
||||||
|
foreach (var queryType in queryTypes)
|
||||||
|
{
|
||||||
|
services.TryAddScoped(queryType);
|
||||||
|
services.TryAddKeyedScoped(typeof(IQuery), queryType.Name, (sp, _) => sp.GetRequiredService(queryType));
|
||||||
}
|
}
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
11
util/SeederApi/Models/Request/SeedRequestModel.cs
Normal file
11
util/SeederApi/Models/Request/SeedRequestModel.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Bit.SeederApi.Models.Requests;
|
||||||
|
|
||||||
|
public class SeedRequestModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public required string Template { get; set; }
|
||||||
|
public JsonElement? Arguments { get; set; }
|
||||||
|
}
|
||||||
7
util/SeederApi/Models/Response/SeedResponseModel.cs
Normal file
7
util/SeederApi/Models/Response/SeedResponseModel.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.SeederApi.Models.Response;
|
||||||
|
|
||||||
|
public class SeedResponseModel
|
||||||
|
{
|
||||||
|
public Guid? SeedId { get; set; }
|
||||||
|
public object? Result { get; set; }
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ builder.Services.AddScoped<Bit.Seeder.Factories.UserSeeder>();
|
|||||||
builder.Services.AddScoped<IRecipeService, RecipeService>();
|
builder.Services.AddScoped<IRecipeService, RecipeService>();
|
||||||
builder.Services.AddScoped<MangleId>(_ => new MangleId());
|
builder.Services.AddScoped<MangleId>(_ => new MangleId());
|
||||||
builder.Services.AddScenes();
|
builder.Services.AddScenes();
|
||||||
|
builder.Services.AddQueries();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -23,4 +23,15 @@ public interface IRecipeService
|
|||||||
/// <exception cref="RecipeExecutionException">Thrown when there's an error destroying the seeded data</exception>
|
/// <exception cref="RecipeExecutionException">Thrown when there's an error destroying the seeded data</exception>
|
||||||
Task<object?> DestroyRecipe(Guid seedId);
|
Task<object?> DestroyRecipe(Guid seedId);
|
||||||
List<SeededData> GetAllSeededData();
|
List<SeededData> GetAllSeededData();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes a query with the given query name and arguments.
|
||||||
|
/// Queries are read-only and do not track entities or create seed IDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queryName">The name of the query (e.g., "EmergencyAccessInviteQuery")</param>
|
||||||
|
/// <param name="arguments">Optional JSON arguments to pass to the query's Execute method</param>
|
||||||
|
/// <returns>The result of the query execution</returns>
|
||||||
|
/// <exception cref="RecipeNotFoundException">Thrown when the query is not found</exception>
|
||||||
|
/// <exception cref="RecipeExecutionException">Thrown when there's an error executing the query</exception>
|
||||||
|
object ExecuteQuery(string queryName, JsonElement? arguments);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,21 +29,16 @@ public class RecipeService(
|
|||||||
{
|
{
|
||||||
var result = ExecuteRecipeMethod(templateName, arguments, "Seed");
|
var result = ExecuteRecipeMethod(templateName, arguments, "Seed");
|
||||||
|
|
||||||
if (result is not RecipeResult recipeResult)
|
if (result.TrackedEntities.Count == 0)
|
||||||
{
|
{
|
||||||
return (Result: result, SeedId: null);
|
return (Result: result.Result, SeedId: null);
|
||||||
}
|
|
||||||
|
|
||||||
if (recipeResult.TrackedEntities.Count == 0)
|
|
||||||
{
|
|
||||||
return (Result: recipeResult.Result, SeedId: null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var seededData = new SeededData
|
var seededData = new SeededData
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
RecipeName = templateName,
|
RecipeName = templateName,
|
||||||
Data = JsonSerializer.Serialize(recipeResult.TrackedEntities),
|
Data = JsonSerializer.Serialize(result.TrackedEntities),
|
||||||
CreationDate = DateTime.UtcNow
|
CreationDate = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,7 +48,68 @@ public class RecipeService(
|
|||||||
logger.LogInformation("Saved seeded data with ID {SeedId} for scene {RecipeName}",
|
logger.LogInformation("Saved seeded data with ID {SeedId} for scene {RecipeName}",
|
||||||
seededData.Id, templateName);
|
seededData.Id, templateName);
|
||||||
|
|
||||||
return (Result: recipeResult.Result, SeedId: seededData.Id);
|
return (Result: result.Result, SeedId: seededData.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ExecuteQuery(string queryName, JsonElement? arguments)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = serviceProvider.GetKeyedService<IQuery>(queryName)
|
||||||
|
?? throw new RecipeNotFoundException(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 RecipeExecutionException(
|
||||||
|
$"Arguments are required for query '{queryName}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new RecipeExecutionException(
|
||||||
|
$"Arguments are required for query '{queryName}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, _jsonOptions);
|
||||||
|
if (requestModel == null)
|
||||||
|
{
|
||||||
|
throw new RecipeExecutionException(
|
||||||
|
$"Failed to deserialize request model for query '{queryName}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new RecipeExecutionException(
|
||||||
|
$"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 RecipeNotFoundException and not RecipeExecutionException)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "Unexpected error executing query: {QueryName}", queryName);
|
||||||
|
throw new RecipeExecutionException(
|
||||||
|
$"An unexpected error occurred while executing query '{queryName}'",
|
||||||
|
ex.InnerException ?? ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object?> DestroyRecipe(Guid seedId)
|
public async Task<object?> DestroyRecipe(Guid seedId)
|
||||||
@@ -110,7 +166,7 @@ public class RecipeService(
|
|||||||
return new { SeedId = seedId, RecipeName = seededData.RecipeName };
|
return new { SeedId = seedId, RecipeName = seededData.RecipeName };
|
||||||
}
|
}
|
||||||
|
|
||||||
private RecipeResult? ExecuteRecipeMethod(string templateName, JsonElement? arguments, string methodName)
|
private SceneResult ExecuteRecipeMethod(string templateName, JsonElement? arguments, string methodName)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user