using System.Data; using Dapper; using PlotLine.Models; namespace PlotLine.Data; public interface IProjectBackupRepository { Task GetProjectBackupAsync(int projectId, int? userId = null); } public sealed class ProjectBackupRepository(ISqlConnectionFactory connectionFactory) : IProjectBackupRepository { public async Task GetProjectBackupAsync(int projectId, int? userId = null) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync( "dbo.ProjectBackup_Export", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure, commandTimeout: 180); var project = await result.ReadSingleOrDefaultAsync(); var data = new ProjectBackupData { Books = (await result.ReadAsync()).ToList(), Chapters = (await result.ReadAsync()).ToList(), Scenes = (await result.ReadAsync()).ToList(), ScenePurposes = (await result.ReadAsync()).ToList(), SceneOutcomes = (await result.ReadAsync()).ToList(), SceneMetricTypes = (await result.ReadAsync()).ToList(), SceneMetricValues = (await result.ReadAsync()).ToList(), TimelineSettings = (await result.ReadAsync()).ToList(), TimelineMetricSettings = (await result.ReadAsync()).ToList(), TimelineViewPresets = (await result.ReadAsync()).ToList(), PlotLines = (await result.ReadAsync()).ToList(), PlotThreads = (await result.ReadAsync()).ToList(), ThreadEvents = (await result.ReadAsync()).ToList(), StoryAssets = (await result.ReadAsync()).ToList(), AssetStates = (await result.ReadAsync()).ToList(), AssetEvents = (await result.ReadAsync()).ToList(), AssetDependencies = (await result.ReadAsync()).ToList(), AssetCustodyEvents = (await result.ReadAsync()).ToList(), AssetCustodyCharacters = (await result.ReadAsync()).ToList(), Characters = (await result.ReadAsync()).ToList(), SceneCharacters = (await result.ReadAsync()).ToList(), CharacterAttributeTypes = (await result.ReadAsync()).ToList(), CharacterAttributeEvents = (await result.ReadAsync()).ToList(), CharacterKnowledge = (await result.ReadAsync()).ToList(), CharacterRelationships = (await result.ReadAsync()).ToList(), RelationshipEvents = (await result.ReadAsync()).ToList(), Locations = (await result.ReadAsync()).ToList(), LocationRelationships = (await result.ReadAsync()).ToList(), FloorPlans = (await result.ReadAsync()).ToList(), FloorPlanFloors = (await result.ReadAsync()).ToList(), FloorPlanBlocks = (await result.ReadAsync()).ToList(), FloorPlanTransitions = (await result.ReadAsync()).ToList(), SceneAssetLocations = (await result.ReadAsync()).ToList(), SceneFloorPlanOccupancy = (await result.ReadAsync()).ToList(), SceneDependencies = (await result.ReadAsync()).ToList(), ValidationRuns = (await result.ReadAsync()).ToList(), ContinuityWarnings = (await result.ReadAsync()).ToList(), SceneNotes = (await result.ReadAsync()).ToList(), SceneWorkflow = (await result.ReadAsync()).ToList(), SceneChecklistItems = (await result.ReadAsync()).ToList(), SceneAttachments = (await result.ReadAsync()).ToList(), ChapterGoals = (await result.ReadAsync()).ToList(), Scenarios = (await result.ReadAsync()).ToList(), ScenarioSceneOrder = (await result.ReadAsync()).ToList(), ScenarioValidationRuns = (await result.ReadAsync()).ToList(), ScenarioWarnings = (await result.ReadAsync()).ToList(), GenreMetricPresets = (await result.ReadAsync()).ToList(), GenreMetricPresetMetrics = (await result.ReadAsync()).ToList(), ProjectAccess = (await result.ReadAsync()).ToList(), UserFiles = (await result.ReadAsync()).ToList() }; if (project is null) { return null; } var bookMetadata = (await connection.QueryAsync( """ SELECT BookID, Subtitle, Tagline, ShortDescription, Status, TargetWordCount, WritingStartDate, TargetCompletionDate, PublicationDate , CoverThumbnailPath, CoverStandardPath FROM dbo.Books WHERE ProjectID = @ProjectID; """, new { ProjectID = projectId })).ToDictionary(x => x.BookID); foreach (var book in data.Books) { if (!bookMetadata.TryGetValue(book.BookID, out var metadata)) { continue; } book.Subtitle = metadata.Subtitle; book.Tagline = metadata.Tagline; book.ShortDescription = metadata.ShortDescription; book.Status = metadata.Status; book.TargetWordCount = metadata.TargetWordCount; book.WritingStartDate = metadata.WritingStartDate; book.TargetCompletionDate = metadata.TargetCompletionDate; book.PublicationDate = metadata.PublicationDate; book.CoverThumbnailPath = metadata.CoverThumbnailPath; book.CoverStandardPath = metadata.CoverStandardPath; } return new ProjectBackupExport { ExportedAtUtc = DateTime.UtcNow, Project = project, Data = data, Diagnostics = new ProjectBackupDiagnostics { ProjectId = projectId, IncludedEntitySets = [ nameof(data.Books), nameof(data.Chapters), nameof(data.Scenes), nameof(data.ScenePurposes), nameof(data.SceneOutcomes), nameof(data.SceneMetricTypes), nameof(data.SceneMetricValues), nameof(data.TimelineSettings), nameof(data.TimelineMetricSettings), nameof(data.TimelineViewPresets), nameof(data.PlotLines), nameof(data.PlotThreads), nameof(data.ThreadEvents), nameof(data.StoryAssets), nameof(data.AssetStates), nameof(data.AssetEvents), nameof(data.AssetDependencies), nameof(data.AssetCustodyEvents), nameof(data.AssetCustodyCharacters), nameof(data.Characters), nameof(data.SceneCharacters), nameof(data.CharacterAttributeTypes), nameof(data.CharacterAttributeEvents), nameof(data.CharacterKnowledge), nameof(data.CharacterRelationships), nameof(data.RelationshipEvents), nameof(data.Locations), nameof(data.LocationRelationships), nameof(data.FloorPlans), nameof(data.FloorPlanFloors), nameof(data.FloorPlanBlocks), nameof(data.FloorPlanTransitions), nameof(data.SceneAssetLocations), nameof(data.SceneFloorPlanOccupancy), nameof(data.SceneDependencies), nameof(data.ValidationRuns), nameof(data.ContinuityWarnings), nameof(data.SceneNotes), nameof(data.SceneWorkflow), nameof(data.SceneChecklistItems), nameof(data.SceneAttachments), nameof(data.ChapterGoals), nameof(data.Scenarios), nameof(data.ScenarioSceneOrder), nameof(data.ScenarioValidationRuns), nameof(data.ScenarioWarnings), nameof(data.GenreMetricPresets), nameof(data.GenreMetricPresetMetrics), nameof(data.ProjectAccess), nameof(data.UserFiles) ], Note = "Original database IDs are included for diagnostics. Project access records are exported for reference only; importing a project backup creates a new project owned by the importing user and does not recreate collaborators." } }; } }