1
0
mirror of https://github.com/bitwarden/server synced 2025-12-13 14:53:34 +00:00

Use a header to track seeded data. This has benefits client side in simplicity and allows us to track entities added during a test, as long as they include the play id header.

This commit is contained in:
Matt Gibson
2025-11-10 18:17:41 -08:00
parent 0b22af53da
commit f2116734a2
20 changed files with 125 additions and 148 deletions

View File

@@ -1,4 +1,6 @@
using Microsoft.Extensions.Hosting; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Bit.Core.Services; namespace Bit.Core.Services;
@@ -11,3 +13,38 @@ public class PlayIdService(IHostEnvironment hostEnvironment) : IPlayIdService
return !string.IsNullOrEmpty(PlayId) && hostEnvironment.IsDevelopment(); return !string.IsNullOrEmpty(PlayId) && hostEnvironment.IsDevelopment();
} }
} }
public class PlayIdSingletonService(IHttpContextAccessor httpContextAccessor, IHostEnvironment hostEnvironment) : IPlayIdService
{
private PlayIdService Current
{
get
{
var httpContext = httpContextAccessor.HttpContext;
if (httpContext == null)
{
throw new InvalidOperationException("HttpContext is not available");
}
return httpContext.RequestServices.GetRequiredService<PlayIdService>();
}
}
public string? PlayId
{
get => Current.PlayId;
set => Current.PlayId = value;
}
public bool InPlay(out string playId)
{
if (hostEnvironment.IsDevelopment())
{
return Current.InPlay(out playId);
}
else
{
playId = string.Empty;
return false;
}
}
}

View File

@@ -1,12 +0,0 @@
namespace Bit.Infrastructure.EntityFramework.Models;
public class SeededData
{
public Guid Id { get; set; }
public required string RecipeName { get; set; }
/// <summary>
/// JSON blob containing all
/// </summary>
public required string Data { get; set; }
public DateTime CreationDate { get; set; }
}

View File

@@ -90,7 +90,6 @@ public class DatabaseContext : DbContext
public DbSet<OrganizationInstallation> OrganizationInstallations { get; set; } public DbSet<OrganizationInstallation> OrganizationInstallations { get; set; }
public DbSet<OrganizationReport> OrganizationReports { get; set; } public DbSet<OrganizationReport> OrganizationReports { get; set; }
public DbSet<OrganizationApplication> OrganizationApplications { get; set; } public DbSet<OrganizationApplication> OrganizationApplications { get; set; }
public DbSet<SeededData> SeededData { get; set; }
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {

View File

@@ -5,7 +5,7 @@ namespace Bit.SharedWeb.Utilities;
public sealed class PlayIdMiddleware(RequestDelegate next) public sealed class PlayIdMiddleware(RequestDelegate next)
{ {
public Task Invoke(HttpContext context, IPlayIdService playIdService) public Task Invoke(HttpContext context, PlayIdService playIdService)
{ {
if (context.Request.Headers.TryGetValue("x-play-id", out var playId)) if (context.Request.Headers.TryGetValue("x-play-id", out var playId))
{ {

View File

@@ -119,8 +119,11 @@ public static class ServiceCollectionExtensions
} }
// Include PlayIdService for tracking Play Ids in repositories // Include PlayIdService for tracking Play Ids in repositories
services.AddScoped<IPlayIdService, PlayIdService>(); // We need the http context accessor to use the Singleton version, which pulls from the scoped version
services.AddHttpContextAccessor();
services.AddSingleton<IPlayIdService, PlayIdSingletonService>();
services.AddScoped<PlayIdService>();
return provider; return provider;
} }

View File

@@ -5,8 +5,8 @@ CREATE TABLE [dbo].[PlayData] (
[OrganizationId] UNIQUEIDENTIFIER NULL, [OrganizationId] UNIQUEIDENTIFIER NULL,
[CreationDate] DATETIME2 (7) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_PlayData] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [PK_PlayData] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_PlayData_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]), CONSTRAINT [FK_PlayData_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_PlayData_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]), CONSTRAINT [FK_PlayData_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,
CONSTRAINT [CK_PlayData_UserOrOrganization] CHECK (([UserId] IS NOT NULL AND [OrganizationId] IS NULL) OR ([UserId] IS NULL AND [OrganizationId] IS NOT NULL)) CONSTRAINT [CK_PlayData_UserOrOrganization] CHECK (([UserId] IS NOT NULL AND [OrganizationId] IS NULL) OR ([UserId] IS NULL AND [OrganizationId] IS NOT NULL))
); );

