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:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
);
|
|
||||||
@@ -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
|
|
||||||
@@ -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))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user