250 lines
11 KiB
C#
250 lines
11 KiB
C#
using Dapper;
|
|
using PlotLine.Data;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public interface IStoryIntelligenceDevelopmentResetService
|
|
{
|
|
Task<StoryIntelligenceDevelopmentResetResult> ResetOwnedProjectsAndImportsAsync(int userId, CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public sealed class StoryIntelligenceDevelopmentResetService(
|
|
ISqlConnectionFactory connectionFactory,
|
|
ILogger<StoryIntelligenceDevelopmentResetService> logger) : IStoryIntelligenceDevelopmentResetService
|
|
{
|
|
public async Task<StoryIntelligenceDevelopmentResetResult> ResetOwnedProjectsAndImportsAsync(int userId, CancellationToken cancellationToken = default)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
var command = new CommandDefinition(PrepareResetSql, new { UserID = userId }, cancellationToken: cancellationToken, commandTimeout: 120);
|
|
var summary = await connection.QuerySingleAsync<PreparedResetSummary>(command);
|
|
|
|
foreach (var projectId in summary.ProjectIdList)
|
|
{
|
|
var deleteCommand = new CommandDefinition(
|
|
"dbo.ProjectHardDelete_ForOwner",
|
|
new { ProjectID = projectId, UserID = userId },
|
|
commandType: System.Data.CommandType.StoredProcedure,
|
|
cancellationToken: cancellationToken,
|
|
commandTimeout: 120);
|
|
await connection.ExecuteAsync(deleteCommand);
|
|
}
|
|
|
|
var verificationCommand = new CommandDefinition(VerifyResetSql, new { UserID = userId }, cancellationToken: cancellationToken, commandTimeout: 120);
|
|
var verification = await connection.QuerySingleAsync<ResetVerification>(verificationCommand);
|
|
var result = new StoryIntelligenceDevelopmentResetResult(
|
|
summary.ProjectIdList.Count,
|
|
summary.ImportSessionCount,
|
|
summary.RunCount,
|
|
verification.RemainingProjects,
|
|
verification.RemainingImports,
|
|
verification.RemainingRuns,
|
|
verification.PreservedIllustrationItems);
|
|
|
|
logger.LogWarning(
|
|
"Admin Story Intelligence reset completed for user {UserId}. DeletedProjects={DeletedProjects} DeletedImports={DeletedImports} DeletedRuns={DeletedRuns} PreservedIllustrationItems={PreservedIllustrationItems}.",
|
|
userId,
|
|
result.DeletedProjects,
|
|
result.DeletedImportSessions,
|
|
result.DeletedRuns,
|
|
result.PreservedIllustrationItems);
|
|
|
|
return result;
|
|
}
|
|
|
|
private const string PrepareResetSql = """
|
|
SET NOCOUNT ON;
|
|
|
|
DECLARE @ProjectIDs TABLE (ProjectID int NOT NULL PRIMARY KEY);
|
|
DECLARE @BookIDs TABLE (BookID int NOT NULL PRIMARY KEY);
|
|
DECLARE @ImportSessionIDs TABLE (ImportSessionID int NOT NULL PRIMARY KEY);
|
|
DECLARE @RunIDs TABLE (StoryIntelligenceRunID int NOT NULL PRIMARY KEY);
|
|
|
|
INSERT @ProjectIDs (ProjectID)
|
|
SELECT DISTINCT p.ProjectID
|
|
FROM dbo.Projects p
|
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
|
WHERE pua.UserID = @UserID
|
|
AND pua.AccessRole = N'Owner'
|
|
AND pua.IsActive = 1;
|
|
|
|
INSERT @BookIDs (BookID)
|
|
SELECT b.BookID
|
|
FROM dbo.Books b
|
|
INNER JOIN @ProjectIDs p ON p.ProjectID = b.ProjectID;
|
|
|
|
INSERT @ImportSessionIDs (ImportSessionID)
|
|
SELECT StoryIntelligenceBookPipelineID
|
|
FROM dbo.StoryIntelligenceBookPipelines pipeline
|
|
WHERE EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = pipeline.ProjectID)
|
|
OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = pipeline.BookID);
|
|
|
|
INSERT @RunIDs (StoryIntelligenceRunID)
|
|
SELECT StoryIntelligenceRunID
|
|
FROM dbo.StoryIntelligenceRuns run
|
|
WHERE run.UserID = @UserID
|
|
AND (
|
|
run.ProjectID IS NULL
|
|
OR EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = run.ProjectID)
|
|
OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = run.BookID)
|
|
);
|
|
|
|
DECLARE @ImportSessionCount int = (SELECT COUNT(*) FROM @ImportSessionIDs);
|
|
DECLARE @RunCount int = (SELECT COUNT(*) FROM @RunIDs);
|
|
|
|
UPDATE dbo.StoryIntelligenceRuns
|
|
SET Status = CASE WHEN Status IN (N'Pending', N'Running') THEN N'Cancelled' ELSE Status END,
|
|
CurrentStage = N'Cancelled before admin reset',
|
|
CurrentMessage = N'Admin reset invalidated this run while preserving generated illustrations.',
|
|
CancellationRequestedUtc = COALESCE(CancellationRequestedUtc, SYSUTCDATETIME()),
|
|
CancelledUtc = COALESCE(CancelledUtc, SYSUTCDATETIME()),
|
|
CompletedUtc = COALESCE(CompletedUtc, SYSUTCDATETIME()),
|
|
UpdatedUtc = SYSUTCDATETIME()
|
|
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceRuns.StoryIntelligenceRunID)
|
|
AND Status IN (N'Pending', N'Running');
|
|
|
|
DELETE FROM dbo.StoryMemoryIllustrationAssignments
|
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryIllustrationAssignments.ImportSessionID);
|
|
|
|
DELETE FROM dbo.StoryMemoryIllustrationDemands
|
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryIllustrationDemands.ImportSessionID);
|
|
|
|
DELETE FROM dbo.StoryMemoryRelationships
|
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryRelationships.ImportSessionID);
|
|
|
|
DELETE FROM dbo.StoryMemoryCharacterAttributes
|
|
WHERE EXISTS
|
|
(
|
|
SELECT 1
|
|
FROM dbo.StoryMemoryCharacters c
|
|
INNER JOIN @ImportSessionIDs ids ON ids.ImportSessionID = c.ImportSessionID
|
|
WHERE c.StoryMemoryCharacterID = StoryMemoryCharacterAttributes.StoryMemoryCharacterID
|
|
);
|
|
|
|
DELETE FROM dbo.StoryMemoryCharacterAliases
|
|
WHERE EXISTS
|
|
(
|
|
SELECT 1
|
|
FROM dbo.StoryMemoryCharacters c
|
|
INNER JOIN @ImportSessionIDs ids ON ids.ImportSessionID = c.ImportSessionID
|
|
WHERE c.StoryMemoryCharacterID = StoryMemoryCharacterAliases.StoryMemoryCharacterID
|
|
);
|
|
|
|
DELETE FROM dbo.StoryMemoryAssets
|
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryAssets.ImportSessionID);
|
|
|
|
DELETE FROM dbo.StoryMemoryLocations
|
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryLocations.ImportSessionID);
|
|
|
|
DELETE FROM dbo.StoryMemoryCharacters
|
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryCharacters.ImportSessionID);
|
|
|
|
DELETE FROM dbo.StoryMemoryProcessedSceneResults
|
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryProcessedSceneResults.ImportSessionID);
|
|
|
|
DELETE FROM dbo.StoryMemoryImportPreferences
|
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryImportPreferences.ImportSessionID);
|
|
|
|
DELETE FROM dbo.StoryIntelligenceCharacterIllustrationAssignments
|
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryIntelligenceCharacterIllustrationAssignments.ImportSessionID)
|
|
OR EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = StoryIntelligenceCharacterIllustrationAssignments.ProjectID);
|
|
|
|
DELETE FROM dbo.StoryIntelligenceIllustrationDemands
|
|
WHERE EXISTS (SELECT 1 FROM @ProjectIDs p WHERE p.ProjectID = StoryIntelligenceIllustrationDemands.ProjectID);
|
|
|
|
DELETE FROM dbo.StoryIntelligenceBookPipelines
|
|
WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryIntelligenceBookPipelines.StoryIntelligenceBookPipelineID);
|
|
|
|
DELETE FROM dbo.StoryIntelligenceSceneResults
|
|
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceSceneResults.StoryIntelligenceRunID);
|
|
|
|
DELETE FROM dbo.StoryIntelligenceChapterResults
|
|
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceChapterResults.StoryIntelligenceRunID);
|
|
|
|
DELETE FROM dbo.StoryIntelligenceImportCommits
|
|
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceImportCommits.StoryIntelligenceRunID);
|
|
|
|
DELETE FROM dbo.StoryIntelligenceRuns
|
|
WHERE EXISTS (SELECT 1 FROM @RunIDs ids WHERE ids.StoryIntelligenceRunID = StoryIntelligenceRuns.StoryIntelligenceRunID);
|
|
|
|
UPDATE dbo.Projects
|
|
SET IsArchived = 1,
|
|
ArchivedDate = COALESCE(ArchivedDate, SYSUTCDATETIME()),
|
|
ArchivedReason = N'Admin Story Intelligence reset preserving generated illustrations',
|
|
UpdatedDate = SYSUTCDATETIME()
|
|
WHERE EXISTS (SELECT 1 FROM @ProjectIDs ids WHERE ids.ProjectID = Projects.ProjectID);
|
|
|
|
SELECT
|
|
ProjectIds = COALESCE(STRING_AGG(CONVERT(varchar(20), ProjectID), ','), ''),
|
|
ImportSessionCount = @ImportSessionCount,
|
|
RunCount = @RunCount
|
|
FROM @ProjectIDs;
|
|
""";
|
|
|
|
private const string VerifyResetSql = """
|
|
SELECT
|
|
RemainingProjects = (
|
|
SELECT COUNT(*)
|
|
FROM dbo.Projects p
|
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
|
WHERE pua.UserID = @UserID AND pua.AccessRole = N'Owner' AND pua.IsActive = 1
|
|
),
|
|
RemainingImports = (
|
|
SELECT COUNT(*)
|
|
FROM dbo.StoryIntelligenceBookPipelines pipeline
|
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = pipeline.ProjectID
|
|
WHERE pua.UserID = @UserID AND pua.AccessRole = N'Owner' AND pua.IsActive = 1
|
|
),
|
|
RemainingRuns = (
|
|
SELECT COUNT(*)
|
|
FROM dbo.StoryIntelligenceRuns
|
|
WHERE UserID = @UserID
|
|
),
|
|
PreservedIllustrationItems = (
|
|
SELECT COUNT(*)
|
|
FROM dbo.IllustrationLibraryItems
|
|
WHERE Category IN (N'Character', N'Location', N'Asset')
|
|
);
|
|
""";
|
|
|
|
private sealed class PreparedResetSummary
|
|
{
|
|
public string ProjectIds { get; init; } = string.Empty;
|
|
public int ImportSessionCount { get; init; }
|
|
public int RunCount { get; init; }
|
|
|
|
public IReadOnlyList<int> ProjectIdList => SplitProjectIds(ProjectIds);
|
|
}
|
|
|
|
private sealed class ResetVerification
|
|
{
|
|
public int RemainingProjects { get; init; }
|
|
public int RemainingImports { get; init; }
|
|
public int RemainingRuns { get; init; }
|
|
public int PreservedIllustrationItems { get; init; }
|
|
}
|
|
|
|
private static IReadOnlyList<int> SplitProjectIds(string projectIds)
|
|
=> string.IsNullOrWhiteSpace(projectIds)
|
|
? []
|
|
: projectIds.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Select(value => int.TryParse(value, out var id) ? id : 0)
|
|
.Where(id => id > 0)
|
|
.ToArray();
|
|
}
|
|
|
|
public sealed record StoryIntelligenceDevelopmentResetResult(
|
|
int DeletedProjects,
|
|
int DeletedImportSessions,
|
|
int DeletedRuns,
|
|
int RemainingProjects,
|
|
int RemainingImportSessions,
|
|
int RemainingRuns,
|
|
int PreservedIllustrationItems)
|
|
{
|
|
public bool Success => RemainingProjects == 0 && RemainingImportSessions == 0 && RemainingRuns == 0;
|
|
|
|
public string Message => Success
|
|
? $"Reset complete. Deleted {DeletedProjects:N0} project(s), {DeletedImportSessions:N0} import(s) and {DeletedRuns:N0} run(s). Preserved {PreservedIllustrationItems:N0} generated image record(s)."
|
|
: $"Reset finished with remaining data: {RemainingProjects:N0} project(s), {RemainingImportSessions:N0} import(s), {RemainingRuns:N0} run(s). Preserved {PreservedIllustrationItems:N0} generated image record(s).";
|
|
}
|