View File

@@ -1,6 +0,0 @@
CREATE TABLE [dbo].[SeededData] (
[Id] UNIQUEIDENTIFIER NOT NULL,
[RecipeName] NVARCHAR (MAX) NOT NULL,
[Data] NVARCHAR (MAX) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
);

View File

@@ -1,10 +0,0 @@
IF OBJECT_ID('dbo.SeededData') IS NULL
BEGIN
CREATE TABLE [dbo].[SeededData] (
[Id] UNIQUEIDENTIFIER NOT NULL,
[RecipeName] NVARCHAR (MAX) NOT NULL,
[Data] NVARCHAR (MAX) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
);
END
GO

View File

@@ -8,8 +8,8 @@ BEGIN
[OrganizationId] UNIQUEIDENTIFIER NULL, [OrganizationId] UNIQUEIDENTIFIER NULL,
[CreationDate] DATETIME2 (7) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_PlayData] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [PK_PlayData] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_PlayData_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]), CONSTRAINT [FK_PlayData_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_PlayData_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]), CONSTRAINT [FK_PlayData_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,
CONSTRAINT [CK_PlayData_UserOrOrganization] CHECK (([UserId] IS NOT NULL AND [OrganizationId] IS NULL) OR ([UserId] IS NULL AND [OrganizationId] IS NOT NULL)) CONSTRAINT [CK_PlayData_UserOrOrganization] CHECK (([UserId] IS NOT NULL AND [OrganizationId] IS NULL) OR ([UserId] IS NULL AND [OrganizationId] IS NOT NULL))
); );

View File

@@ -1,7 +1,7 @@
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.Models;
namespace Bit.Seeder.Factories; namespace Bit.Seeder.Factories;

View File

@@ -1,6 +1,6 @@
using Bit.Core.Enums; using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Infrastructure.EntityFramework.Models;
using Bit.RustSDK; using Bit.RustSDK;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;

View File

@@ -3,7 +3,7 @@
public interface IScene public interface IScene
{ {
Type GetRequestType(); Type GetRequestType();
SceneResult<object?> Seed(object request); Task<SceneResult<object?>> SeedAsync(object request);
} }
/// <summary> /// <summary>
@@ -12,19 +12,19 @@ public interface IScene
/// <typeparam name="TRequest"></typeparam> /// <typeparam name="TRequest"></typeparam>
public interface IScene<TRequest> : IScene where TRequest : class public interface IScene<TRequest> : IScene where TRequest : class
{ {
SceneResult Seed(TRequest request); Task<SceneResult> SeedAsync(TRequest request);
Type IScene.GetRequestType() => typeof(TRequest); Type IScene.GetRequestType() => typeof(TRequest);
SceneResult<object?> IScene.Seed(object request) async Task<SceneResult<object?>> IScene.SeedAsync(object request)
{ {
var result = Seed((TRequest)request); var result = await SeedAsync((TRequest)request);
return new SceneResult(mangleMap: result.MangleMap, trackedEntities: result.TrackedEntities); return new SceneResult(mangleMap: result.MangleMap);
} }
} }
public interface IScene<TRequest, TResult> : IScene where TRequest : class where TResult : class public interface IScene<TRequest, TResult> : IScene where TRequest : class where TResult : class
{ {
SceneResult<TResult> Seed(TRequest request); Task<SceneResult<TResult>> SeedAsync(TRequest request);
Type IScene.GetRequestType() => typeof(TRequest); Type IScene.GetRequestType() => typeof(TRequest);
SceneResult<object?> IScene.Seed(object request) => (SceneResult<object?>)Seed((TRequest)request); async Task<SceneResult<object?>> IScene.SeedAsync(object request) => (SceneResult<object?>)await SeedAsync((TRequest)request);
} }

View File

