From 3b2b25eeabb958779b4f8a73770892de95c88643 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Mon, 24 Aug 2026 21:19:59 +0000 Subject: [PATCH] Fix admin reset and project hard delete --- PlotLine.Tests/Program.cs | 45 ++++ PlotLine/Controllers/AdminController.cs | 26 ++- PlotLine/Controllers/ProjectsController.cs | 8 +- PlotLine/Data/Repositories.cs | 199 +++++++++++++++++- PlotLine/Services/CoreServices.cs | 8 + ...toryIntelligenceDevelopmentResetService.cs | 27 +-- 6 files changed, 279 insertions(+), 34 deletions(-) diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 9fc3a09..775a6e6 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -104,6 +104,9 @@ var tests = new (string Name, Action Test)[] ("Word Companion writing screens receive resolved context identifiers", WordCompanionWritingScreensReceiveResolvedContextIdentifiers), ("Writing screens show diagnostic empty states", WritingScreensShowDiagnosticEmptyStates), ("Word Companion three-screen flow logs connection identifiers", WordCompanionThreeScreenFlowLogsConnectionIdentifiers), + ("Admin Story Intelligence reset returns to diagnostics with messages", AdminStoryIntelligenceResetReturnsToDiagnosticsWithMessages), + ("Project hard delete clears Story Intelligence dependencies in transaction", ProjectHardDeleteClearsStoryIntelligenceDependenciesInTransaction), + ("Permanent project delete keeps archive and name safety gates", PermanentProjectDeleteKeepsArchiveAndNameSafetyGates), ("Word manuscript cleanup classifies PlotDirector markers by tag only", WordManuscriptCleanupClassifiesPlotDirectorMarkersByTagOnly), ("Word manuscript cleanup preserves content controls content", WordManuscriptCleanupPreservesContentControlContent), ("Word manuscript cleanup UI separates unlink from clean unlink", WordManuscriptCleanupUiSeparatesUnlinkFromCleanUnlink), @@ -117,6 +120,48 @@ foreach (var test in tests) Console.WriteLine($"PASS {test.Name}"); } +static void AdminStoryIntelligenceResetReturnsToDiagnosticsWithMessages() +{ + var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine"); + var controller = File.ReadAllText(Path.Combine(root, "Controllers/AdminController.cs")); + var service = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceDevelopmentResetService.cs")); + + Assert(controller.Contains("try", StringComparison.Ordinal) + && controller.Contains("catch (Exception ex)", StringComparison.Ordinal), "Reset action should convert failures into a diagnostics-page error."); + Assert(controller.Contains("TempData[result.Success ? \"AdminMessage\" : \"AdminError\"]", StringComparison.Ordinal), "Reset action should show a clear success or error message."); + Assert(controller.Contains("return RedirectToAction(nameof(StoryIntelligenceDiagnostics))", StringComparison.Ordinal), "Reset action should return to the stable diagnostics route."); + Assert(service.Contains("IProjectRepository projects", StringComparison.Ordinal), "Reset should delegate project deletion to the shared hard-delete repository path."); + Assert(!service.Contains("DELETE FROM dbo.StoryIntelligenceRuns", StringComparison.Ordinal), "Reset should not delete run parents before hard-delete dependency cleanup."); +} + +static void ProjectHardDeleteClearsStoryIntelligenceDependenciesInTransaction() +{ + var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine"); + var repository = File.ReadAllText(Path.Combine(root, "Data/Repositories.cs")); + + Assert(repository.Contains("using var transaction = connection.BeginTransaction()", StringComparison.Ordinal), "Project hard delete should be wrapped by a repository transaction."); + Assert(repository.Contains("ProjectHardDeleteStoryIntelligenceDependencyCleanupSql", StringComparison.Ordinal), "Project hard delete should run scoped Story Intelligence dependency cleanup before the stored procedure."); + Assert(repository.Contains("AllowActiveProject = false", StringComparison.Ordinal), "Archived-project permanent delete must refuse active projects before cleanup."); + Assert(repository.Contains("THROW 52002, 'Only archived projects can be permanently deleted.'", StringComparison.Ordinal), "Hard delete cleanup should enforce the archive gate before deleting child rows."); + Assert(repository.Contains("CharacterIntelligenceEvidenceEvents", StringComparison.Ordinal), "Hard delete cleanup should remove character intelligence evidence rows."); + Assert(repository.Contains("CharacterIntelligenceProfiles", StringComparison.Ordinal), "Hard delete cleanup should remove character intelligence profile rows."); + Assert(repository.Contains("StoryMemoryProcessedSceneResults", StringComparison.Ordinal), "Hard delete cleanup should remove durable story memory scene-result rows."); + Assert(repository.Contains("StoryMemoryImportPreferences", StringComparison.Ordinal), "Hard delete cleanup should remove durable story memory import rows."); + Assert(repository.Contains("WHERE ProjectID = @ProjectID", StringComparison.Ordinal), "Hard delete cleanup must be scoped by project id."); +} + +static void PermanentProjectDeleteKeepsArchiveAndNameSafetyGates() +{ + var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine"); + var controller = File.ReadAllText(Path.Combine(root, "Controllers/ProjectsController.cs")); + var service = File.ReadAllText(Path.Combine(root, "Services/CoreServices.cs")); + + Assert(controller.Contains("GetArchivedOwnerProjectAsync(projectId)", StringComparison.Ordinal), "Permanent delete should require an archived owner project."); + Assert(controller.Contains("string.Equals(confirmationName?.Trim(), project.ProjectName, StringComparison.Ordinal)", StringComparison.Ordinal), "Permanent delete should require an exact project-name confirmation."); + Assert(service.Contains("return ProjectHardDeleteResult.ActiveProject()", StringComparison.Ordinal), "Permanent delete should reject non-archived projects."); + Assert(service.Contains("Blocking database dependency", StringComparison.Ordinal), "Permanent delete failures should report the blocking database dependency."); +} + static void RepairsTrailingDecimalConfidence() { var result = StoryIntelligenceJsonRepair.Repair("""{"observations":[{"confidence":0. }]}"""); diff --git a/PlotLine/Controllers/AdminController.cs b/PlotLine/Controllers/AdminController.cs index 84a984e..dd1eb4c 100644 --- a/PlotLine/Controllers/AdminController.cs +++ b/PlotLine/Controllers/AdminController.cs @@ -22,7 +22,8 @@ public sealed class AdminController( IStoryIntelligenceFullChapterTestService fullChapterTest, IStoryIntelligenceImportCommitService storyIntelligenceImportCommit, INameIntelligenceService nameIntelligence, - IIllustrationLibraryService illustrationLibrary) : Controller + IIllustrationLibraryService illustrationLibrary, + ILogger logger) : Controller { [HttpGet("")] [HttpGet("index")] @@ -198,8 +199,27 @@ public sealed class AdminController( return Forbid(); } - var result = await storyIntelligenceDevelopmentReset.ResetOwnedProjectsAndImportsAsync(userId, cancellationToken); - TempData[result.Success ? "AdminMessage" : "AdminError"] = result.Message; + logger.LogWarning("Story Intelligence development reset requested by admin user {UserId}.", userId); + + try + { + var result = await storyIntelligenceDevelopmentReset.ResetOwnedProjectsAndImportsAsync(userId, cancellationToken); + TempData[result.Success ? "AdminMessage" : "AdminError"] = result.Message; + logger.LogWarning( + "Story Intelligence development reset finished for admin user {UserId}. Success={Success} DeletedProjects={DeletedProjects} RemainingProjects={RemainingProjects} RemainingImports={RemainingImports} RemainingRuns={RemainingRuns}.", + userId, + result.Success, + result.DeletedProjects, + result.RemainingProjects, + result.RemainingImportSessions, + result.RemainingRuns); + } + catch (Exception ex) + { + logger.LogError(ex, "Story Intelligence development reset failed for admin user {UserId}.", userId); + TempData["AdminError"] = "Story Intelligence reset failed. No files were removed; check the application logs for the blocking dependency."; + } + return RedirectToAction(nameof(StoryIntelligenceDiagnostics)); } diff --git a/PlotLine/Controllers/ProjectsController.cs b/PlotLine/Controllers/ProjectsController.cs index 4c1e454..8bc5dfc 100644 --- a/PlotLine/Controllers/ProjectsController.cs +++ b/PlotLine/Controllers/ProjectsController.cs @@ -6,7 +6,7 @@ using PlotLine.ViewModels; namespace PlotLine.Controllers; [Authorize] -public sealed class ProjectsController(IProjectService projects, IProjectActivityService activity) : Controller +public sealed class ProjectsController(IProjectService projects, IProjectActivityService activity, ILogger logger) : Controller { public async Task Index() => View(await projects.ListAsync()); @@ -92,15 +92,19 @@ public sealed class ProjectsController(IProjectService projects, IProjectActivit [ValidateAntiForgeryToken] public async Task HardDeleteArchivedProject(int projectId, string? confirmationName) { + logger.LogWarning("Permanent project delete requested for archived project {ProjectID}.", projectId); + var project = await projects.GetArchivedOwnerProjectAsync(projectId); if (project is null) { + logger.LogWarning("Permanent project delete rejected for project {ProjectID}: project is not archived or current user is not owner.", projectId); TempData["ArchiveError"] = "Only an archived project owner can permanently delete a project."; return RedirectToAction(nameof(Index)); } if (!string.Equals(confirmationName?.Trim(), project.ProjectName, StringComparison.Ordinal)) { + logger.LogWarning("Permanent project delete rejected for archived project {ProjectID}: confirmation name did not match.", projectId); TempData["ArchiveError"] = "Type the project name exactly to permanently delete it."; return RedirectToAction(nameof(Index)); } @@ -108,10 +112,12 @@ public sealed class ProjectsController(IProjectService projects, IProjectActivit var result = await projects.HardDeleteAsync(projectId); if (result.Succeeded) { + logger.LogWarning("Permanent project delete completed for archived project {ProjectID}.", projectId); TempData["ArchiveMessage"] = "Project permanently deleted."; } else { + logger.LogWarning("Permanent project delete failed for archived project {ProjectID}: {Message}", projectId, result.Message); TempData["ArchiveError"] = result.NotArchived ? "Only archived projects can be permanently deleted." : result.Message; diff --git a/PlotLine/Data/Repositories.cs b/PlotLine/Data/Repositories.cs index 739c21f..1edd164 100644 --- a/PlotLine/Data/Repositories.cs +++ b/PlotLine/Data/Repositories.cs @@ -2098,12 +2098,163 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) : public async Task HardDeleteForOwnerAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); - await connection.ExecuteAsync( - "dbo.ProjectHardDelete_ForOwner", - new { ProjectID = projectId, UserID = userId }, - commandType: CommandType.StoredProcedure); + connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + await connection.ExecuteAsync( + ProjectHardDeleteStoryIntelligenceDependencyCleanupSql, + new { ProjectID = projectId, UserID = userId, AllowActiveProject = false }, + transaction, + commandTimeout: 120); + + await connection.ExecuteAsync( + "dbo.ProjectHardDelete_ForOwner", + new { ProjectID = projectId, UserID = userId }, + transaction, + commandType: CommandType.StoredProcedure); + + transaction.Commit(); + } + catch + { + TryRollback(transaction); + throw; + } } + private const string ProjectHardDeleteStoryIntelligenceDependencyCleanupSql = """ +SET NOCOUNT ON; + +IF NOT EXISTS +( + SELECT 1 + FROM dbo.Projects p + INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID + WHERE p.ProjectID = @ProjectID + AND pua.UserID = @UserID + AND pua.AccessRole = N'Owner' + AND pua.IsActive = 1 +) + THROW 52001, 'Project was not found for this owner.', 1; + +IF @AllowActiveProject = 0 AND EXISTS (SELECT 1 FROM dbo.Projects WHERE ProjectID = @ProjectID AND IsArchived = 0) + THROW 52002, 'Only archived projects can be permanently deleted.', 1; + +DECLARE @BookIDs TABLE (BookID int NOT NULL PRIMARY KEY); +DECLARE @ChapterIDs TABLE (ChapterID int NOT NULL PRIMARY KEY); +DECLARE @SceneIDs TABLE (SceneID int NOT NULL PRIMARY KEY); +DECLARE @StoryIntelligenceRunIDs TABLE (StoryIntelligenceRunID int NOT NULL PRIMARY KEY); +DECLARE @StoryIntelligenceChapterResultIDs TABLE (ChapterResultID int NOT NULL PRIMARY KEY); +DECLARE @StoryIntelligenceSceneResultIDs TABLE (SceneResultID int NOT NULL PRIMARY KEY); +DECLARE @ImportSessionIDs TABLE (ImportSessionID int NOT NULL PRIMARY KEY); +DECLARE @CharacterIntelligenceProfileIDs TABLE (CharacterIntelligenceProfileID int NOT NULL PRIMARY KEY); + +INSERT @BookIDs (BookID) +SELECT BookID FROM dbo.Books WHERE ProjectID = @ProjectID; + +INSERT @ChapterIDs (ChapterID) +SELECT c.ChapterID +FROM dbo.Chapters c +INNER JOIN @BookIDs b ON b.BookID = c.BookID; + +INSERT @SceneIDs (SceneID) +SELECT s.SceneID +FROM dbo.Scenes s +INNER JOIN @ChapterIDs c ON c.ChapterID = s.ChapterID; + +INSERT @ImportSessionIDs (ImportSessionID) +SELECT StoryIntelligenceBookPipelineID +FROM dbo.StoryIntelligenceBookPipelines pipeline +WHERE pipeline.ProjectID = @ProjectID + OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = pipeline.BookID); + +INSERT @StoryIntelligenceRunIDs (StoryIntelligenceRunID) +SELECT DISTINCT r.StoryIntelligenceRunID +FROM dbo.StoryIntelligenceRuns r +WHERE r.ProjectID = @ProjectID + OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = r.BookID) + OR EXISTS (SELECT 1 FROM @ChapterIDs c WHERE c.ChapterID = r.ChapterID) + OR EXISTS (SELECT 1 FROM @SceneIDs s INNER JOIN dbo.Scenes scene ON scene.SceneID = s.SceneID WHERE scene.ImportRunID = r.StoryIntelligenceRunID); + +INSERT @StoryIntelligenceChapterResultIDs (ChapterResultID) +SELECT DISTINCT cr.ChapterResultID +FROM dbo.StoryIntelligenceChapterResults cr +WHERE cr.ProjectID = @ProjectID + OR EXISTS (SELECT 1 FROM @StoryIntelligenceRunIDs r WHERE r.StoryIntelligenceRunID = cr.StoryIntelligenceRunID) + OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = cr.BookID) + OR EXISTS (SELECT 1 FROM @ChapterIDs c WHERE c.ChapterID = cr.ChapterID); + +INSERT @StoryIntelligenceSceneResultIDs (SceneResultID) +SELECT DISTINCT sr.SceneResultID +FROM dbo.StoryIntelligenceSceneResults sr +WHERE sr.ProjectID = @ProjectID + OR EXISTS (SELECT 1 FROM @StoryIntelligenceRunIDs r WHERE r.StoryIntelligenceRunID = sr.StoryIntelligenceRunID) + OR EXISTS (SELECT 1 FROM @StoryIntelligenceChapterResultIDs cr WHERE cr.ChapterResultID = sr.ChapterResultID) + OR EXISTS (SELECT 1 FROM @BookIDs b WHERE b.BookID = sr.BookID) + OR EXISTS (SELECT 1 FROM @ChapterIDs c WHERE c.ChapterID = sr.ChapterID) + OR EXISTS (SELECT 1 FROM @SceneIDs s WHERE s.SceneID = sr.SceneID); + +INSERT @CharacterIntelligenceProfileIDs (CharacterIntelligenceProfileID) +SELECT DISTINCT profile.CharacterIntelligenceProfileID +FROM dbo.CharacterIntelligenceProfiles profile +WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs sessionIDs WHERE sessionIDs.ImportSessionID = profile.ImportSessionID) + OR EXISTS (SELECT 1 FROM @StoryIntelligenceSceneResultIDs resultIDs WHERE resultIDs.SceneResultID = profile.LastUpdatedSceneResultID); + +DELETE evidence +FROM dbo.CharacterIntelligenceEvidenceEvents evidence +WHERE EXISTS (SELECT 1 FROM @CharacterIntelligenceProfileIDs profileIDs WHERE profileIDs.CharacterIntelligenceProfileID = evidence.CharacterIntelligenceProfileID) + OR EXISTS (SELECT 1 FROM @StoryIntelligenceSceneResultIDs resultIDs WHERE resultIDs.SceneResultID = evidence.SceneResultID); + +DELETE profileRows +FROM dbo.CharacterIntelligenceProfiles profileRows +WHERE EXISTS (SELECT 1 FROM @CharacterIntelligenceProfileIDs profileIDs WHERE profileIDs.CharacterIntelligenceProfileID = profileRows.CharacterIntelligenceProfileID); + +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) + OR EXISTS (SELECT 1 FROM @StoryIntelligenceSceneResultIDs resultIDs WHERE resultIDs.SceneResultID = StoryMemoryProcessedSceneResults.SceneResultID); + +DELETE FROM dbo.StoryMemoryImportPreferences +WHERE EXISTS (SELECT 1 FROM @ImportSessionIDs ids WHERE ids.ImportSessionID = StoryMemoryImportPreferences.ImportSessionID); +"""; + public async Task> ListHardDeleteFilePathsForAccountDeletionAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); @@ -2117,10 +2268,42 @@ public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) : public async Task HardDeleteForAccountDeletionAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); - await connection.ExecuteAsync( - "dbo.ProjectHardDelete_ForAccountDeletion", - new { ProjectID = projectId, UserID = userId }, - commandType: CommandType.StoredProcedure); + connection.Open(); + using var transaction = connection.BeginTransaction(); + + try + { + await connection.ExecuteAsync( + ProjectHardDeleteStoryIntelligenceDependencyCleanupSql, + new { ProjectID = projectId, UserID = userId, AllowActiveProject = true }, + transaction, + commandTimeout: 120); + + await connection.ExecuteAsync( + "dbo.ProjectHardDelete_ForAccountDeletion", + new { ProjectID = projectId, UserID = userId }, + transaction, + commandType: CommandType.StoredProcedure); + + transaction.Commit(); + } + catch + { + TryRollback(transaction); + throw; + } + } + + private static void TryRollback(IDbTransaction transaction) + { + try + { + transaction.Rollback(); + } + catch + { + // The stored procedure may have already rolled back the ambient transaction. + } } } diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index 06d78b9..5b15f49 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -3,6 +3,7 @@ using System.Net; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Options; using PlotLine.Data; using PlotLine.Models; @@ -917,6 +918,13 @@ public sealed class ProjectService( userId, filePaths.Count); } + catch (SqlException ex) + { + logger.LogError(ex, "Database hard delete failed for archived project {ProjectID} owned by user {UserID}. File cleanup was not attempted.", + projectId, + userId); + return ProjectHardDeleteResult.Failure($"Project could not be permanently deleted. Blocking database dependency: {ex.Errors[0].Message}"); + } catch (Exception ex) { logger.LogError(ex, "Database hard delete failed for archived project {ProjectID} owned by user {UserID}. File cleanup was not attempted.", diff --git a/PlotLine/Services/StoryIntelligenceDevelopmentResetService.cs b/PlotLine/Services/StoryIntelligenceDevelopmentResetService.cs index a4869bc..3ad66c3 100644 --- a/PlotLine/Services/StoryIntelligenceDevelopmentResetService.cs +++ b/PlotLine/Services/StoryIntelligenceDevelopmentResetService.cs @@ -10,23 +10,21 @@ public interface IStoryIntelligenceDevelopmentResetService public sealed class StoryIntelligenceDevelopmentResetService( ISqlConnectionFactory connectionFactory, + IProjectRepository projects, ILogger logger) : IStoryIntelligenceDevelopmentResetService { public async Task ResetOwnedProjectsAndImportsAsync(int userId, CancellationToken cancellationToken = default) { + logger.LogWarning("Admin Story Intelligence reset started for user {UserId}.", userId); + using var connection = connectionFactory.CreateConnection(); var command = new CommandDefinition(PrepareResetSql, new { UserID = userId }, cancellationToken: cancellationToken, commandTimeout: 120); var summary = await connection.QuerySingleAsync(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); + logger.LogWarning("Admin Story Intelligence reset hard-deleting archived project {ProjectID} for user {UserId}.", projectId, userId); + await projects.HardDeleteForOwnerAsync(projectId, userId); } var verificationCommand = new CommandDefinition(VerifyResetSql, new { UserID = userId }, cancellationToken: cancellationToken, commandTimeout: 120); @@ -154,21 +152,6 @@ END; 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()),