using System.Data; using Dapper; using PlotLine.Models; namespace PlotLine.Data; public interface IProjectRepository { Task> ListAsync(); Task GetAsync(int projectId); Task SaveAsync(Project project); Task ArchiveAsync(int projectId); } public interface IBookRepository { Task> ListByProjectAsync(int projectId); Task GetAsync(int bookId); Task SaveAsync(Book book); Task ArchiveAsync(int bookId); } public interface IChapterRepository { Task> ListByBookAsync(int bookId); Task GetAsync(int chapterId); Task SaveAsync(Chapter chapter); Task ArchiveAsync(int chapterId); } public interface ISceneRepository { Task> ListByChapterAsync(int chapterId); Task GetAsync(int sceneId); Task SaveAsync(Scene scene); Task SavePurposesAsync(int sceneId, IEnumerable purposeIds); Task SaveMetricValuesAsync(int sceneId, IEnumerable metricValues); Task ArchiveAsync(int sceneId); Task MoveAsync(int sceneId, string direction); Task> ListChapterOptionsAsync(int projectId); Task GetMovePreviewAsync(int sceneId, int targetChapterId, string position); Task MoveToChapterAsync(int sceneId, int targetChapterId, string position, bool renumber); Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position); Task RenumberChapterAsync(int chapterId); } public interface ILookupRepository { Task GetAllAsync(); } public interface ITimelineRepository { Task GetByProjectAsync(int projectId, int? bookId); } public interface ITimelinePresetRepository { Task> ListAsync(int projectId, int? bookId); Task GetAsync(int timelineViewPresetId); Task SaveAsync(TimelineViewPreset preset); Task ArchiveAsync(int timelineViewPresetId); } public interface IPlotRepository { Task GetLookupsAsync(); Task> ListPlotLinesAsync(int projectId); Task GetPlotLineAsync(int plotLineId); Task SavePlotLineAsync(PlotLineItem plotLine); Task ArchivePlotLineAsync(int plotLineId); Task> ListPlotThreadsByProjectAsync(int projectId); Task> ListPlotThreadsByPlotLineAsync(int plotLineId); Task GetPlotThreadAsync(int plotThreadId); Task SavePlotThreadAsync(PlotThread plotThread); Task ArchivePlotThreadAsync(int plotThreadId); Task> ListThreadEventsBySceneAsync(int sceneId); Task> ListThreadEventsByPlotThreadAsync(int plotThreadId); Task SaveThreadEventAsync(ThreadEvent threadEvent); Task DeleteThreadEventAsync(int threadEventId); Task> ListSceneOptionsAsync(int projectId); } public interface IAssetRepository { Task GetLookupsAsync(); Task> ListAssetsAsync(int projectId); Task GetAssetAsync(int storyAssetId); Task SaveAssetAsync(StoryAsset asset); Task ArchiveAssetAsync(int storyAssetId); Task> ListEventsBySceneAsync(int sceneId); Task> ListEventsByAssetAsync(int storyAssetId); Task SaveEventAsync(AssetEvent assetEvent); Task DeleteEventAsync(int assetEventId); Task> ListDependenciesByAssetAsync(int storyAssetId); Task SaveDependencyAsync(AssetDependency dependency); Task DeleteDependencyAsync(int assetDependencyId); Task> ListCustodyBySceneAsync(int sceneId); Task> ListCustodyByAssetAsync(int storyAssetId); Task SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable custodianCharacterIds, int custodyRoleId); Task DeleteCustodyEventAsync(int assetCustodyEventId); Task<(IReadOnlyList Assets, IReadOnlyList Events)> GetTimelineAsync(int projectId, int? bookId); Task> CheckDependencyIssuesAsync(int storyAssetId, int sceneId, int? toStateId); } public interface ICharacterRepository { Task GetLookupsAsync(); Task> ListCharactersAsync(int projectId); Task GetCharacterAsync(int characterId); Task SaveCharacterAsync(Character character); Task ArchiveCharacterAsync(int characterId); Task> ListSceneCharactersAsync(int sceneId); Task SaveSceneCharacterAsync(SceneCharacter sceneCharacter); Task DeleteSceneCharacterAsync(int sceneCharacterId); Task> ListAttributeEventsByCharacterAsync(int characterId); Task SaveAttributeEventAsync(CharacterAttributeEvent attributeEvent); Task> ListKnowledgeBySceneAsync(int sceneId); Task> ListKnowledgeByCharacterAsync(int characterId); Task SaveKnowledgeAsync(CharacterKnowledgeItem knowledge); Task DeleteKnowledgeAsync(int characterKnowledgeId); Task> ListRelationshipsByProjectAsync(int projectId); Task> ListRelationshipsByCharacterAsync(int characterId); Task SaveRelationshipAsync(CharacterRelationship relationship); Task ArchiveRelationshipAsync(int characterRelationshipId); Task> ListRelationshipEventsBySceneAsync(int sceneId); Task> ListRelationshipEventsByCharacterAsync(int characterId); Task SaveRelationshipEventAsync(RelationshipEvent relationshipEvent); Task DeleteRelationshipEventAsync(int relationshipEventId); Task<(IReadOnlyList Characters, IReadOnlyList Appearances)> GetTimelineAsync(int projectId, int? bookId); } public interface ILocationRepository { Task GetLookupsAsync(); Task> ListByProjectAsync(int projectId); Task GetAsync(int locationId); Task SaveAsync(LocationItem location); Task ArchiveAsync(int locationId); Task> ListRelationshipsByProjectAsync(int projectId); Task> ListRelationshipsByLocationAsync(int locationId); Task SaveRelationshipAsync(LocationRelationship relationship); Task ArchiveRelationshipAsync(int locationRelationshipId); Task> ListSceneAssetLocationsAsync(int sceneId); Task SaveSceneAssetLocationAsync(SceneAssetLocation sceneAssetLocation, bool updateCurrentLocation); Task DeleteSceneAssetLocationAsync(int sceneAssetLocationId); Task> ListScenesByLocationAsync(int locationId); Task> ListCharactersByLocationAsync(int locationId); Task> ListAssetsByLocationAsync(int locationId); } public interface IWarningRepository { Task GetLookupsAsync(); Task ValidateProjectAsync(int projectId); Task ValidateBookAsync(int bookId); Task ValidateSceneAsync(int sceneId); Task> ListAsync(int projectId, int? bookId, int? sceneId, int? warningTypeId, int? warningSeverityId, string? entityType, bool includeDismissed, bool includeIntentional); Task> ListBySceneAsync(int sceneId); Task> GetSceneCountsAsync(int projectId, int? bookId); Task DismissAsync(int continuityWarningId); Task MarkIntentionalAsync(int continuityWarningId); Task RestoreAsync(int continuityWarningId); Task FinaliseSceneDependencyWarningsAsync(int projectId, int? bookId); } public interface ISceneDependencyRepository { Task GetLookupsAsync(); Task> ListBySceneAsync(int sceneId); Task> ListByProjectAsync(int projectId, int? bookId); Task> GetCountsAsync(int projectId, int? bookId); Task SaveAsync(SceneDependency dependency); Task ArchiveAsync(int sceneDependencyId); } public interface IAnalyticsRepository { Task GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber); } public interface IStoryBibleRepository { Task> GetCharacterSummariesAsync(int projectId); Task> GetThreadSummariesAsync(int projectId); Task> GetAssetSummariesAsync(int projectId); Task> GetRelationshipSummariesAsync(int projectId); Task> GetLocationSummariesAsync(int projectId); Task> GetTimelineSummaryAsync(int projectId); Task> GetOpenQuestionsAsync(int projectId); Task> GetRevisionProgressAsync(int projectId); Task> SearchAsync(int projectId, string? query); } public interface IWriterWorkspaceRepository { Task> ListNoteTypesAsync(); Task> ListAttachmentTypesAsync(); Task GetWorkflowAsync(int sceneId); Task SaveWorkflowAsync(SceneWorkflow workflow); Task> ListNotesAsync(int sceneId); Task SaveNoteAsync(SceneNote note); Task DeleteNoteAsync(int sceneNoteId); Task> ListChecklistItemsAsync(int sceneId); Task SaveChecklistItemAsync(SceneChecklistItem item); Task DeleteChecklistItemAsync(int sceneChecklistItemId); Task> ListAttachmentsAsync(int sceneId); Task SaveAttachmentAsync(SceneAttachment attachment); Task DeleteAttachmentAsync(int sceneAttachmentId); Task GetChapterGoalAsync(int chapterId); Task SaveChapterGoalAsync(ChapterGoal goal); Task> GetChapterWorkflowAsync(int chapterId); Task> GetWriterDashboardScenesAsync(int? projectId, string queue); Task> GetWriterDashboardRemindersAsync(int? projectId); } public interface IExportRepository { Task GetStoryBibleAsync(int projectId); Task GetNarrativeTimelineAsync(int projectId); Task GetCharacterReferenceAsync(int characterId); Task GetSceneBriefAsync(int sceneId); Task GetChapterBriefAsync(int chapterId); Task GetWriterSessionBriefAsync(int sceneId); Task GetResearchPackAsync(int projectId); Task GetSearchResultsAsync(int projectId, string query); } public interface IDynamicsRepository { Task<(Character? Character, IReadOnlyList ArcPoints, IReadOnlyList RelationshipEvents, IReadOnlyList KnowledgeEvents, IReadOnlyList Issues)> GetCharacterArcAsync(int characterId); Task<(CharacterRelationship? Relationship, IReadOnlyList Events, IReadOnlyList Issues)> GetRelationshipEvolutionAsync(int characterRelationshipId); Task<(Character? Character, IReadOnlyList KnowledgeEvents, IReadOnlyList Issues)> GetKnowledgeTimelineAsync(int characterId); Task<(IReadOnlyList Pairings, IReadOnlyList Issues)> GetStoryDynamicsAsync(int projectId); Task RunContinuityChecksAsync(int projectId, int? bookId, int? sceneId); } public interface IScenarioRepository { Task> ListAsync(int projectId); Task GetAsync(int scenarioId); Task CreateAsync(int projectId, int? bookId, string scenarioName, string? description); Task<(IReadOnlyList Books, IReadOnlyList Chapters, IReadOnlyList Scenes, IReadOnlyList Warnings)> GetTimelineAsync(int scenarioId); Task MoveSceneAsync(int scenarioId, int sceneId, int? anchorSceneId, int? targetChapterId, string position); Task ValidateAsync(int scenarioId); Task<(IReadOnlyList MovedScenes, IReadOnlyList Warnings)> CompareAsync(int scenarioId); Task ApplyAsync(int scenarioId); Task ArchiveAsync(int scenarioId); } public interface IArchiveRepository { Task> ListAsync(int? projectId, string? entityType); Task> ListProjectFiltersAsync(); Task ArchiveAsync(string entityType, int entityId, string? reason); Task RestoreAsync(string entityType, int entityId); Task DeleteForeverAsync(string entityType, int entityId, string confirmationText); } public sealed class TimelineData { public Project? Project { get; init; } public IReadOnlyList Books { get; init; } = []; public IReadOnlyList Chapters { get; init; } = []; public IReadOnlyList Scenes { get; init; } = []; public IReadOnlyList PurposeLabels { get; init; } = []; public IReadOnlyList MetricTypes { get; init; } = []; public IReadOnlyList MetricPoints { get; init; } = []; public IReadOnlyList PlotLines { get; init; } = []; public IReadOnlyList ThreadEvents { get; init; } = []; } public sealed class LocationRepository(ISqlConnectionFactory connectionFactory) : ILocationRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.LocationLookup_All", commandType: CommandType.StoredProcedure); return new LocationLookupData { LocationTypes = (await result.ReadAsync()).ToList(), RelationshipTypes = (await result.ReadAsync()).ToList() }; } public async Task> ListByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Location_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Location_Get", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(LocationItem location) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Location_Save", new { location.LocationID, location.ProjectID, location.ParentLocationID, location.LocationName, location.LocationTypeID, location.Description }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Location_Archive", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); } public async Task> ListRelationshipsByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationRelationship_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListRelationshipsByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationRelationship_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveRelationshipAsync(LocationRelationship relationship) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.LocationRelationship_Save", new { relationship.LocationRelationshipID, relationship.FromLocationID, relationship.ToLocationID, relationship.LocationRelationshipTypeID, relationship.IsBidirectional, relationship.Strength, relationship.CanHearNormalSpeech, relationship.CanHearLoudNoise, relationship.CanSeeMovement, relationship.CanSeeClearly, relationship.CanTravelDirectly, relationship.Notes }, commandType: CommandType.StoredProcedure); } public async Task ArchiveRelationshipAsync(int locationRelationshipId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.LocationRelationship_Archive", new { LocationRelationshipID = locationRelationshipId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneAssetLocationsAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneAssetLocation_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveSceneAssetLocationAsync(SceneAssetLocation sceneAssetLocation, bool updateCurrentLocation) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneAssetLocation_Save", new { sceneAssetLocation.SceneAssetLocationID, sceneAssetLocation.SceneID, sceneAssetLocation.StoryAssetID, sceneAssetLocation.LocationID, sceneAssetLocation.Description, UpdateCurrentLocation = updateCurrentLocation }, commandType: CommandType.StoredProcedure); } public async Task DeleteSceneAssetLocationAsync(int sceneAssetLocationId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneAssetLocation_Delete", new { SceneAssetLocationID = sceneAssetLocationId }, commandType: CommandType.StoredProcedure); } public async Task> ListScenesByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationScene_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListCharactersByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationCharacter_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListAssetsByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneAssetLocation_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class WarningRepository(ISqlConnectionFactory connectionFactory) : IWarningRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.WarningLookup_All", commandType: CommandType.StoredProcedure); return new WarningLookupData { WarningTypes = (await result.ReadAsync()).ToList(), Severities = (await result.ReadAsync()).ToList() }; } public async Task ValidateProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ContinuityValidation_ValidateProject", new { ProjectID = projectId, TriggeredBy = "User" }, commandType: CommandType.StoredProcedure); } public async Task ValidateBookAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ContinuityValidation_ValidateBook", new { BookID = bookId, TriggeredBy = "User" }, commandType: CommandType.StoredProcedure); } public async Task ValidateSceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ContinuityValidation_ValidateScene", new { SceneID = sceneId, TriggeredBy = "User" }, commandType: CommandType.StoredProcedure); } public async Task> ListAsync(int projectId, int? bookId, int? sceneId, int? warningTypeId, int? warningSeverityId, string? entityType, bool includeDismissed, bool includeIntentional) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ContinuityWarning_List", new { ProjectID = projectId, BookID = bookId, SceneID = sceneId, WarningTypeID = warningTypeId, WarningSeverityID = warningSeverityId, EntityType = string.IsNullOrWhiteSpace(entityType) ? null : entityType, IncludeDismissed = includeDismissed, IncludeIntentional = includeIntentional }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ContinuityWarning_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetSceneCountsAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ContinuityWarning_SceneCounts", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task DismissAsync(int continuityWarningId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ContinuityWarning_Dismiss", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure); } public async Task MarkIntentionalAsync(int continuityWarningId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ContinuityWarning_MarkIntentional", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure); } public async Task RestoreAsync(int continuityWarningId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ContinuityWarning_Restore", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure); } public async Task FinaliseSceneDependencyWarningsAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneDependency_FinaliseWarnings", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); } } public sealed class AnalyticsData { public IReadOnlyList MetricPoints { get; init; } = []; public IReadOnlyList RevisionSummary { get; init; } = []; public IReadOnlyList RevisionAttentionScenes { get; init; } = []; public IReadOnlyList PurposeSummary { get; init; } = []; public IReadOnlyList SceneGaps { get; init; } = []; public IReadOnlyList ThreadHealth { get; init; } = []; public IReadOnlyList AssetHealth { get; init; } = []; public IReadOnlyList WarningSeverityCounts { get; init; } = []; public IReadOnlyList WarningTypeCounts { get; init; } = []; public IReadOnlyList WarningHotspots { get; init; } = []; public IReadOnlyList PovSummary { get; init; } = []; public IReadOnlyList MultiPovChapters { get; init; } = []; public IReadOnlyList CharacterAppearances { get; init; } = []; public IReadOnlyList BusyScenes { get; init; } = []; public IReadOnlyList LocationUsage { get; init; } = []; } public sealed class AnalyticsRepository(ISqlConnectionFactory connectionFactory) : IAnalyticsRepository { public async Task GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync( "dbo.Analytics_StoryHealth", new { ProjectID = projectId, BookID = bookId, StartChapterNumber = startChapterNumber, EndChapterNumber = endChapterNumber, StartSceneNumber = startSceneNumber, EndSceneNumber = endSceneNumber }, commandType: CommandType.StoredProcedure); return new AnalyticsData { MetricPoints = (await result.ReadAsync()).ToList(), RevisionSummary = (await result.ReadAsync()).ToList(), RevisionAttentionScenes = (await result.ReadAsync()).ToList(), PurposeSummary = (await result.ReadAsync()).ToList(), SceneGaps = (await result.ReadAsync()).ToList(), ThreadHealth = (await result.ReadAsync()).ToList(), AssetHealth = (await result.ReadAsync()).ToList(), WarningSeverityCounts = (await result.ReadAsync()).ToList(), WarningTypeCounts = (await result.ReadAsync()).ToList(), WarningHotspots = (await result.ReadAsync()).ToList(), PovSummary = (await result.ReadAsync()).ToList(), MultiPovChapters = (await result.ReadAsync()).ToList(), CharacterAppearances = (await result.ReadAsync()).ToList(), BusyScenes = (await result.ReadAsync()).ToList(), LocationUsage = (await result.ReadAsync()).ToList() }; } } public sealed class StoryBibleRepository(ISqlConnectionFactory connectionFactory) : IStoryBibleRepository { public async Task> GetCharacterSummariesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_CharacterSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetThreadSummariesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_ThreadSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetAssetSummariesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_AssetSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetRelationshipSummariesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_RelationshipSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetLocationSummariesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_LocationSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetTimelineSummaryAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_TimelineSummary", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetOpenQuestionsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_OpenQuestions", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetRevisionProgressAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_RevisionProgress", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> SearchAsync(int projectId, string? query) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_Search", new { ProjectID = projectId, Query = query }, commandType: CommandType.StoredProcedure)).ToList(); } } public sealed class WriterWorkspaceRepository(ISqlConnectionFactory connectionFactory) : IWriterWorkspaceRepository { public async Task> ListNoteTypesAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneNoteType_List", commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListAttachmentTypesAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneAttachmentType_List", commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetWorkflowAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync("dbo.SceneWorkflow_Get", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); } public async Task SaveWorkflowAsync(SceneWorkflow workflow) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.SceneWorkflow_Save", new { workflow.SceneID, workflow.DraftStatus, workflow.EstimatedWordCount, workflow.ActualWordCount, workflow.DraftedDate, workflow.LastWorkedOn, workflow.ReadyForDraft, workflow.ReadyForRevision, workflow.ReadyForPolish, workflow.IsBlocked, workflow.BlockedReason, workflow.Priority }, commandType: CommandType.StoredProcedure); } public async Task> ListNotesAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneNote_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveNoteAsync(SceneNote note) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneNote_Save", new { note.SceneNoteID, note.SceneID, note.SceneNoteTypeID, note.NoteTitle, note.NoteText, note.SortOrder, note.IsPinned, note.IsResolved }, commandType: CommandType.StoredProcedure); } public async Task DeleteNoteAsync(int sceneNoteId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneNote_Delete", new { SceneNoteID = sceneNoteId }, commandType: CommandType.StoredProcedure); } public async Task> ListChecklistItemsAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneChecklist_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveChecklistItemAsync(SceneChecklistItem item) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneChecklist_Save", new { item.SceneChecklistItemID, item.SceneID, item.ItemText, item.IsCompleted, item.SortOrder }, commandType: CommandType.StoredProcedure); } public async Task DeleteChecklistItemAsync(int sceneChecklistItemId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneChecklist_Delete", new { SceneChecklistItemID = sceneChecklistItemId }, commandType: CommandType.StoredProcedure); } public async Task> ListAttachmentsAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneAttachment_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveAttachmentAsync(SceneAttachment attachment) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneAttachment_Save", new { attachment.SceneAttachmentID, attachment.SceneID, attachment.SceneAttachmentTypeID, attachment.Title, attachment.FilePath, attachment.ExternalUrl, attachment.Notes }, commandType: CommandType.StoredProcedure); } public async Task DeleteAttachmentAsync(int sceneAttachmentId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneAttachment_Delete", new { SceneAttachmentID = sceneAttachmentId }, commandType: CommandType.StoredProcedure); } public async Task GetChapterGoalAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync("dbo.ChapterGoal_Get", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); } public async Task SaveChapterGoalAsync(ChapterGoal goal) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.ChapterGoal_Save", new { goal.ChapterID, goal.GoalSummary, goal.EmotionalGoal, goal.ReaderTakeaway, goal.MustInclude, goal.RisksOrConcerns }, commandType: CommandType.StoredProcedure); } public async Task> GetChapterWorkflowAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ChapterWorkflow_Summary", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetWriterDashboardScenesAsync(int? projectId, string queue) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.WriterDashboard_Scenes", new { ProjectID = projectId, Queue = queue }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetWriterDashboardRemindersAsync(int? projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.WriterDashboard_Reminders", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class ExportRepository(ISqlConnectionFactory connectionFactory) : IExportRepository { public async Task GetStoryBibleAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Export_StoryBible", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Story Bible", Characters = (await result.ReadAsync()).ToList(), PlotThreads = (await result.ReadAsync()).ToList(), Assets = (await result.ReadAsync()).ToList(), Relationships = (await result.ReadAsync()).ToList(), Locations = (await result.ReadAsync()).ToList(), Timeline = (await result.ReadAsync()).ToList(), OpenQuestions = (await result.ReadAsync()).ToList(), RevisionItems = (await result.ReadAsync()).ToList() }; } public async Task GetNarrativeTimelineAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Export_NarrativeTimeline", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Narrative Timeline Summary", Timeline = rows.ToList() }; } public async Task GetCharacterReferenceAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Export_CharacterReference", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Character Reference Pack", Character = await result.ReadSingleOrDefaultAsync(), CharacterRelationships = (await result.ReadAsync()).ToList(), CharacterAppearances = (await result.ReadAsync()).ToList(), CharacterKnowledge = (await result.ReadAsync()).ToList(), CharacterCustody = (await result.ReadAsync()).ToList(), Items = (await result.ReadAsync()).ToList() }; } public async Task GetSceneBriefAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Export_SceneBrief", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Scene Brief", SceneHeader = await result.ReadSingleOrDefaultAsync(), Items = (await result.ReadAsync()).ToList() }; } public async Task GetChapterBriefAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Export_ChapterBrief", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Chapter Brief", ChapterHeader = await result.ReadSingleOrDefaultAsync(), Timeline = (await result.ReadAsync()).ToList(), Items = (await result.ReadAsync()).ToList() }; } public async Task GetWriterSessionBriefAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Export_WriterSessionBrief", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Writing Session Brief", SceneHeader = await result.ReadSingleOrDefaultAsync(), Items = (await result.ReadAsync()).ToList() }; } public async Task GetResearchPackAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Export_ResearchPack", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Research Pack", ResearchItems = rows.ToList() }; } public async Task GetSearchResultsAsync(int projectId, string query) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Export_SearchResults", new { ProjectID = projectId, Query = query }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = $"Search Results: {query}", SearchResults = rows.ToList() }; } } public sealed class DynamicsRepository(ISqlConnectionFactory connectionFactory) : IDynamicsRepository { public async Task<(Character? Character, IReadOnlyList ArcPoints, IReadOnlyList RelationshipEvents, IReadOnlyList KnowledgeEvents, IReadOnlyList Issues)> GetCharacterArcAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Dynamics_CharacterArc", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return ( await result.ReadSingleOrDefaultAsync(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task<(CharacterRelationship? Relationship, IReadOnlyList Events, IReadOnlyList Issues)> GetRelationshipEvolutionAsync(int characterRelationshipId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Dynamics_RelationshipEvolution", new { CharacterRelationshipID = characterRelationshipId }, commandType: CommandType.StoredProcedure); return ( await result.ReadSingleOrDefaultAsync(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task<(Character? Character, IReadOnlyList KnowledgeEvents, IReadOnlyList Issues)> GetKnowledgeTimelineAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Dynamics_KnowledgeTimeline", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return ( await result.ReadSingleOrDefaultAsync(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task<(IReadOnlyList Pairings, IReadOnlyList Issues)> GetStoryDynamicsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Dynamics_StoryDashboard", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return ((await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task RunContinuityChecksAsync(int projectId, int? bookId, int? sceneId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Dynamics_RunContinuityChecks", new { ProjectID = projectId, BookID = bookId, SceneID = sceneId }, commandType: CommandType.StoredProcedure); } } public sealed class ScenarioRepository(ISqlConnectionFactory connectionFactory) : IScenarioRepository { public async Task> ListAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Scenario_List", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Scenario_Get", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); } public async Task CreateAsync(int projectId, int? bookId, string scenarioName, string? description) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Scenario_Create", new { ProjectID = projectId, BookID = bookId, ScenarioName = scenarioName, Description = description }, commandType: CommandType.StoredProcedure); } public async Task<(IReadOnlyList Books, IReadOnlyList Chapters, IReadOnlyList Scenes, IReadOnlyList Warnings)> GetTimelineAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Scenario_Timeline", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); return ( (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task MoveSceneAsync(int scenarioId, int sceneId, int? anchorSceneId, int? targetChapterId, string position) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Scenario_MoveScene", new { ScenarioID = scenarioId, SceneID = sceneId, AnchorSceneID = anchorSceneId, TargetChapterID = targetChapterId, Position = position }, commandType: CommandType.StoredProcedure); } public async Task ValidateAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync("dbo.Scenario_Validate", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); } public async Task<(IReadOnlyList MovedScenes, IReadOnlyList Warnings)> CompareAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Scenario_Compare", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); return ((await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task ApplyAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scenario_ApplyToMain", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scenario_Archive", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); } } public sealed class ArchiveRepository(ISqlConnectionFactory connectionFactory) : IArchiveRepository { public async Task> ListAsync(int? projectId, string? entityType) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.Archive_List", new { ProjectID = projectId, EntityType = entityType }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListProjectFiltersAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.Archive_ProjectFilterList", commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task ArchiveAsync(string entityType, int entityId, string? reason) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Archive_Entity", new { EntityType = entityType, EntityID = entityId, ArchivedReason = reason }, commandType: CommandType.StoredProcedure); } public async Task RestoreAsync(string entityType, int entityId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Archive_Restore", new { EntityType = entityType, EntityID = entityId }, commandType: CommandType.StoredProcedure); } public async Task DeleteForeverAsync(string entityType, int entityId, string confirmationText) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Archive_DeleteForever", new { EntityType = entityType, EntityID = entityId, ConfirmationText = confirmationText }, commandType: CommandType.StoredProcedure); } } public sealed class SceneDependencyRepository(ISqlConnectionFactory connectionFactory) : ISceneDependencyRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependencyLookup_All", commandType: CommandType.StoredProcedure); return new SceneDependencyLookupData { DependencyTypes = rows.ToList() }; } public async Task> ListBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependency_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListByProjectAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependency_ListByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetCountsAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependency_Counts", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveAsync(SceneDependency dependency) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneDependency_Save", new { dependency.SceneDependencyID, dependency.ProjectID, dependency.SourceSceneID, dependency.TargetSceneID, dependency.SceneDependencyTypeID, dependency.Description, dependency.IsHardDependency }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int sceneDependencyId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneDependency_Archive", new { SceneDependencyID = sceneDependencyId }, commandType: CommandType.StoredProcedure); } } public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) : IProjectRepository { public async Task> ListAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Project_List", commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Project_Get", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(Project project) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Project_Save", new { project.ProjectID, project.ProjectName, project.Description }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Project_Archive", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); } } public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IBookRepository { public async Task> ListByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Book_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Book_Get", new { BookID = bookId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(Book book) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Book_Save", new { book.BookID, book.ProjectID, book.BookTitle, book.BookNumber, book.Description }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Book_Archive", new { BookID = bookId }, commandType: CommandType.StoredProcedure); } } public sealed class ChapterRepository(ISqlConnectionFactory connectionFactory) : IChapterRepository { public async Task> ListByBookAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Chapter_ListByBook", new { BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Chapter_Get", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(Chapter chapter) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Chapter_Save", new { chapter.ChapterID, chapter.BookID, chapter.ChapterNumber, chapter.ChapterTitle, chapter.Summary, chapter.RevisionStatusID }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Chapter_Archive", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); } } public sealed class SceneRepository(ISqlConnectionFactory connectionFactory) : ISceneRepository { public async Task> ListByChapterAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Scene_ListByChapter", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Scene_Get", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); var scene = await result.ReadSingleOrDefaultAsync(); if (scene is null) { return null; } scene.SelectedPurposeTypeIds = (await result.ReadAsync()).ToList(); scene.Metrics = (await result.ReadAsync()).ToList(); return scene; } public async Task SaveAsync(Scene scene) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Scene_Save", new { scene.SceneID, scene.ChapterID, scene.SceneNumber, scene.SceneTitle, scene.Summary, scene.TimeModeID, scene.StartDateTime, scene.EndDateTime, scene.DurationAmount, scene.DurationUnitID, scene.RelativeTimeText, scene.TimeConfidenceID, scene.ScenePurposeNotes, scene.SceneOutcomeNotes, scene.RevisionStatusID, scene.PrimaryLocationID }, commandType: CommandType.StoredProcedure); } public async Task SavePurposesAsync(int sceneId, IEnumerable purposeIds) { using var connection = connectionFactory.CreateConnection(); var cleanPurposeIds = purposeIds .Where(id => id > 0) .Distinct() .ToList(); await connection.ExecuteAsync( "dbo.Scene_SavePurposes", new { SceneID = sceneId, PurposeIds = string.Join(',', cleanPurposeIds) }, commandType: CommandType.StoredProcedure); } public async Task SaveMetricValuesAsync(int sceneId, IEnumerable metricValues) { using var connection = connectionFactory.CreateConnection(); foreach (var metric in metricValues) { await connection.ExecuteAsync( "dbo.SceneMetric_SaveValue", new { SceneID = sceneId, metric.MetricTypeID, metric.Value, metric.Notes }, commandType: CommandType.StoredProcedure); } } public async Task ArchiveAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scene_Archive", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); } public async Task MoveAsync(int sceneId, string direction) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scene_Move", new { SceneID = sceneId, Direction = direction }, commandType: CommandType.StoredProcedure); } public async Task> ListChapterOptionsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ChapterOption_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetMovePreviewAsync(int sceneId, int targetChapterId, string position) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.SceneMovePreview_Get", new { SceneID = sceneId, TargetChapterID = targetChapterId, Position = position }, commandType: CommandType.StoredProcedure); } public async Task MoveToChapterAsync(int sceneId, int targetChapterId, string position, bool renumber) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Scene_MoveToChapter", new { SceneID = sceneId, TargetChapterID = targetChapterId, Position = position, Renumber = renumber }, commandType: CommandType.StoredProcedure); } public async Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Scene_MoveRelative", new { SceneID = sceneId, AnchorSceneID = anchorSceneId, Position = position }, commandType: CommandType.StoredProcedure); } public async Task RenumberChapterAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scene_RenumberChapter", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); } } public sealed class LookupRepository(ISqlConnectionFactory connectionFactory) : ILookupRepository { public async Task GetAllAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Lookup_All", commandType: CommandType.StoredProcedure); return new LookupData { RevisionStatuses = (await result.ReadAsync()).ToList(), TimeModes = (await result.ReadAsync()).ToList(), DurationUnits = (await result.ReadAsync()).ToList(), TimeConfidences = (await result.ReadAsync()).ToList(), ScenePurposeTypes = (await result.ReadAsync()).ToList() }; } } public sealed class TimelineRepository(ISqlConnectionFactory connectionFactory) : ITimelineRepository { public async Task GetByProjectAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync( "dbo.Timeline_GetByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); var project = await result.ReadSingleOrDefaultAsync(); var books = (await result.ReadAsync()).ToList(); var chapters = (await result.ReadAsync()).ToList(); var scenes = (await result.ReadAsync()).ToList(); var purposeLabels = (await result.ReadAsync()).ToList(); var metricTypes = (await result.ReadAsync()).ToList(); var metricPoints = (await result.ReadAsync()).ToList(); var plotLines = (await result.ReadAsync()).ToList(); var threadEvents = (await result.ReadAsync()).ToList(); return new TimelineData { Project = project, Books = books, Chapters = chapters, Scenes = scenes, PurposeLabels = purposeLabels, MetricTypes = metricTypes, MetricPoints = metricPoints, PlotLines = plotLines, ThreadEvents = threadEvents }; } } public sealed class TimelinePresetRepository(ISqlConnectionFactory connectionFactory) : ITimelinePresetRepository { public async Task> ListAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.TimelineViewPreset_List", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int timelineViewPresetId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.TimelineViewPreset_Get", new { TimelineViewPresetID = timelineViewPresetId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(TimelineViewPreset preset) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.TimelineViewPreset_Save", new { preset.TimelineViewPresetID, preset.ProjectID, preset.BookID, preset.PresetName, preset.FilterJson, preset.VisibilityJson, preset.FocusType, preset.FocusID }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int timelineViewPresetId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.TimelineViewPreset_Archive", new { TimelineViewPresetID = timelineViewPresetId }, commandType: CommandType.StoredProcedure); } } public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IPlotRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.PlotLookup_All", commandType: CommandType.StoredProcedure); return new PlotLookupData { PlotLineTypes = (await result.ReadAsync()).ToList(), ThreadTypes = (await result.ReadAsync()).ToList(), ThreadStatuses = (await result.ReadAsync()).ToList(), ThreadEventTypes = (await result.ReadAsync()).ToList() }; } public async Task> ListPlotLinesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.PlotLine_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetPlotLineAsync(int plotLineId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.PlotLine_Get", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure); } public async Task SavePlotLineAsync(PlotLineItem plotLine) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.PlotLine_Save", new { plotLine.PlotLineID, plotLine.ProjectID, plotLine.BookID, plotLine.PlotLineName, plotLine.PlotLineTypeID, plotLine.Description, plotLine.ParentPlotLineID, plotLine.SortOrder, plotLine.Colour, plotLine.IsMainPlot, plotLine.IsVisibleOnTimeline }, commandType: CommandType.StoredProcedure); } public async Task ArchivePlotLineAsync(int plotLineId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.PlotLine_Archive", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure); } public async Task> ListPlotThreadsByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.PlotThread_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListPlotThreadsByPlotLineAsync(int plotLineId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.PlotThread_ListByPlotLine", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetPlotThreadAsync(int plotThreadId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.PlotThread_Get", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure); } public async Task SavePlotThreadAsync(PlotThread plotThread) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.PlotThread_Save", new { plotThread.PlotThreadID, plotThread.PlotLineID, plotThread.ThreadTitle, plotThread.ThreadTypeID, plotThread.ThreadStatusID, plotThread.Importance, plotThread.Summary, plotThread.IntroducedSceneID, plotThread.PlannedResolutionSceneID, plotThread.ActualResolutionSceneID }, commandType: CommandType.StoredProcedure); } public async Task ArchivePlotThreadAsync(int plotThreadId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.PlotThread_Archive", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure); } public async Task> ListThreadEventsBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ThreadEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListThreadEventsByPlotThreadAsync(int plotThreadId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ThreadEvent_ListByPlotThread", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveThreadEventAsync(ThreadEvent threadEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ThreadEvent_Save", new { threadEvent.ThreadEventID, threadEvent.PlotThreadID, threadEvent.SceneID, threadEvent.EventTypeID, threadEvent.EventTitle, threadEvent.EventDescription }, commandType: CommandType.StoredProcedure); } public async Task DeleteThreadEventAsync(int threadEventId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ThreadEvent_Delete", new { ThreadEventID = threadEventId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneOptionsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneOption_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : IAssetRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.AssetLookup_All", commandType: CommandType.StoredProcedure); return new AssetLookupData { AssetKinds = (await result.ReadAsync()).ToList(), AssetStates = (await result.ReadAsync()).ToList(), AssetEventTypes = (await result.ReadAsync()).ToList(), AssetDependencyTypes = (await result.ReadAsync()).ToList(), AssetCustodyEventTypes = (await result.ReadAsync()).ToList(), CustodyRoles = (await result.ReadAsync()).ToList() }; } public async Task> ListAssetsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.StoryAsset_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.StoryAsset_Get", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); } public async Task SaveAssetAsync(StoryAsset asset) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.StoryAsset_Save", new { asset.StoryAssetID, asset.ProjectID, asset.AssetName, asset.AssetKindID, asset.Description, asset.Importance, asset.CurrentStateID, asset.CurrentLocationID, asset.IsResolved }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.StoryAsset_Archive", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); } public async Task> ListEventsBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListEventsByAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveEventAsync(AssetEvent assetEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.AssetEvent_Save", new { assetEvent.AssetEventID, assetEvent.StoryAssetID, assetEvent.SceneID, assetEvent.AssetEventTypeID, assetEvent.FromStateID, assetEvent.ToStateID, assetEvent.EventTitle, assetEvent.EventDescription }, commandType: CommandType.StoredProcedure); } public async Task DeleteEventAsync(int assetEventId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.AssetEvent_Delete", new { AssetEventID = assetEventId }, commandType: CommandType.StoredProcedure); } public async Task> ListDependenciesByAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetDependency_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveDependencyAsync(AssetDependency dependency) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.AssetDependency_Save", new { dependency.AssetDependencyID, dependency.SourceAssetID, dependency.TargetAssetID, dependency.AssetDependencyTypeID, dependency.SourceRequiredStateID, dependency.TargetBlockedStateID, dependency.Description }, commandType: CommandType.StoredProcedure); } public async Task DeleteDependencyAsync(int assetDependencyId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.AssetDependency_Delete", new { AssetDependencyID = assetDependencyId }, commandType: CommandType.StoredProcedure); } public async Task> ListCustodyBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetCustodyEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListCustodyByAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetCustodyEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable custodianCharacterIds, int custodyRoleId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.AssetCustodyEvent_Save", new { custodyEvent.AssetCustodyEventID, custodyEvent.StoryAssetID, custodyEvent.SceneID, custodyEvent.AssetCustodyEventTypeID, custodyEvent.Description, CustodianNames = custodianNames, CustodianCharacterIds = string.Join(',', custodianCharacterIds.Where(id => id > 0).Distinct()), CustodyRoleID = custodyRoleId }, commandType: CommandType.StoredProcedure); } public async Task DeleteCustodyEventAsync(int assetCustodyEventId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.AssetCustodyEvent_Delete", new { AssetCustodyEventID = assetCustodyEventId }, commandType: CommandType.StoredProcedure); } public async Task<(IReadOnlyList Assets, IReadOnlyList Events)> GetTimelineAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.AssetTimeline_GetByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); var assets = (await result.ReadAsync()).ToList(); var events = (await result.ReadAsync()).ToList(); return (assets, events); } public async Task> CheckDependencyIssuesAsync(int storyAssetId, int sceneId, int? toStateId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.AssetDependency_CheckForEvent", new { StoryAssetID = storyAssetId, SceneID = sceneId, ToStateID = toStateId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory) : ICharacterRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.CharacterLookup_All", commandType: CommandType.StoredProcedure); return new CharacterLookupData { RoleTypes = (await result.ReadAsync()).ToList(), PresenceTypes = (await result.ReadAsync()).ToList(), AttributeTypes = (await result.ReadAsync()).ToList(), KnowledgeStates = (await result.ReadAsync()).ToList(), RelationshipTypes = (await result.ReadAsync()).ToList(), RelationshipStates = (await result.ReadAsync()).ToList() }; } public async Task> ListCharactersAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Character_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Character_Get", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); } public async Task SaveCharacterAsync(Character character) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Character_Save", new { character.CharacterID, character.ProjectID, character.CharacterName, character.ShortName, character.Sex, character.BirthDate, character.AgeAtSeriesStart, character.AgeReferenceSceneID, character.Height, character.EyeColour, character.DefaultDescription }, commandType: CommandType.StoredProcedure); } public async Task ArchiveCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Character_Archive", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneCharactersAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneCharacter_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveSceneCharacterAsync(SceneCharacter sceneCharacter) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneCharacter_Save", new { sceneCharacter.SceneCharacterID, sceneCharacter.SceneID, sceneCharacter.CharacterID, sceneCharacter.RoleInSceneTypeID, sceneCharacter.PresenceTypeID, sceneCharacter.LocationID, sceneCharacter.EntryLocationID, sceneCharacter.ExitLocationID, sceneCharacter.AppearanceNotes, sceneCharacter.OutfitDescription, sceneCharacter.PhysicalCondition, sceneCharacter.EmotionalState, sceneCharacter.KnowledgeNotes }, commandType: CommandType.StoredProcedure); } public async Task DeleteSceneCharacterAsync(int sceneCharacterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneCharacter_Delete", new { SceneCharacterID = sceneCharacterId }, commandType: CommandType.StoredProcedure); } public async Task> ListAttributeEventsByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.CharacterAttributeEvent_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveAttributeEventAsync(CharacterAttributeEvent attributeEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.CharacterAttributeEvent_Save", new { attributeEvent.CharacterAttributeEventID, attributeEvent.CharacterID, attributeEvent.CharacterAttributeTypeID, attributeEvent.SceneID, attributeEvent.AttributeValue, attributeEvent.Description }, commandType: CommandType.StoredProcedure); } public async Task> ListKnowledgeBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.CharacterKnowledge_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListKnowledgeByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.CharacterKnowledge_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveKnowledgeAsync(CharacterKnowledgeItem knowledge) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.CharacterKnowledge_Save", new { knowledge.CharacterKnowledgeID, knowledge.CharacterID, knowledge.StoryAssetID, knowledge.PlotThreadID, knowledge.SceneID, knowledge.KnowledgeStateID, knowledge.SourceEventID, knowledge.Description }, commandType: CommandType.StoredProcedure); } public async Task DeleteKnowledgeAsync(int characterKnowledgeId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.CharacterKnowledge_Delete", new { CharacterKnowledgeID = characterKnowledgeId }, commandType: CommandType.StoredProcedure); } public async Task> ListRelationshipsByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Relationship_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListRelationshipsByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Relationship_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveRelationshipAsync(CharacterRelationship relationship) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Relationship_Save", new { relationship.CharacterRelationshipID, relationship.ProjectID, relationship.CharacterAID, relationship.CharacterBID, relationship.RelationshipTypeID, relationship.IsPermanent, relationship.StartSceneID, relationship.EndSceneID, relationship.IsKnownToReader, relationship.Notes }, commandType: CommandType.StoredProcedure); } public async Task ArchiveRelationshipAsync(int characterRelationshipId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Relationship_Archive", new { CharacterRelationshipID = characterRelationshipId }, commandType: CommandType.StoredProcedure); } public async Task> ListRelationshipEventsBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.RelationshipEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListRelationshipEventsByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.RelationshipEvent_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveRelationshipEventAsync(RelationshipEvent relationshipEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.RelationshipEvent_Save", new { relationshipEvent.RelationshipEventID, relationshipEvent.CharacterRelationshipID, relationshipEvent.SceneID, relationshipEvent.RelationshipStateID, relationshipEvent.Intensity, relationshipEvent.IsKnownToOtherCharacter, relationshipEvent.Description }, commandType: CommandType.StoredProcedure); } public async Task DeleteRelationshipEventAsync(int relationshipEventId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.RelationshipEvent_Delete", new { RelationshipEventID = relationshipEventId }, commandType: CommandType.StoredProcedure); } public async Task<(IReadOnlyList Characters, IReadOnlyList Appearances)> GetTimelineAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.CharacterTimeline_GetByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); var characters = (await result.ReadAsync()).ToList(); var appearances = (await result.ReadAsync()).ToList(); return (characters, appearances); } }