@@ -1,4 +1,4 @@
using Bit.Infrastructure.EntityFramework.Models; using Bit.Core.Entities;
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Seeder.Factories; using Bit.Seeder.Factories;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;

View File

@@ -2,21 +2,19 @@
public class SceneResult : SceneResult<object?> public class SceneResult : SceneResult<object?>
{ {
public SceneResult(Dictionary<string, string?> mangleMap, Dictionary<string, List<Guid>> trackedEntities) public SceneResult(Dictionary<string, string?> mangleMap)
: base(result: null, mangleMap: mangleMap, trackedEntities: trackedEntities) { } : base(result: null, mangleMap: mangleMap) { }
} }
public class SceneResult<TResult> public class SceneResult<TResult>
{ {
public TResult Result { get; init; } public TResult Result { get; init; }
public Dictionary<string, string?> MangleMap { get; init; } public Dictionary<string, string?> MangleMap { get; init; }
public Dictionary<string, List<Guid>> TrackedEntities { get; init; }
public SceneResult(TResult result, Dictionary<string, string?> mangleMap, Dictionary<string, List<Guid>> trackedEntities) public SceneResult(TResult result, Dictionary<string, string?> mangleMap)
{ {
Result = result; Result = result;
MangleMap = mangleMap; MangleMap = mangleMap;
TrackedEntities = trackedEntities;
} }
public static explicit operator SceneResult<object?>(SceneResult<TResult> v) public static explicit operator SceneResult<object?>(SceneResult<TResult> v)
@@ -25,11 +23,11 @@ public class SceneResult<TResult>
if (result is null) if (result is null)
{ {
return new SceneResult<object?>(result: null, mangleMap: v.MangleMap, trackedEntities: v.TrackedEntities); return new SceneResult<object?>(result: null, mangleMap: v.MangleMap);
} }
else else
{ {
return new SceneResult<object?>(result: result, mangleMap: v.MangleMap, trackedEntities: v.TrackedEntities); return new SceneResult<object?>(result: result, mangleMap: v.MangleMap);
} }
} }
} }

View File

@@ -1,10 +1,10 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Core.Repositories;
using Bit.Seeder.Factories; using Bit.Seeder.Factories;
namespace Bit.Seeder.Scenes; namespace Bit.Seeder.Scenes;
public class SingleUserScene(DatabaseContext db, UserSeeder userSeeder) : IScene<SingleUserScene.Request> public class SingleUserScene(UserSeeder userSeeder, IUserRepository userRepository) : IScene<SingleUserScene.Request>
{ {
public class Request public class Request
{ {
@@ -14,12 +14,11 @@ public class SingleUserScene(DatabaseContext db, UserSeeder userSeeder) : IScene
public bool Premium { get; set; } = false; public bool Premium { get; set; } = false;
} }
public SceneResult Seed(Request request) public async Task<SceneResult> SeedAsync(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); await userRepository.CreateAsync(user);
db.SaveChanges();
return new SceneResult(mangleMap: userSeeder.GetMangleMap(user, new UserData return new SceneResult(mangleMap: userSeeder.GetMangleMap(user, new UserData
{ {
@@ -31,9 +30,6 @@ public class SingleUserScene(DatabaseContext db, UserSeeder userSeeder) : IScene
ApiKey = user.ApiKey, ApiKey = user.ApiKey,
Kdf = user.Kdf, Kdf = user.Kdf,
KdfIterations = user.KdfIterations, KdfIterations = user.KdfIterations,
}), trackedEntities: new Dictionary<string, List<Guid>> }));
{
["User"] = [user.Id]
});
} }
} }

View File

@@ -6,17 +6,18 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.SeederApi.Controllers; namespace Bit.SeederApi.Controllers;
[Route("seed")] [Route("seed")]
public class SeedController(ILogger<SeedController> logger, ISceneService sceneService) public class SeedController(ILogger<SeedController> logger, ISceneService sceneService, IServiceProvider serviceProvider)
: Controller : Controller
{ {
[HttpPost] [HttpPost]
public IActionResult Seed([FromBody] SeedRequestModel request) public async Task<IActionResult> Seed([FromBody] SeedRequestModel request)
{ {
logger.LogInformation("Received seed request {Provider}", serviceProvider.GetType().FullName);
logger.LogInformation("Seeding with template: {Template}", request.Template); logger.LogInformation("Seeding with template: {Template}", request.Template);
try try
{ {
SceneResponseModel response = sceneService.ExecuteScene(request.Template, request.Arguments); SceneResponseModel response = await sceneService.ExecuteScene(request.Template, request.Arguments);
return Json(response); return Json(response);
} }
@@ -36,24 +37,24 @@ public class SeedController(ILogger<SeedController> logger, ISceneService sceneS
} }
[HttpDelete("batch")] [HttpDelete("batch")]
public async Task<IActionResult> DeleteBatch([FromBody] List<Guid> seedIds) public async Task<IActionResult> DeleteBatch([FromBody] List<string> playIds)
{ {
logger.LogInformation("Deleting batch of seeded data with IDs: {SeedIds}", string.Join(", ", seedIds)); logger.LogInformation("Deleting batch of seeded data with IDs: {PlayIds}", string.Join(", ", playIds));
var aggregateException = new AggregateException(); var aggregateException = new AggregateException();
await Task.Run(async () => await Task.Run(async () =>
{ {
foreach (var seedId in seedIds) foreach (var playId in playIds)
{ {
try try
{ {
await sceneService.DestroyScene(seedId); await sceneService.DestroyScene(playId);
} }
catch (Exception ex) catch (Exception ex)
{ {
aggregateException = new AggregateException(aggregateException, ex); aggregateException = new AggregateException(aggregateException, ex);
logger.LogError(ex, "Error deleting seeded data: {SeedId}", seedId); logger.LogError(ex, "Error deleting seeded data: {SeedId}", playId);
} }
} }
}); });
@@ -72,20 +73,20 @@ public class SeedController(ILogger<SeedController> logger, ISceneService sceneS
}); });
} }
[HttpDelete("{seedId}")] [HttpDelete("{playId}")]
public async Task<IActionResult> Delete([FromRoute] Guid seedId) public async Task<IActionResult> Delete([FromRoute] string playId)
{ {
logger.LogInformation("Deleting seeded data with ID: {SeedId}", seedId); logger.LogInformation("Deleting seeded data with ID: {PlayId}", playId);
try try
{ {
var result = await sceneService.DestroyScene(seedId); var result = await sceneService.DestroyScene(playId);
return Json(result); return Json(result);
} }
catch (SceneExecutionException ex) catch (SceneExecutionException ex)
{ {
logger.LogError(ex, "Error deleting seeded data: {SeedId}", seedId); logger.LogError(ex, "Error deleting seeded data: {PlayId}", playId);
return BadRequest(new return BadRequest(new
{ {
Error = ex.Message, Error = ex.Message,
@@ -101,25 +102,23 @@ public class SeedController(ILogger<SeedController> logger, ISceneService sceneS
logger.LogInformation("Deleting all seeded data"); logger.LogInformation("Deleting all seeded data");
// Pull all Seeded Data ids // Pull all Seeded Data ids
var seededData = sceneService.GetAllSeededData();
var playIds = sceneService.GetAllPlayIds();
var aggregateException = new AggregateException(); var aggregateException = new AggregateException();
await Task.Run(async () => foreach (var playId in playIds)
{
foreach (var sd in seededData)
{ {
try try
{ {
await sceneService.DestroyScene(sd.Id); await sceneService.DestroyScene(playId);
} }
catch (Exception ex) catch (Exception ex)
{ {
aggregateException = new AggregateException(aggregateException, ex); aggregateException = new AggregateException(aggregateException, ex);
logger.LogError(ex, "Error deleting seeded data: {SeedId}", sd.Id); logger.LogError(ex, "Error deleting seeded data: {PlayId}", playId);
} }
} }
});
if (aggregateException.InnerExceptions.Count > 0) if (aggregateException.InnerExceptions.Count > 0)
{ {

View File

@@ -4,17 +4,15 @@ namespace Bit.SeederApi.Models.Response;
public class SceneResponseModel public class SceneResponseModel
{ {
public required Guid? SeedId { get; init; }
public required Dictionary<string, string?>? MangleMap { get; init; } public required Dictionary<string, string?>? MangleMap { get; init; }
public required object? Result { get; init; } public required object? Result { get; init; }
public static SceneResponseModel FromSceneResult<T>(SceneResult<T> sceneResult, Guid? seedId) public static SceneResponseModel FromSceneResult<T>(SceneResult<T> sceneResult)
{ {
return new SceneResponseModel return new SceneResponseModel
{ {
Result = sceneResult.Result, Result = sceneResult.Result,
MangleMap = sceneResult.MangleMap, MangleMap = sceneResult.MangleMap,
SeedId = seedId
}; };
} }
} }

View File

@@ -6,6 +6,7 @@ using Bit.SharedWeb.Utilities;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddHttpContextAccessor();
var globalSettings = builder.Services.AddGlobalSettingsServices(builder.Configuration, builder.Environment); var globalSettings = builder.Services.AddGlobalSettingsServices(builder.Configuration, builder.Environment);
@@ -26,6 +27,9 @@ builder.Services.AddQueries();
var app = builder.Build(); var app = builder.Build();
// Add PlayIdMiddleware services
app.UseMiddleware<PlayIdMiddleware>();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {

View File

@@ -1,5 +1,4 @@
using System.Text.Json; using System.Text.Json;
using Bit.Infrastructure.EntityFramework.Models;
using Bit.SeederApi.Models.Response; using Bit.SeederApi.Models.Response;
namespace Bit.SeederApi.Services; namespace Bit.SeederApi.Services;
@@ -14,14 +13,14 @@ public interface ISceneService
/// <returns>A tuple containing the result and optional seed ID for tracked entities</returns> /// <returns>A tuple containing the result and optional seed ID for tracked entities</returns>
/// <exception cref="SceneNotFoundException">Thrown when the scene template is not found</exception> /// <exception cref="SceneNotFoundException">Thrown when the scene template is not found</exception>
/// <exception cref="SceneExecutionException">Thrown when there's an error executing the scene</exception> /// <exception cref="SceneExecutionException">Thrown when there's an error executing the scene</exception>
SceneResponseModel ExecuteScene(string templateName, JsonElement? arguments); Task<SceneResponseModel> ExecuteScene(string templateName, JsonElement? arguments);
/// <summary> /// <summary>
/// Destroys data created by a scene using the seeded data ID. /// Destroys data created by a scene using the seeded data ID.
/// </summary> /// </summary>
/// <param name="seedId">The ID of the seeded data to destroy</param> /// <param name="playId">The ID of the seeded data to destroy</param>
/// <returns>The result of the destroy operation</returns> /// <returns>The result of the destroy operation</returns>
/// <exception cref="SceneExecutionException">Thrown when there's an error destroying the seeded data</exception> /// <exception cref="SceneExecutionException">Thrown when there's an error destroying the seeded data</exception>
Task<object?> DestroyScene(Guid seedId); Task<object?> DestroyScene(string playId);
List<SeededData> GetAllSeededData(); List<string> GetAllPlayIds();
} }

View File

@@ -1,6 +1,5 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Seeder; using Bit.Seeder;
using Bit.SeederApi.Models.Response; using Bit.SeederApi.Models.Response;
@@ -12,6 +11,7 @@ public class SceneService(
ILogger<SceneService> logger, ILogger<SceneService> logger,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IUserRepository userRepository, IUserRepository userRepository,
IPlayDataRepository playDataRepository,
IOrganizationRepository organizationRepository) IOrganizationRepository organizationRepository)
: ISceneService : ISceneService
{ {
@@ -21,35 +21,18 @@ public class SceneService(
PropertyNamingPolicy = JsonNamingPolicy.CamelCase PropertyNamingPolicy = JsonNamingPolicy.CamelCase
}; };
public List<SeededData> GetAllSeededData() public List<string> GetAllPlayIds()
{ {
return databaseContext.SeededData.ToList(); return [.. databaseContext.PlayData
.Select(pd => pd.PlayId)
.Distinct()];
} }
public SceneResponseModel ExecuteScene(string templateName, JsonElement? arguments) public async Task<SceneResponseModel> ExecuteScene(string templateName, JsonElement? arguments)
{ {
var result = ExecuteSceneMethod(templateName, arguments, "Seed"); var result = await ExecuteSceneMethod(templateName, arguments, "Seed");
if (result.TrackedEntities.Count == 0) return SceneResponseModel.FromSceneResult(result);
{
return SceneResponseModel.FromSceneResult(result, seedId: null);
}
var seededData = new SeededData
{
Id = Guid.NewGuid(),
RecipeName = templateName,
Data = JsonSerializer.Serialize(result.TrackedEntities),
CreationDate = DateTime.UtcNow
};
databaseContext.Add(seededData);
databaseContext.SaveChanges();
logger.LogInformation("Saved seeded data with ID {SeedId} for scene {SceneName}",
seededData.Id, templateName);
return SceneResponseModel.FromSceneResult(result, seededData.Id);
} }
public object ExecuteQuery(string queryName, JsonElement? arguments) public object ExecuteQuery(string queryName, JsonElement? arguments)
@@ -113,31 +96,23 @@ public class SceneService(
} }
} }
public async Task<object?> DestroyScene(Guid seedId) public async Task<object?> DestroyScene(string playId)
{ {
var seededData = databaseContext.SeededData.FirstOrDefault(s => s.Id == seedId); // Note, delete cascade will remove PlayData entries
if (seededData == null)
{
logger.LogInformation("No seeded data found with ID {SeedId}, skipping", seedId);
return null;
}
var trackedEntities = JsonSerializer.Deserialize<Dictionary<string, List<Guid>>>(seededData.Data); var playData = await playDataRepository.GetByPlayIdAsync(playId);
if (trackedEntities == null) var userIds = playData.Select(pd => pd.UserId).Distinct().ToList();
{ var organizationIds = playData.Select(pd => pd.OrganizationId).Distinct().ToList();
throw new SceneExecutionException($"Failed to deserialize tracked entities for seed ID {seedId}");
}
// Delete in reverse order to respect foreign key constraints // Delete Users before Oraganizations to respect foreign key constraints
if (trackedEntities.TryGetValue("User", out var userIds)) if (userIds.Count > 0)
{ {
var users = databaseContext.Users.Where(u => userIds.Contains(u.Id)); var users = databaseContext.Users.Where(u => userIds.Contains(u.Id));
await userRepository.DeleteManyAsync(users); await userRepository.DeleteManyAsync(users);
} }
if (organizationIds.Count > 0)
if (trackedEntities.TryGetValue("Organization", out var orgIds))
{ {
var organizations = databaseContext.Organizations.Where(o => orgIds.Contains(o.Id)); var organizations = databaseContext.Organizations.Where(o => organizationIds.Contains(o.Id));
var aggregateException = new AggregateException(); var aggregateException = new AggregateException();
foreach (var org in organizations) foreach (var org in organizations)
{ {
@@ -153,21 +128,18 @@ public class SceneService(
if (aggregateException.InnerExceptions.Count > 0) if (aggregateException.InnerExceptions.Count > 0)
{ {
throw new SceneExecutionException( throw new SceneExecutionException(
$"One or more errors occurred while deleting organizations for seed ID {seedId}", $"One or more errors occurred while deleting organizations for seed ID {playId}",
aggregateException); aggregateException);
} }
} }
databaseContext.Remove(seededData); logger.LogInformation("Successfully destroyed seeded data with ID {PlayId}",
databaseContext.SaveChanges(); playId);
logger.LogInformation("Successfully destroyed seeded data with ID {SeedId} for scene {SceneName}", return new { PlayId = playId };
seedId, seededData.RecipeName);
return new { SeedId = seedId, SceneName = seededData.RecipeName };
} }
private SceneResult<object?> ExecuteSceneMethod(string templateName, JsonElement? arguments, string methodName) private async Task<SceneResult<object?>> ExecuteSceneMethod(string templateName, JsonElement? arguments, string methodName)
{ {
try try
{ {
@@ -214,7 +186,7 @@ public class SceneService(
} }
} }
var result = scene.Seed(requestModel); var result = await scene.SeedAsync(requestModel);
logger.LogInformation("Successfully executed {MethodName} on scene: {TemplateName}", methodName, templateName); logger.LogInformation("Successfully executed {MethodName} on scene: {TemplateName}", methodName, templateName);
return result; return result;