PlotDirector/PlotLine/Data/Repositories.cs
2026-06-15 15:23:35 +01:00

3325 lines
153 KiB
C#

using System.Data;
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public interface IProjectRepository
{
Task<IReadOnlyList<Project>> ListAsync();
Task<IReadOnlyList<Project>> ListForUserAsync(int userId);
Task<IReadOnlyList<Project>> ListActiveForUserAsync(int userId);
Task<IReadOnlyList<Project>> ListArchivedForUserAsync(int userId);
Task<IReadOnlyList<Project>> ListOwnedForAccountDeletionAsync(int userId);
Task<Project?> GetAsync(int projectId);
Task<Project?> GetForUserAsync(int projectId, int userId);
Task<Project?> GetArchivedForOwnerAsync(int projectId, int userId);
Task<ProjectDashboardMetrics> GetDashboardMetricsAsync(int projectId);
Task<IReadOnlyList<GenreMetricPreset>> ListGenreMetricPresetsAsync();
Task<int> SaveAsync(Project project);
Task<int> SaveAsync(Project project, int userId, string? genreMetricPresetKey);
Task<int> SaveAsync(Project project, string? genreMetricPresetKey);
Task ArchiveAsync(int projectId);
Task ArchiveForUserAsync(int projectId, int userId);
Task<bool> ArchiveForOwnerAsync(int projectId, int userId);
Task<bool> RestoreForOwnerAsync(int projectId, int userId);
Task<IReadOnlyList<string>> ListHardDeleteFilePathsForOwnerAsync(int projectId, int userId);
Task HardDeleteForOwnerAsync(int projectId, int userId);
Task<IReadOnlyList<string>> ListHardDeleteFilePathsForAccountDeletionAsync(int projectId, int userId);
Task HardDeleteForAccountDeletionAsync(int projectId, int userId);
}
public interface IProjectCollaborationRepository
{
Task<int?> GetOwnerUserIdAsync(int projectId);
Task<IReadOnlyList<ProjectCollaborator>> ListCollaboratorsAsync(int projectId);
Task<IReadOnlyList<ProjectInvitation>> ListPendingInvitationsAsync(int projectId);
Task<bool> CollaboratorCountsForOwnerAsync(int ownerUserId, int collaboratorUserId);
Task<bool> AddCollaboratorAsync(int projectId, int userId, int invitedByUserId);
Task<bool> RemoveCollaboratorAsync(int projectId, int userId);
Task<int> CreatePendingInvitationAsync(int projectId, string emailAddress, int invitedByUserId);
}
public interface IProjectActivityRepository
{
Task<IReadOnlyList<ProjectActivity>> ListForProjectAsync(int projectId);
Task<long> RecordAsync(int projectId, int? userId, string activityType, string entityType, int? entityId, string? entityName, string? description = null);
}
public interface IBookRepository
{
Task<IReadOnlyList<Book>> ListByProjectAsync(int projectId);
Task<Book?> GetAsync(int bookId);
Task<Book?> GetCoverPathsAsync(int bookId);
Task<int> SaveAsync(Book book);
Task UpdateCoverAsync(int bookId, string? thumbnailPath, string? standardPath);
Task ArchiveAsync(int bookId);
}
public interface IChapterRepository
{
Task<IReadOnlyList<Chapter>> ListByBookAsync(int bookId);
Task<Chapter?> GetAsync(int chapterId);
Task<int> SaveAsync(Chapter chapter);
Task ArchiveAsync(int chapterId);
}
public interface ISceneRepository
{
Task<IReadOnlyList<Scene>> ListByChapterAsync(int chapterId);
Task<Scene?> GetAsync(int sceneId);
Task<int> SaveAsync(Scene scene);
Task SavePurposesAsync(int sceneId, IEnumerable<int> purposeIds);
Task SaveMetricValuesAsync(int sceneId, IEnumerable<SceneMetricValue> metricValues);
Task SaveMetricValueAsync(int sceneId, int metricTypeId, int value);
Task ArchiveAsync(int sceneId);
Task MoveAsync(int sceneId, string direction);
Task<IReadOnlyList<ChapterOption>> ListChapterOptionsAsync(int projectId);
Task<SceneMovePreview?> 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<LookupData> GetAllAsync();
}
public interface ITimelineRepository
{
Task<TimelineData> GetByProjectAsync(int projectId, int? bookId, bool includeMetrics, IEnumerable<int> metricTypeIds, bool includePlotLines);
}
public interface ITimelinePresetRepository
{
Task<IReadOnlyList<TimelineViewPreset>> ListAsync(int projectId, int? bookId);
Task<TimelineViewPreset?> GetAsync(int timelineViewPresetId);
Task<int> SaveAsync(TimelineViewPreset preset);
Task ArchiveAsync(int timelineViewPresetId);
}
public interface ITimelineSettingsRepository
{
Task<ProjectTimelineSettings?> GetAsync(int projectId);
Task<IReadOnlyList<SceneMetricType>> ListMetricTypesAsync(int projectId);
Task<IReadOnlyList<ProjectTimelineMetricSetting>> ListMetricSettingsAsync(int projectId);
Task SaveAsync(ProjectTimelineSettings settings, IEnumerable<int> metricTypeIds);
}
public interface ISceneMetricTypeRepository
{
Task<IReadOnlyList<SceneMetricType>> ListForManagementAsync(int projectId);
Task<SceneMetricType?> GetAsync(int metricTypeId, int projectId);
Task<int> SaveAsync(SceneMetricType metric);
Task<bool> SetActiveAsync(int metricTypeId, int projectId, bool isActive);
Task<bool> MoveAsync(int metricTypeId, int projectId, string direction);
Task AddMissingDefaultsAsync(int projectId);
Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId);
Task<bool> HasProjectMetricsAsync(int projectId);
}
public interface IPlotRepository
{
Task<PlotLookupData> GetLookupsAsync();
Task<IReadOnlyList<PlotLineItem>> ListPlotLinesAsync(int projectId);
Task<PlotLineItem?> GetPlotLineAsync(int plotLineId);
Task<int> SavePlotLineAsync(PlotLineItem plotLine);
Task ArchivePlotLineAsync(int plotLineId);
Task<IReadOnlyList<PlotThread>> ListPlotThreadsByProjectAsync(int projectId);
Task<IReadOnlyList<PlotThread>> ListPlotThreadsByPlotLineAsync(int plotLineId);
Task<PlotThread?> GetPlotThreadAsync(int plotThreadId);
Task<int> SavePlotThreadAsync(PlotThread plotThread);
Task ArchivePlotThreadAsync(int plotThreadId);
Task<IReadOnlyList<ThreadEvent>> ListThreadEventsBySceneAsync(int sceneId);
Task<IReadOnlyList<ThreadEvent>> ListThreadEventsByPlotThreadAsync(int plotThreadId);
Task<int> SaveThreadEventAsync(ThreadEvent threadEvent);
Task SaveThreadEventTargetsAsync(int threadEventId, IEnumerable<int> plotLineIds);
Task DeleteThreadEventAsync(int threadEventId);
Task<IReadOnlyList<SceneOption>> ListSceneOptionsAsync(int projectId);
}
public interface IAssetRepository
{
Task<AssetLookupData> GetLookupsAsync();
Task<IReadOnlyList<StoryAsset>> ListAssetsAsync(int projectId);
Task<StoryAsset?> GetAssetAsync(int storyAssetId);
Task<int> SaveAssetAsync(StoryAsset asset);
Task ArchiveAssetAsync(int storyAssetId);
Task<IReadOnlyList<AssetEvent>> ListEventsBySceneAsync(int sceneId);
Task<IReadOnlyList<AssetEvent>> ListEventsByAssetAsync(int storyAssetId);
Task<int> SaveEventAsync(AssetEvent assetEvent);
Task DeleteEventAsync(int assetEventId);
Task<IReadOnlyList<AssetDependency>> ListDependenciesByAssetAsync(int storyAssetId);
Task<int> SaveDependencyAsync(AssetDependency dependency);
Task DeleteDependencyAsync(int assetDependencyId);
Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyBySceneAsync(int sceneId);
Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyByAssetAsync(int storyAssetId);
Task<IReadOnlyList<AssetCustodyCharacter>> ListCustodyCharactersByProjectAsync(int projectId);
Task<int> SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable<int> custodianCharacterIds, int custodyRoleId);
Task DeleteCustodyEventAsync(int assetCustodyEventId);
Task<(IReadOnlyList<StoryAsset> Assets, IReadOnlyList<AssetEvent> Events)> GetTimelineAsync(int projectId, int? bookId);
Task<IReadOnlyList<AssetDependencyIssue>> CheckDependencyIssuesAsync(int storyAssetId, int sceneId, int? toStateId);
}
public interface ICharacterRepository
{
Task<CharacterLookupData> GetLookupsAsync();
Task<IReadOnlyList<Character>> ListCharactersAsync(int projectId);
Task<Character?> GetCharacterAsync(int characterId);
Task<int> SaveCharacterAsync(Character character);
Task ArchiveCharacterAsync(int characterId);
Task<IReadOnlyList<SceneCharacter>> ListSceneCharactersAsync(int sceneId);
Task<int> SaveSceneCharacterAsync(SceneCharacter sceneCharacter);
Task DeleteSceneCharacterAsync(int sceneCharacterId);
Task<IReadOnlyList<CharacterAttributeEvent>> ListAttributeEventsByCharacterAsync(int characterId);
Task<int> SaveAttributeEventAsync(CharacterAttributeEvent attributeEvent);
Task<IReadOnlyList<CharacterKnowledgeItem>> ListKnowledgeBySceneAsync(int sceneId);
Task<IReadOnlyList<CharacterKnowledgeItem>> ListKnowledgeByCharacterAsync(int characterId);
Task<int> SaveKnowledgeAsync(CharacterKnowledgeItem knowledge);
Task DeleteKnowledgeAsync(int characterKnowledgeId);
Task<IReadOnlyList<CharacterRelationship>> ListRelationshipsByProjectAsync(int projectId);
Task<IReadOnlyList<CharacterRelationship>> ListRelationshipsByCharacterAsync(int characterId);
Task<IReadOnlyList<CharacterRelationship>> ListInitialRelationshipsByCharacterAsync(int characterId);
Task<CharacterRelationship?> GetRelationshipAsync(int characterRelationshipId);
Task<int> SaveRelationshipAsync(CharacterRelationship relationship);
Task ArchiveRelationshipAsync(int characterRelationshipId);
Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsBySceneAsync(int sceneId);
Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsByCharacterAsync(int characterId);
Task<int> SaveRelationshipEventAsync(RelationshipEvent relationshipEvent);
Task DeleteRelationshipEventAsync(int relationshipEventId);
Task<(IReadOnlyList<Character> Characters, IReadOnlyList<SceneCharacter> Appearances)> GetTimelineAsync(int projectId, int? bookId);
Task<RelationshipMapData> GetRelationshipMapAsync(int projectId);
}
public interface ILocationRepository
{
Task<LocationLookupData> GetLookupsAsync();
Task<IReadOnlyList<LocationItem>> ListByProjectAsync(int projectId);
Task<LocationItem?> GetAsync(int locationId);
Task<int> SaveAsync(LocationItem location);
Task ArchiveAsync(int locationId);
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByProjectAsync(int projectId);
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByLocationAsync(int locationId);
Task<int> SaveRelationshipAsync(LocationRelationship relationship);
Task ArchiveRelationshipAsync(int locationRelationshipId);
Task<IReadOnlyList<SceneAssetLocation>> ListSceneAssetLocationsAsync(int sceneId);
Task<int> SaveSceneAssetLocationAsync(SceneAssetLocation sceneAssetLocation, bool updateCurrentLocation);
Task DeleteSceneAssetLocationAsync(int sceneAssetLocationId);
Task<IReadOnlyList<Scene>> ListScenesByLocationAsync(int locationId);
Task<IReadOnlyList<SceneCharacter>> ListCharactersByLocationAsync(int locationId);
Task<IReadOnlyList<SceneAssetLocation>> ListAssetsByLocationAsync(int locationId);
}
public interface IWarningRepository
{
Task<WarningLookupData> GetLookupsAsync();
Task<ContinuityValidationSummary> ValidateProjectAsync(int projectId);
Task<ContinuityValidationSummary> ValidateBookAsync(int bookId);
Task<ContinuityValidationSummary> ValidateSceneAsync(int sceneId);
Task<IReadOnlyList<ContinuityWarning>> ListAsync(int projectId, int? bookId, int? sceneId, int? warningTypeId, int? warningSeverityId, string? entityType, bool includeDismissed, bool includeIntentional);
Task<IReadOnlyList<ContinuityWarning>> ListBySceneAsync(int sceneId);
Task<IReadOnlyList<SceneWarningCount>> 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 IContinuityWarningAcknowledgementRepository
{
Task<IReadOnlyList<ContinuityWarningAcknowledgement>> ListByProjectAsync(int projectId);
Task<ContinuityWarningAcknowledgement?> GetAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId);
Task SaveAsync(ContinuityWarningAcknowledgement acknowledgement);
Task DeleteAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId);
}
public interface ISceneDependencyRepository
{
Task<SceneDependencyLookupData> GetLookupsAsync();
Task<IReadOnlyList<SceneDependency>> ListBySceneAsync(int sceneId);
Task<IReadOnlyList<SceneDependency>> ListByProjectAsync(int projectId, int? bookId);
Task<IReadOnlyList<SceneDependencyCount>> GetCountsAsync(int projectId, int? bookId);
Task<int> SaveAsync(SceneDependency dependency);
Task ArchiveAsync(int sceneDependencyId);
}
public interface IAnalyticsRepository
{
Task<AnalyticsData> GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber);
}
public interface IStoryBibleRepository
{
Task<IReadOnlyList<StoryBibleCharacterSummary>> GetCharacterSummariesAsync(int projectId);
Task<IReadOnlyList<StoryBibleThreadSummary>> GetThreadSummariesAsync(int projectId);
Task<IReadOnlyList<StoryBibleAssetSummary>> GetAssetSummariesAsync(int projectId);
Task<IReadOnlyList<StoryBibleRelationshipSummary>> GetRelationshipSummariesAsync(int projectId);
Task<IReadOnlyList<StoryBibleLocationSummary>> GetLocationSummariesAsync(int projectId);
Task<IReadOnlyList<StoryBibleTimelineSummary>> GetTimelineSummaryAsync(int projectId);
Task<IReadOnlyList<StoryBibleAttentionItem>> GetOpenQuestionsAsync(int projectId);
Task<IReadOnlyList<StoryBibleRevisionItem>> GetRevisionProgressAsync(int projectId);
Task<IReadOnlyList<StoryBibleSearchResult>> SearchAsync(int projectId, string? query);
}
public interface IWriterWorkspaceRepository
{
Task<IReadOnlyList<SceneNoteType>> ListNoteTypesAsync();
Task<IReadOnlyList<SceneAttachmentType>> ListAttachmentTypesAsync();
Task<SceneWorkflow> GetWorkflowAsync(int sceneId);
Task SaveWorkflowAsync(SceneWorkflow workflow);
Task<IReadOnlyList<SceneNote>> ListNotesAsync(int sceneId);
Task<int> SaveNoteAsync(SceneNote note);
Task DeleteNoteAsync(int sceneNoteId);
Task<IReadOnlyList<SceneChecklistItem>> ListChecklistItemsAsync(int sceneId);
Task<int> SaveChecklistItemAsync(SceneChecklistItem item);
Task DeleteChecklistItemAsync(int sceneChecklistItemId);
Task<IReadOnlyList<SceneAttachment>> ListAttachmentsAsync(int sceneId);
Task<int> SaveAttachmentAsync(SceneAttachment attachment);
Task DeleteAttachmentAsync(int sceneAttachmentId);
Task<ChapterGoal> GetChapterGoalAsync(int chapterId);
Task SaveChapterGoalAsync(ChapterGoal goal);
Task<IReadOnlyList<ChapterWorkflowScene>> GetChapterWorkflowAsync(int chapterId);
Task<IReadOnlyList<WriterDashboardScene>> GetWriterDashboardScenesAsync(int? projectId, string queue);
Task<IReadOnlyList<StoryBibleAttentionItem>> GetWriterDashboardRemindersAsync(int? projectId);
}
public interface IWritingScheduleRepository
{
Task<IReadOnlyList<WritingPlan>> GetWritingPlansByUserAsync(int userId);
Task<WritingPlan?> GetWritingPlanByIDAsync(int writingPlanId);
Task<int> CreateWritingPlanAsync(WritingPlan plan);
Task UpdateWritingPlanAsync(WritingPlan plan);
Task DeleteWritingPlanAsync(int writingPlanId);
Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive);
Task<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByPlanAsync(int writingPlanId);
Task<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate);
Task<int> CreateScheduleItemAsync(WritingScheduleItem item);
Task UpdateScheduleItemAsync(WritingScheduleItem item);
Task DeleteScheduleItemAsync(int writingScheduleItemId);
Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc);
Task<IReadOnlyList<WritingPlanManagementItem>> GetWritingPlansForManagementAsync(int userId);
Task<IReadOnlyList<WritingScheduleScene>> GetSchedulingScenesAsync(int projectId, int? bookId, bool includeBlockedScenes);
Task ReplaceScheduleItemsAsync(int writingPlanId, IReadOnlyList<WritingScheduleItem> items);
Task<IReadOnlyList<WritingPlanSummary>> GetWritingPlanSummariesByUserAsync(int userId, int? projectId = null);
Task<IReadOnlyList<WritingSchedulePreviewItem>> GetSchedulePreviewItemsAsync(int writingPlanId);
Task DeleteScheduleItemsByPlanAsync(int writingPlanId);
Task<IReadOnlyList<WritingScheduleDashboardItem>> GetActiveScheduleDashboardItemsAsync(int userId, int? projectId = null);
Task<int> UpdatePlannedScheduleItemStatusForUserAsync(int writingScheduleItemId, int userId, string scheduleStatus, DateTime? completedDateUtc);
Task<IReadOnlyList<WritingPlanSummary>> GetPlansRequiringRebalanceAsync(int userId, DateTime today, int? projectId = null);
Task UpdatePlannedScheduleItemDatesAsync(int writingPlanId, IReadOnlyList<WritingScheduleItemDateChange> changes);
}
public interface IExportRepository
{
Task<ExportPacket> GetStoryBibleAsync(int projectId);
Task<ExportPacket> GetNarrativeTimelineAsync(int projectId);
Task<ExportPacket> GetCharacterReferenceAsync(int characterId);
Task<ExportPacket> GetSceneBriefAsync(int sceneId);
Task<ExportPacket> GetChapterBriefAsync(int chapterId);
Task<ExportPacket> GetWriterSessionBriefAsync(int sceneId);
Task<ExportPacket> GetResearchPackAsync(int projectId);
Task<ExportPacket> GetSearchResultsAsync(int projectId, string query);
}
public interface IDynamicsRepository
{
Task<(Character? Character, IReadOnlyList<CharacterArcPoint> ArcPoints, IReadOnlyList<RelationshipEvent> RelationshipEvents, IReadOnlyList<CharacterKnowledgeItem> KnowledgeEvents, IReadOnlyList<DynamicsIssue> Issues)> GetCharacterArcAsync(int characterId);
Task<(CharacterRelationship? Relationship, IReadOnlyList<RelationshipProgressionPoint> Events, IReadOnlyList<DynamicsIssue> Issues)> GetRelationshipEvolutionAsync(int characterRelationshipId);
Task<(Character? Character, IReadOnlyList<CharacterKnowledgeItem> KnowledgeEvents, IReadOnlyList<DynamicsIssue> Issues)> GetKnowledgeTimelineAsync(int characterId);
Task<(IReadOnlyList<CharacterPairingSummary> Pairings, IReadOnlyList<DynamicsIssue> Issues)> GetStoryDynamicsAsync(int projectId);
Task RunContinuityChecksAsync(int projectId, int? bookId, int? sceneId);
}
public interface IScenarioRepository
{
Task<IReadOnlyList<Scenario>> ListAsync(int projectId);
Task<Scenario?> GetAsync(int scenarioId);
Task<int> CreateAsync(int projectId, int? bookId, string scenarioName, string? description);
Task<(IReadOnlyList<Book> Books, IReadOnlyList<Chapter> Chapters, IReadOnlyList<ScenarioScene> Scenes, IReadOnlyList<ScenarioWarning> Warnings)> GetTimelineAsync(int scenarioId);
Task MoveSceneAsync(int scenarioId, int sceneId, int? anchorSceneId, int? targetChapterId, string position);
Task<int> ValidateAsync(int scenarioId);
Task<(IReadOnlyList<ScenarioComparisonRow> MovedScenes, IReadOnlyList<ScenarioWarning> Warnings)> CompareAsync(int scenarioId);
Task ApplyAsync(int scenarioId);
Task ArchiveAsync(int scenarioId);
}
public interface IArchiveRepository
{
Task<IReadOnlyList<ArchivedItem>> ListAsync(int? projectId, string? entityType);
Task<IReadOnlyList<ArchivedItem>> ListForUserAsync(int? projectId, string? entityType, int userId);
Task<IReadOnlyList<Project>> ListProjectFiltersAsync();
Task<IReadOnlyList<Project>> ListProjectFiltersForUserAsync(int userId);
Task ArchiveAsync(string entityType, int entityId, string? reason);
Task RestoreAsync(string entityType, int entityId);
Task<ArchiveDeleteResult> DeleteForeverAsync(string entityType, int entityId, string confirmationText);
}
public interface IAcceptanceRepository
{
Task<IReadOnlyList<Phase1AcceptanceChecklistItem>> GetPhase1ChecklistAsync(int? projectId);
}
public sealed class TimelineData
{
public Project? Project { get; init; }
public IReadOnlyList<Book> Books { get; init; } = [];
public IReadOnlyList<Chapter> Chapters { get; init; } = [];
public IReadOnlyList<Scene> Scenes { get; init; } = [];
public IReadOnlyList<ScenePurposeLabel> PurposeLabels { get; init; } = [];
public IReadOnlyList<SceneMetricType> MetricTypes { get; init; } = [];
public IReadOnlyList<TimelineMetricPoint> MetricPoints { get; init; } = [];
public IReadOnlyList<PlotLineItem> PlotLines { get; init; } = [];
public IReadOnlyList<ThreadEvent> ThreadEvents { get; init; } = [];
}
public sealed class LocationRepository(ISqlConnectionFactory connectionFactory) : ILocationRepository
{
public async Task<LocationLookupData> 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<LocationType>()).ToList(),
RelationshipTypes = (await result.ReadAsync<LocationRelationshipType>()).ToList()
};
}
public async Task<IReadOnlyList<LocationItem>> ListByProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<LocationItem>("dbo.Location_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<LocationItem?> GetAsync(int locationId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<LocationItem>("dbo.Location_Get", new { LocationID = locationId }, commandType: CommandType.StoredProcedure);
}
public async Task<int> SaveAsync(LocationItem location)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.Location_Save",
new
{
location.LocationID,
location.ProjectID,
location.ParentLocationID,
location.LocationName,
location.LocationTypeID,
location.Description,
location.ShowInQuickAddBar
},
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<IReadOnlyList<LocationRelationship>> ListRelationshipsByProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<LocationRelationship>("dbo.LocationRelationship_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByLocationAsync(int locationId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<LocationRelationship>("dbo.LocationRelationship_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveRelationshipAsync(LocationRelationship relationship)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<SceneAssetLocation>> ListSceneAssetLocationsAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneAssetLocation>("dbo.SceneAssetLocation_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveSceneAssetLocationAsync(SceneAssetLocation sceneAssetLocation, bool updateCurrentLocation)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<Scene>> ListScenesByLocationAsync(int locationId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Scene>("dbo.LocationScene_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<SceneCharacter>> ListCharactersByLocationAsync(int locationId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneCharacter>("dbo.LocationCharacter_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<SceneAssetLocation>> ListAssetsByLocationAsync(int locationId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneAssetLocation>("dbo.SceneAssetLocation_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
}
public sealed class WarningRepository(ISqlConnectionFactory connectionFactory) : IWarningRepository
{
public async Task<WarningLookupData> 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<WarningType>()).ToList(),
Severities = (await result.ReadAsync<WarningSeverity>()).ToList()
};
}
public async Task<ContinuityValidationSummary> ValidateProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<ContinuityValidationSummary>(
"dbo.ContinuityValidation_ValidateProject",
new { ProjectID = projectId, TriggeredBy = "User" },
commandType: CommandType.StoredProcedure);
}
public async Task<ContinuityValidationSummary> ValidateBookAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<ContinuityValidationSummary>(
"dbo.ContinuityValidation_ValidateBook",
new { BookID = bookId, TriggeredBy = "User" },
commandType: CommandType.StoredProcedure);
}
public async Task<ContinuityValidationSummary> ValidateSceneAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<ContinuityValidationSummary>(
"dbo.ContinuityValidation_ValidateScene",
new { SceneID = sceneId, TriggeredBy = "User" },
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<ContinuityWarning>> 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<ContinuityWarning>(
"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<IReadOnlyList<ContinuityWarning>> ListBySceneAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ContinuityWarning>("dbo.ContinuityWarning_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<SceneWarningCount>> GetSceneCountsAsync(int projectId, int? bookId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneWarningCount>("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 ContinuityWarningAcknowledgementRepository(ISqlConnectionFactory connectionFactory) : IContinuityWarningAcknowledgementRepository
{
public async Task<IReadOnlyList<ContinuityWarningAcknowledgement>> ListByProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ContinuityWarningAcknowledgement>(
"dbo.ContinuityWarningAcknowledgement_ListByProject",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<ContinuityWarningAcknowledgement?> GetAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<ContinuityWarningAcknowledgement>(
"dbo.ContinuityWarningAcknowledgement_Get",
new
{
ProjectID = projectId,
WarningType = warningType,
SceneID = sceneId,
CharacterID = characterId,
AssetID = assetId
},
commandType: CommandType.StoredProcedure);
}
public async Task SaveAsync(ContinuityWarningAcknowledgement acknowledgement)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.ContinuityWarningAcknowledgement_Save",
new
{
acknowledgement.ProjectID,
acknowledgement.WarningType,
acknowledgement.SceneID,
acknowledgement.CharacterID,
acknowledgement.AssetID,
acknowledgement.AcknowledgedByUserID,
acknowledgement.Notes
},
commandType: CommandType.StoredProcedure);
}
public async Task DeleteAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.ContinuityWarningAcknowledgement_Delete",
new
{
ProjectID = projectId,
WarningType = warningType,
SceneID = sceneId,
CharacterID = characterId,
AssetID = assetId
},
commandType: CommandType.StoredProcedure);
}
}
public sealed class AnalyticsData
{
public IReadOnlyList<AnalyticsMetricPoint> MetricPoints { get; init; } = [];
public IReadOnlyList<AnalyticsRevisionSummary> RevisionSummary { get; init; } = [];
public IReadOnlyList<AnalyticsRevisionScene> RevisionAttentionScenes { get; init; } = [];
public IReadOnlyList<AnalyticsPurposeSummary> PurposeSummary { get; init; } = [];
public IReadOnlyList<AnalyticsSceneGap> SceneGaps { get; init; } = [];
public IReadOnlyList<AnalyticsThreadHealth> ThreadHealth { get; init; } = [];
public IReadOnlyList<AnalyticsAssetHealth> AssetHealth { get; init; } = [];
public IReadOnlyList<AnalyticsWarningSeverityCount> WarningSeverityCounts { get; init; } = [];
public IReadOnlyList<AnalyticsWarningTypeCount> WarningTypeCounts { get; init; } = [];
public IReadOnlyList<AnalyticsWarningHotspot> WarningHotspots { get; init; } = [];
public IReadOnlyList<AnalyticsPovSummary> PovSummary { get; init; } = [];
public IReadOnlyList<AnalyticsMultiPovChapter> MultiPovChapters { get; init; } = [];
public IReadOnlyList<AnalyticsCharacterAppearance> CharacterAppearances { get; init; } = [];
public IReadOnlyList<AnalyticsBusyScene> BusyScenes { get; init; } = [];
public IReadOnlyList<AnalyticsLocationUsage> LocationUsage { get; init; } = [];
}
public sealed class AnalyticsRepository(ISqlConnectionFactory connectionFactory) : IAnalyticsRepository
{
public async Task<AnalyticsData> 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<AnalyticsMetricPoint>()).ToList(),
RevisionSummary = (await result.ReadAsync<AnalyticsRevisionSummary>()).ToList(),
RevisionAttentionScenes = (await result.ReadAsync<AnalyticsRevisionScene>()).ToList(),
PurposeSummary = (await result.ReadAsync<AnalyticsPurposeSummary>()).ToList(),
SceneGaps = (await result.ReadAsync<AnalyticsSceneGap>()).ToList(),
ThreadHealth = (await result.ReadAsync<AnalyticsThreadHealth>()).ToList(),
AssetHealth = (await result.ReadAsync<AnalyticsAssetHealth>()).ToList(),
WarningSeverityCounts = (await result.ReadAsync<AnalyticsWarningSeverityCount>()).ToList(),
WarningTypeCounts = (await result.ReadAsync<AnalyticsWarningTypeCount>()).ToList(),
WarningHotspots = (await result.ReadAsync<AnalyticsWarningHotspot>()).ToList(),
PovSummary = (await result.ReadAsync<AnalyticsPovSummary>()).ToList(),
MultiPovChapters = (await result.ReadAsync<AnalyticsMultiPovChapter>()).ToList(),
CharacterAppearances = (await result.ReadAsync<AnalyticsCharacterAppearance>()).ToList(),
BusyScenes = (await result.ReadAsync<AnalyticsBusyScene>()).ToList(),
LocationUsage = (await result.ReadAsync<AnalyticsLocationUsage>()).ToList()
};
}
}
public sealed class StoryBibleRepository(ISqlConnectionFactory connectionFactory) : IStoryBibleRepository
{
public async Task<IReadOnlyList<StoryBibleCharacterSummary>> GetCharacterSummariesAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return (await connection.QueryAsync<StoryBibleCharacterSummary>("dbo.StoryBible_CharacterSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
}
public async Task<IReadOnlyList<StoryBibleThreadSummary>> GetThreadSummariesAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return (await connection.QueryAsync<StoryBibleThreadSummary>("dbo.StoryBible_ThreadSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
}
public async Task<IReadOnlyList<StoryBibleAssetSummary>> GetAssetSummariesAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return (await connection.QueryAsync<StoryBibleAssetSummary>("dbo.StoryBible_AssetSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
}
public async Task<IReadOnlyList<StoryBibleRelationshipSummary>> GetRelationshipSummariesAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return (await connection.QueryAsync<StoryBibleRelationshipSummary>("dbo.StoryBible_RelationshipSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
}
public async Task<IReadOnlyList<StoryBibleLocationSummary>> GetLocationSummariesAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return (await connection.QueryAsync<StoryBibleLocationSummary>("dbo.StoryBible_LocationSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
}
public async Task<IReadOnlyList<StoryBibleTimelineSummary>> GetTimelineSummaryAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return (await connection.QueryAsync<StoryBibleTimelineSummary>("dbo.StoryBible_TimelineSummary", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
}
public async Task<IReadOnlyList<StoryBibleAttentionItem>> GetOpenQuestionsAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return (await connection.QueryAsync<StoryBibleAttentionItem>("dbo.StoryBible_OpenQuestions", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
}
public async Task<IReadOnlyList<StoryBibleRevisionItem>> GetRevisionProgressAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return (await connection.QueryAsync<StoryBibleRevisionItem>("dbo.StoryBible_RevisionProgress", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList();
}
public async Task<IReadOnlyList<StoryBibleSearchResult>> SearchAsync(int projectId, string? query)
{
using var connection = connectionFactory.CreateConnection();
return (await connection.QueryAsync<StoryBibleSearchResult>("dbo.StoryBible_Search", new { ProjectID = projectId, Query = query }, commandType: CommandType.StoredProcedure)).ToList();
}
}
public sealed class WriterWorkspaceRepository(ISqlConnectionFactory connectionFactory) : IWriterWorkspaceRepository
{
public async Task<IReadOnlyList<SceneNoteType>> ListNoteTypesAsync()
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneNoteType>("dbo.SceneNoteType_List", commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<SceneAttachmentType>> ListAttachmentTypesAsync()
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneAttachmentType>("dbo.SceneAttachmentType_List", commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<SceneWorkflow> GetWorkflowAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<SceneWorkflow>("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<IReadOnlyList<SceneNote>> ListNotesAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneNote>("dbo.SceneNote_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveNoteAsync(SceneNote note)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<SceneChecklistItem>> ListChecklistItemsAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneChecklistItem>("dbo.SceneChecklist_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveChecklistItemAsync(SceneChecklistItem item)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<SceneAttachment>> ListAttachmentsAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneAttachment>("dbo.SceneAttachment_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveAttachmentAsync(SceneAttachment attachment)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<ChapterGoal> GetChapterGoalAsync(int chapterId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<ChapterGoal>("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<IReadOnlyList<ChapterWorkflowScene>> GetChapterWorkflowAsync(int chapterId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ChapterWorkflowScene>("dbo.ChapterWorkflow_Summary", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<WriterDashboardScene>> GetWriterDashboardScenesAsync(int? projectId, string queue)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WriterDashboardScene>("dbo.WriterDashboard_Scenes", new { ProjectID = projectId, Queue = queue }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<StoryBibleAttentionItem>> GetWriterDashboardRemindersAsync(int? projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<StoryBibleAttentionItem>("dbo.WriterDashboard_Reminders", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
}
public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFactory) : IWritingScheduleRepository
{
public async Task<IReadOnlyList<WritingPlan>> GetWritingPlansByUserAsync(int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingPlan>("dbo.GetWritingPlansByUser", new { UserID = userId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<WritingPlan?> GetWritingPlanByIDAsync(int writingPlanId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WritingPlan>("dbo.GetWritingPlanByID", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure);
}
public async Task<int> CreateWritingPlanAsync(WritingPlan plan)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.CreateWritingPlan",
new
{
plan.UserID,
plan.ProjectID,
plan.BookID,
plan.PlanName,
plan.GoalType,
plan.StartDate,
plan.DeadlineDate,
plan.AvailableDaysJson,
plan.SessionLengthMinutes,
plan.IncludeBlockedScenes,
plan.RebalanceMode,
plan.IsActive
},
commandType: CommandType.StoredProcedure);
}
public async Task UpdateWritingPlanAsync(WritingPlan plan)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.UpdateWritingPlan",
new
{
plan.WritingPlanID,
plan.UserID,
plan.ProjectID,
plan.BookID,
plan.PlanName,
plan.GoalType,
plan.StartDate,
plan.DeadlineDate,
plan.AvailableDaysJson,
plan.SessionLengthMinutes,
plan.IncludeBlockedScenes,
plan.RebalanceMode,
plan.IsActive
},
commandType: CommandType.StoredProcedure);
}
public async Task DeleteWritingPlanAsync(int writingPlanId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync("dbo.DeleteWritingPlan", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure);
}
public async Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync("dbo.SetWritingPlanActive", new { WritingPlanID = writingPlanId, IsActive = isActive }, commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByPlanAsync(int writingPlanId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingScheduleItem>("dbo.GetScheduleItemsByPlan", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingScheduleItem>(
"dbo.GetScheduleItemsByDateRange",
new { WritingPlanID = writingPlanId, StartDate = startDate, EndDate = endDate },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> CreateScheduleItemAsync(WritingScheduleItem item)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.CreateScheduleItem",
new
{
item.WritingPlanID,
item.SceneID,
item.ScheduledDate,
item.TaskType,
item.EstimatedMinutes,
item.TargetWords,
item.ScheduleStatus,
item.CompletedDateUtc,
item.Notes
},
commandType: CommandType.StoredProcedure);
}
public async Task UpdateScheduleItemAsync(WritingScheduleItem item)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.UpdateScheduleItem",
new
{
item.WritingScheduleItemID,
item.WritingPlanID,
item.SceneID,
item.ScheduledDate,
item.TaskType,
item.EstimatedMinutes,
item.TargetWords,
item.ScheduleStatus,
item.CompletedDateUtc,
item.Notes
},
commandType: CommandType.StoredProcedure);
}
public async Task DeleteScheduleItemAsync(int writingScheduleItemId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync("dbo.DeleteScheduleItem", new { WritingScheduleItemID = writingScheduleItemId }, commandType: CommandType.StoredProcedure);
}
public async Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.UpdateScheduleItemStatus",
new { WritingScheduleItemID = writingScheduleItemId, ScheduleStatus = scheduleStatus, CompletedDateUtc = completedDateUtc },
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<WritingPlanManagementItem>> GetWritingPlansForManagementAsync(int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingPlanManagementItem>(
"""
SELECT wp.WritingPlanID,
wp.UserID,
wp.ProjectID,
p.ProjectName,
wp.BookID,
b.BookTitle,
wp.PlanName,
wp.GoalType,
wp.StartDate,
wp.DeadlineDate,
wp.AvailableDaysJson,
wp.SessionLengthMinutes,
wp.IsActive,
SUM(CASE WHEN wsi.ScheduleStatus = N'Planned' THEN 1 ELSE 0 END) AS PlannedTaskCount,
MAX(CASE WHEN wsi.ScheduleStatus = N'Planned' THEN wsi.ScheduledDate ELSE NULL END) AS ProjectedFinishDate
FROM dbo.WritingPlans wp
INNER JOIN dbo.Projects p ON p.ProjectID = wp.ProjectID
LEFT JOIN dbo.Books b ON b.BookID = wp.BookID
LEFT JOIN dbo.WritingScheduleItems wsi ON wsi.WritingPlanID = wp.WritingPlanID
WHERE wp.UserID = @UserID
GROUP BY wp.WritingPlanID,
wp.UserID,
wp.ProjectID,
p.ProjectName,
wp.BookID,
b.BookTitle,
wp.PlanName,
wp.GoalType,
wp.StartDate,
wp.DeadlineDate,
wp.AvailableDaysJson,
wp.SessionLengthMinutes,
wp.IsActive
ORDER BY wp.IsActive DESC, wp.DeadlineDate, wp.PlanName, wp.WritingPlanID;
""",
new { UserID = userId });
return rows.ToList();
}
public async Task<IReadOnlyList<WritingScheduleScene>> GetSchedulingScenesAsync(int projectId, int? bookId, bool includeBlockedScenes)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingScheduleScene>(
"""
SELECT s.SceneID,
rs.StatusName AS RevisionStatusName,
sw.EstimatedWordCount,
sw.Priority,
COALESCE(sw.IsBlocked, 0) AS IsBlocked,
b.SortOrder AS BookSortOrder,
c.SortOrder AS ChapterSortOrder,
s.SortOrder AS SceneSortOrder
FROM dbo.Projects p
INNER JOIN dbo.Books b ON b.ProjectID = p.ProjectID AND b.IsArchived = 0
INNER JOIN dbo.Chapters c ON c.BookID = b.BookID AND c.IsArchived = 0
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
WHERE p.ProjectID = @ProjectID
AND p.IsArchived = 0
AND (@BookID IS NULL OR b.BookID = @BookID)
AND rs.StatusName IN (N'Outlined', N'Needs Work', N'Revised')
AND (@IncludeBlockedScenes = 1 OR COALESCE(sw.IsBlocked, 0) = 0)
ORDER BY COALESCE(sw.Priority, 0) DESC, b.SortOrder, c.SortOrder, s.SortOrder, s.SceneID;
""",
new { ProjectID = projectId, BookID = bookId, IncludeBlockedScenes = includeBlockedScenes });
return rows.ToList();
}
public async Task ReplaceScheduleItemsAsync(int writingPlanId, IReadOnlyList<WritingScheduleItem> items)
{
using var connection = connectionFactory.CreateConnection();
connection.Open();
using var transaction = connection.BeginTransaction();
try
{
await connection.ExecuteAsync(
"DELETE FROM dbo.WritingScheduleItems WHERE WritingPlanID = @WritingPlanID;",
new { WritingPlanID = writingPlanId },
transaction);
foreach (var item in items)
{
await connection.ExecuteAsync(
"""
INSERT dbo.WritingScheduleItems
(
WritingPlanID, SceneID, ScheduledDate, TaskType, EstimatedMinutes, TargetWords,
ScheduleStatus, CompletedDateUtc, Notes
)
VALUES
(
@WritingPlanID, @SceneID, @ScheduledDate, @TaskType, @EstimatedMinutes, @TargetWords,
@ScheduleStatus, @CompletedDateUtc, @Notes
);
""",
new
{
item.WritingPlanID,
item.SceneID,
item.ScheduledDate,
item.TaskType,
item.EstimatedMinutes,
item.TargetWords,
item.ScheduleStatus,
item.CompletedDateUtc,
item.Notes
},
transaction);
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
public async Task<IReadOnlyList<WritingPlanSummary>> GetWritingPlanSummariesByUserAsync(int userId, int? projectId = null)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingPlanSummary>(
"""
SELECT wp.WritingPlanID,
wp.UserID,
wp.ProjectID,
p.ProjectName,
wp.BookID,
b.BookTitle,
wp.PlanName,
wp.DeadlineDate,
wp.IsActive
FROM dbo.WritingPlans wp
INNER JOIN dbo.Projects p ON p.ProjectID = wp.ProjectID
LEFT JOIN dbo.Books b ON b.BookID = wp.BookID
WHERE wp.UserID = @UserID
AND (@ProjectID IS NULL OR wp.ProjectID = @ProjectID)
ORDER BY wp.IsActive DESC, wp.DeadlineDate, wp.PlanName, wp.WritingPlanID;
""",
new { UserID = userId, ProjectID = projectId });
return rows.ToList();
}
public async Task<IReadOnlyList<WritingSchedulePreviewItem>> GetSchedulePreviewItemsAsync(int writingPlanId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingSchedulePreviewItem>(
"""
SELECT wsi.WritingScheduleItemID,
wsi.WritingPlanID,
wsi.SceneID,
wsi.ScheduledDate,
wsi.TaskType,
b.BookTitle,
c.ChapterNumber,
c.ChapterTitle,
s.SceneNumber,
s.SceneTitle,
sw.Priority,
wsi.EstimatedMinutes,
wsi.TargetWords,
wsi.ScheduleStatus,
wsi.Notes,
COALESCE(sw.IsBlocked, 0) AS IsBlocked
FROM dbo.WritingScheduleItems wsi
INNER JOIN dbo.Scenes s ON s.SceneID = wsi.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
WHERE wsi.WritingPlanID = @WritingPlanID
ORDER BY wsi.ScheduledDate, wsi.WritingScheduleItemID;
""",
new { WritingPlanID = writingPlanId });
return rows.ToList();
}
public async Task DeleteScheduleItemsByPlanAsync(int writingPlanId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"DELETE FROM dbo.WritingScheduleItems WHERE WritingPlanID = @WritingPlanID;",
new { WritingPlanID = writingPlanId });
}
public async Task<IReadOnlyList<WritingScheduleDashboardItem>> GetActiveScheduleDashboardItemsAsync(int userId, int? projectId = null)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingScheduleDashboardItem>(
"""
SELECT wsi.WritingScheduleItemID,
wsi.WritingPlanID,
wp.PlanName,
wsi.SceneID,
wsi.ScheduledDate,
wsi.TaskType,
b.BookTitle,
c.ChapterNumber,
c.ChapterTitle,
c.SortOrder AS ChapterSortOrder,
s.SceneNumber,
s.SceneTitle,
s.SortOrder AS SceneSortOrder,
sw.Priority,
wsi.EstimatedMinutes,
wsi.TargetWords,
wsi.ScheduleStatus,
COALESCE(sw.IsBlocked, 0) AS IsBlocked
FROM dbo.WritingPlans wp
INNER JOIN dbo.WritingScheduleItems wsi ON wsi.WritingPlanID = wp.WritingPlanID
INNER JOIN dbo.Scenes s ON s.SceneID = wsi.SceneID
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = c.BookID
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
WHERE wp.UserID = @UserID
AND wp.IsActive = 1
AND (@ProjectID IS NULL OR wp.ProjectID = @ProjectID)
ORDER BY wsi.ScheduledDate, wsi.WritingScheduleItemID;
""",
new { UserID = userId, ProjectID = projectId });
return rows.ToList();
}
public async Task<int> UpdatePlannedScheduleItemStatusForUserAsync(int writingScheduleItemId, int userId, string scheduleStatus, DateTime? completedDateUtc)
{
using var connection = connectionFactory.CreateConnection();
return await connection.ExecuteAsync(
"""
UPDATE wsi
SET ScheduleStatus = @ScheduleStatus,
CompletedDateUtc = @CompletedDateUtc,
ModifiedDateUtc = SYSUTCDATETIME()
FROM dbo.WritingScheduleItems wsi
INNER JOIN dbo.WritingPlans wp ON wp.WritingPlanID = wsi.WritingPlanID
WHERE wsi.WritingScheduleItemID = @WritingScheduleItemID
AND wp.UserID = @UserID
AND wp.IsActive = 1
AND wsi.ScheduleStatus = N'Planned';
""",
new
{
WritingScheduleItemID = writingScheduleItemId,
UserID = userId,
ScheduleStatus = scheduleStatus,
CompletedDateUtc = completedDateUtc
});
}
public async Task<IReadOnlyList<WritingPlanSummary>> GetPlansRequiringRebalanceAsync(int userId, DateTime today, int? projectId = null)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingPlanSummary>(
"""
SELECT wp.WritingPlanID,
wp.UserID,
wp.ProjectID,
p.ProjectName,
wp.BookID,
b.BookTitle,
wp.PlanName,
wp.DeadlineDate,
wp.IsActive
FROM dbo.WritingPlans wp
INNER JOIN dbo.Projects p ON p.ProjectID = wp.ProjectID
LEFT JOIN dbo.Books b ON b.BookID = wp.BookID
WHERE wp.UserID = @UserID
AND wp.IsActive = 1
AND (@ProjectID IS NULL OR wp.ProjectID = @ProjectID)
AND EXISTS
(
SELECT 1
FROM dbo.WritingScheduleItems wsi
WHERE wsi.WritingPlanID = wp.WritingPlanID
AND wsi.ScheduleStatus = N'Planned'
AND wsi.ScheduledDate < @Today
)
ORDER BY wp.DeadlineDate, wp.PlanName, wp.WritingPlanID;
""",
new { UserID = userId, Today = today.Date, ProjectID = projectId });
return rows.ToList();
}
public async Task UpdatePlannedScheduleItemDatesAsync(int writingPlanId, IReadOnlyList<WritingScheduleItemDateChange> changes)
{
if (changes.Count == 0)
{
return;
}
using var connection = connectionFactory.CreateConnection();
connection.Open();
using var transaction = connection.BeginTransaction();
try
{
foreach (var change in changes)
{
await connection.ExecuteAsync(
"""
UPDATE dbo.WritingScheduleItems
SET ScheduledDate = @ScheduledDate,
ModifiedDateUtc = SYSUTCDATETIME()
WHERE WritingPlanID = @WritingPlanID
AND WritingScheduleItemID = @WritingScheduleItemID
AND ScheduleStatus = N'Planned';
""",
new
{
WritingPlanID = writingPlanId,
change.WritingScheduleItemID,
ScheduledDate = change.ScheduledDate.Date
},
transaction);
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
public sealed class ExportRepository(ISqlConnectionFactory connectionFactory) : IExportRepository
{
public async Task<ExportPacket> 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<StoryBibleCharacterSummary>()).ToList(),
PlotThreads = (await result.ReadAsync<StoryBibleThreadSummary>()).ToList(),
Assets = (await result.ReadAsync<StoryBibleAssetSummary>()).ToList(),
Relationships = (await result.ReadAsync<StoryBibleRelationshipSummary>()).ToList(),
Locations = (await result.ReadAsync<StoryBibleLocationSummary>()).ToList(),
Timeline = (await result.ReadAsync<StoryBibleTimelineSummary>()).ToList(),
OpenQuestions = (await result.ReadAsync<StoryBibleAttentionItem>()).ToList(),
RevisionItems = (await result.ReadAsync<StoryBibleRevisionItem>()).ToList()
};
}
public async Task<ExportPacket> GetNarrativeTimelineAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<StoryBibleTimelineSummary>("dbo.Export_NarrativeTimeline", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return new ExportPacket { Title = "Narrative Timeline Summary", Timeline = rows.ToList() };
}
public async Task<ExportPacket> 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<Character>(),
CharacterRelationships = (await result.ReadAsync<CharacterRelationship>()).ToList(),
CharacterAppearances = (await result.ReadAsync<SceneCharacter>()).ToList(),
CharacterKnowledge = (await result.ReadAsync<CharacterKnowledgeItem>()).ToList(),
CharacterCustody = (await result.ReadAsync<AssetCustodyEvent>()).ToList(),
Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
};
}
public async Task<ExportPacket> 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<ExportSceneBriefHeader>(),
Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
};
}
public async Task<ExportPacket> 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<ExportChapterBriefHeader>(),
Timeline = (await result.ReadAsync<StoryBibleTimelineSummary>()).ToList(),
Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
};
}
public async Task<ExportPacket> 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<ExportSceneBriefHeader>(),
Items = (await result.ReadAsync<ExportBriefItem>()).ToList()
};
}
public async Task<ExportPacket> GetResearchPackAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ExportResearchItem>("dbo.Export_ResearchPack", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return new ExportPacket { Title = "Research Pack", ResearchItems = rows.ToList() };
}
public async Task<ExportPacket> GetSearchResultsAsync(int projectId, string query)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<StoryBibleSearchResult>("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<CharacterArcPoint> ArcPoints, IReadOnlyList<RelationshipEvent> RelationshipEvents, IReadOnlyList<CharacterKnowledgeItem> KnowledgeEvents, IReadOnlyList<DynamicsIssue> 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<Character>(),
(await result.ReadAsync<CharacterArcPoint>()).ToList(),
(await result.ReadAsync<RelationshipEvent>()).ToList(),
(await result.ReadAsync<CharacterKnowledgeItem>()).ToList(),
(await result.ReadAsync<DynamicsIssue>()).ToList());
}
public async Task<(CharacterRelationship? Relationship, IReadOnlyList<RelationshipProgressionPoint> Events, IReadOnlyList<DynamicsIssue> 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<CharacterRelationship>(),
(await result.ReadAsync<RelationshipProgressionPoint>()).ToList(),
(await result.ReadAsync<DynamicsIssue>()).ToList());
}
public async Task<(Character? Character, IReadOnlyList<CharacterKnowledgeItem> KnowledgeEvents, IReadOnlyList<DynamicsIssue> 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<Character>(),
(await result.ReadAsync<CharacterKnowledgeItem>()).ToList(),
(await result.ReadAsync<DynamicsIssue>()).ToList());
}
public async Task<(IReadOnlyList<CharacterPairingSummary> Pairings, IReadOnlyList<DynamicsIssue> 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<CharacterPairingSummary>()).ToList(), (await result.ReadAsync<DynamicsIssue>()).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<IReadOnlyList<Scenario>> ListAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Scenario>("dbo.Scenario_List", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<Scenario?> GetAsync(int scenarioId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Scenario>("dbo.Scenario_Get", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure);
}
public async Task<int> CreateAsync(int projectId, int? bookId, string scenarioName, string? description)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.Scenario_Create",
new { ProjectID = projectId, BookID = bookId, ScenarioName = scenarioName, Description = description },
commandType: CommandType.StoredProcedure);
}
public async Task<(IReadOnlyList<Book> Books, IReadOnlyList<Chapter> Chapters, IReadOnlyList<ScenarioScene> Scenes, IReadOnlyList<ScenarioWarning> 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<Book>()).ToList(),
(await result.ReadAsync<Chapter>()).ToList(),
(await result.ReadAsync<ScenarioScene>()).ToList(),
(await result.ReadAsync<ScenarioWarning>()).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<int> ValidateAsync(int scenarioId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>("dbo.Scenario_Validate", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure);
}
public async Task<(IReadOnlyList<ScenarioComparisonRow> MovedScenes, IReadOnlyList<ScenarioWarning> 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<ScenarioComparisonRow>()).ToList(), (await result.ReadAsync<ScenarioWarning>()).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<IReadOnlyList<ArchivedItem>> ListAsync(int? projectId, string? entityType)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ArchivedItem>(
"dbo.Archive_List",
new { ProjectID = projectId, EntityType = entityType },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<ArchivedItem>> ListForUserAsync(int? projectId, string? entityType, int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ArchivedItem>(
"dbo.Archive_ListForUser",
new { ProjectID = projectId, EntityType = entityType, UserID = userId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<Project>> ListProjectFiltersAsync()
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Project>(
"dbo.Archive_ProjectFilterList",
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<Project>> ListProjectFiltersForUserAsync(int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Project>(
"dbo.Archive_ProjectFilterListForUser",
new { UserID = userId },
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<ArchiveDeleteResult> DeleteForeverAsync(string entityType, int entityId, string confirmationText)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<ArchiveDeleteResult>(
"dbo.Archive_DeleteForever",
new { EntityType = entityType, EntityID = entityId, ConfirmationText = confirmationText },
commandType: CommandType.StoredProcedure);
}
}
public sealed class AcceptanceRepository(ISqlConnectionFactory connectionFactory) : IAcceptanceRepository
{
public async Task<IReadOnlyList<Phase1AcceptanceChecklistItem>> GetPhase1ChecklistAsync(int? projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Phase1AcceptanceChecklistItem>(
"dbo.Acceptance_Phase1Checklist",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
}
public sealed class SceneDependencyRepository(ISqlConnectionFactory connectionFactory) : ISceneDependencyRepository
{
public async Task<SceneDependencyLookupData> GetLookupsAsync()
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneDependencyType>("dbo.SceneDependencyLookup_All", commandType: CommandType.StoredProcedure);
return new SceneDependencyLookupData { DependencyTypes = rows.ToList() };
}
public async Task<IReadOnlyList<SceneDependency>> ListBySceneAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneDependency>("dbo.SceneDependency_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<SceneDependency>> ListByProjectAsync(int projectId, int? bookId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneDependency>("dbo.SceneDependency_ListByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<SceneDependencyCount>> GetCountsAsync(int projectId, int? bookId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneDependencyCount>("dbo.SceneDependency_Counts", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveAsync(SceneDependency dependency)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<Project>> ListAsync()
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Project>("dbo.Project_List", commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<Project>> ListForUserAsync(int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Project>("dbo.Project_ListForUser", new { UserID = userId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<Project>> ListActiveForUserAsync(int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Project>("dbo.Project_ListActiveForUser", new { UserID = userId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<Project>> ListArchivedForUserAsync(int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Project>("dbo.Project_ListArchivedForUser", new { UserID = userId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<Project>> ListOwnedForAccountDeletionAsync(int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Project>(
"dbo.Project_ListOwnedForAccountDeletion",
new { UserID = userId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<Project?> GetAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Project>("dbo.Project_Get", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
}
public async Task<Project?> GetForUserAsync(int projectId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Project>("dbo.Project_GetForUser", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure);
}
public async Task<Project?> GetArchivedForOwnerAsync(int projectId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Project>("dbo.Project_GetArchivedForOwner", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure);
}
public async Task<ProjectDashboardMetrics> GetDashboardMetricsAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<ProjectDashboardMetrics>(
"""
SELECT
(SELECT COUNT(1) FROM dbo.Characters WHERE ProjectID = @ProjectID AND IsArchived = 0) AS CharacterCount,
(SELECT COUNT(1) FROM dbo.Locations WHERE ProjectID = @ProjectID AND IsArchived = 0) AS LocationCount,
(
SELECT COUNT(1)
FROM dbo.PlotThreads pt
INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID
INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID
WHERE pl.ProjectID = @ProjectID
AND pl.IsArchived = 0
AND pt.IsArchived = 0
AND ts.IsOpenStatus = 1
) AS ActivePlotThreadCount,
(
SELECT COUNT(1)
FROM dbo.ContinuityWarnings
WHERE ProjectID = @ProjectID
AND IsDismissed = 0
AND IsIntentional = 0
) AS ContinuityWarningCount;
""",
new { ProjectID = projectId });
}
public async Task<IReadOnlyList<GenreMetricPreset>> ListGenreMetricPresetsAsync()
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<GenreMetricPreset>("dbo.GenreMetricPreset_List", commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public Task<int> SaveAsync(Project project) => SaveAsync(project, null);
public Task<int> SaveAsync(Project project, int userId, string? genreMetricPresetKey)
=> SaveInternalAsync(project, userId, genreMetricPresetKey);
public async Task<int> SaveAsync(Project project, string? genreMetricPresetKey)
=> await SaveInternalAsync(project, null, genreMetricPresetKey);
private async Task<int> SaveInternalAsync(Project project, int? userId, string? genreMetricPresetKey)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.Project_Save",
new { project.ProjectID, project.ProjectName, project.Description, GenreMetricPresetKey = genreMetricPresetKey, UserID = userId },
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 async Task ArchiveForUserAsync(int projectId, int userId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync("dbo.Project_ArchiveForUser", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure);
}
public async Task<bool> ArchiveForOwnerAsync(int projectId, int userId)
{
using var connection = connectionFactory.CreateConnection();
var rowsAffected = await connection.QuerySingleAsync<int>(
"dbo.Project_ArchiveForOwner",
new { ProjectID = projectId, UserID = userId },
commandType: CommandType.StoredProcedure);
return rowsAffected > 0;
}
public async Task<bool> RestoreForOwnerAsync(int projectId, int userId)
{
using var connection = connectionFactory.CreateConnection();
var rowsAffected = await connection.QuerySingleAsync<int>(
"dbo.Project_RestoreForOwner",
new { ProjectID = projectId, UserID = userId },
commandType: CommandType.StoredProcedure);
return rowsAffected > 0;
}
public async Task<IReadOnlyList<string>> ListHardDeleteFilePathsForOwnerAsync(int projectId, int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<string>(
"dbo.ProjectHardDelete_ListFilePathsForOwner",
new { ProjectID = projectId, UserID = userId },
commandType: CommandType.StoredProcedure);
return rows.Where(path => !string.IsNullOrWhiteSpace(path)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
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);
}
public async Task<IReadOnlyList<string>> ListHardDeleteFilePathsForAccountDeletionAsync(int projectId, int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<string>(
"dbo.ProjectHardDelete_ListFilePathsForAccountDeletion",
new { ProjectID = projectId, UserID = userId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
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);
}
}
public sealed class ProjectCollaborationRepository(ISqlConnectionFactory connectionFactory) : IProjectCollaborationRepository
{
public async Task<int?> GetOwnerUserIdAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<int?>(
"dbo.ProjectCollaboration_GetOwner",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<ProjectCollaborator>> ListCollaboratorsAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ProjectCollaborator>(
"dbo.ProjectCollaboration_ListCollaborators",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<ProjectInvitation>> ListPendingInvitationsAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ProjectInvitation>(
"dbo.ProjectCollaboration_ListPendingInvitations",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<bool> CollaboratorCountsForOwnerAsync(int ownerUserId, int collaboratorUserId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<bool>(
"dbo.ProjectCollaboration_CollaboratorCountsForOwner",
new { OwnerUserID = ownerUserId, CollaboratorUserID = collaboratorUserId },
commandType: CommandType.StoredProcedure);
}
public async Task<bool> AddCollaboratorAsync(int projectId, int userId, int invitedByUserId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<bool>(
"dbo.ProjectCollaboration_AddCollaborator",
new { ProjectID = projectId, UserID = userId, InvitedByUserID = invitedByUserId },
commandType: CommandType.StoredProcedure);
}
public async Task<bool> RemoveCollaboratorAsync(int projectId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<bool>(
"dbo.ProjectCollaboration_RemoveCollaborator",
new { ProjectID = projectId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
public async Task<int> CreatePendingInvitationAsync(int projectId, string emailAddress, int invitedByUserId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.ProjectCollaboration_CreatePendingInvitation",
new { ProjectID = projectId, EmailAddress = emailAddress, InvitedByUserID = invitedByUserId },
commandType: CommandType.StoredProcedure);
}
}
public sealed class ProjectActivityRepository(ISqlConnectionFactory connectionFactory) : IProjectActivityRepository
{
public async Task<IReadOnlyList<ProjectActivity>> ListForProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ProjectActivity>(
"dbo.ProjectActivity_ListForProject",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<long> RecordAsync(int projectId, int? userId, string activityType, string entityType, int? entityId, string? entityName, string? description = null)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<long>(
"dbo.ProjectAudit_Record",
new
{
ProjectID = projectId,
UserID = userId,
ActivityType = activityType,
EntityType = entityType,
EntityID = entityId,
EntityName = entityName,
Description = description
},
commandType: CommandType.StoredProcedure);
}
}
public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IBookRepository
{
public async Task<IReadOnlyList<Book>> ListByProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Book>("dbo.Book_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<Book?> GetAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Book>("dbo.Book_Get", new { BookID = bookId }, commandType: CommandType.StoredProcedure);
}
public async Task<Book?> GetCoverPathsAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Book>(
"""
SELECT BookID, ProjectID, CoverThumbnailPath, CoverStandardPath
FROM dbo.Books
WHERE BookID = @BookID;
""",
new { BookID = bookId });
}
public async Task<int> SaveAsync(Book book)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.Book_Save",
new
{
book.BookID,
book.ProjectID,
book.BookTitle,
book.Subtitle,
book.Tagline,
book.ShortDescription,
book.Status,
book.BookNumber,
book.Description,
book.TargetWordCount,
book.WritingStartDate,
book.TargetCompletionDate,
book.PublicationDate
},
commandType: CommandType.StoredProcedure);
}
public async Task UpdateCoverAsync(int bookId, string? thumbnailPath, string? standardPath)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.Book_UpdateCover",
new { BookID = bookId, CoverThumbnailPath = thumbnailPath, CoverStandardPath = standardPath },
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<IReadOnlyList<Chapter>> ListByBookAsync(int bookId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Chapter>("dbo.Chapter_ListByBook", new { BookID = bookId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<Chapter?> GetAsync(int chapterId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Chapter>("dbo.Chapter_Get", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure);
}
public async Task<int> SaveAsync(Chapter chapter)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<Scene>> ListByChapterAsync(int chapterId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Scene>("dbo.Scene_ListByChapter", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<Scene?> 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<Scene>();
if (scene is null)
{
return null;
}
scene.SelectedPurposeTypeIds = (await result.ReadAsync<int>()).ToList();
scene.Metrics = (await result.ReadAsync<SceneMetricValue>()).ToList();
return scene;
}
public async Task<int> SaveAsync(Scene scene)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<int> 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<SceneMetricValue> 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 SaveMetricValueAsync(int sceneId, int metricTypeId, int value)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"""
MERGE dbo.SceneMetricValues AS target
USING (SELECT @SceneID AS SceneID, @MetricTypeID AS MetricTypeID) AS source
ON target.SceneID = source.SceneID AND target.MetricTypeID = source.MetricTypeID
WHEN MATCHED THEN
UPDATE SET Value = @Value
WHEN NOT MATCHED THEN
INSERT (SceneID, MetricTypeID, Value, Notes)
VALUES (@SceneID, @MetricTypeID, @Value, NULL);
""",
new { SceneID = sceneId, MetricTypeID = metricTypeId, Value = value });
}
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<IReadOnlyList<ChapterOption>> ListChapterOptionsAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ChapterOption>("dbo.ChapterOption_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<SceneMovePreview?> GetMovePreviewAsync(int sceneId, int targetChapterId, string position)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<SceneMovePreview>(
"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<LookupData> 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<RevisionStatus>()).ToList(),
TimeModes = (await result.ReadAsync<TimeMode>()).ToList(),
DurationUnits = (await result.ReadAsync<DurationUnit>()).ToList(),
TimeConfidences = (await result.ReadAsync<TimeConfidence>()).ToList(),
ScenePurposeTypes = (await result.ReadAsync<ScenePurposeType>()).ToList()
};
}
}
public sealed class TimelineRepository(ISqlConnectionFactory connectionFactory) : ITimelineRepository
{
public async Task<TimelineData> GetByProjectAsync(int projectId, int? bookId, bool includeMetrics, IEnumerable<int> metricTypeIds, bool includePlotLines)
{
using var connection = connectionFactory.CreateConnection();
var selectedMetricIds = string.Join(',', metricTypeIds.Where(id => id > 0).Distinct());
using var result = await connection.QueryMultipleAsync(
"dbo.Timeline_GetByProject",
new
{
ProjectID = projectId,
BookID = bookId,
IncludeMetrics = includeMetrics,
SelectedMetricTypeIDs = selectedMetricIds,
IncludePlotLines = includePlotLines
},
commandType: CommandType.StoredProcedure);
var project = await result.ReadSingleOrDefaultAsync<Project>();
var books = (await result.ReadAsync<Book>()).ToList();
var chapters = (await result.ReadAsync<Chapter>()).ToList();
var scenes = (await result.ReadAsync<Scene>()).ToList();
var purposeLabels = (await result.ReadAsync<ScenePurposeLabel>()).ToList();
var metricTypes = (await result.ReadAsync<SceneMetricType>()).ToList();
var metricPoints = (await result.ReadAsync<TimelineMetricPoint>()).ToList();
var plotLines = (await result.ReadAsync<PlotLineItem>()).ToList();
var threadEvents = (await result.ReadAsync<ThreadEvent>()).ToList();
await PopulateThreadEventTargetsAsync(connection, threadEvents);
return new TimelineData
{
Project = project,
Books = books,
Chapters = chapters,
Scenes = scenes,
PurposeLabels = purposeLabels,
MetricTypes = metricTypes,
MetricPoints = metricPoints,
PlotLines = plotLines,
ThreadEvents = threadEvents
};
}
private static async Task PopulateThreadEventTargetsAsync(IDbConnection connection, IReadOnlyList<ThreadEvent> threadEvents)
{
var threadEventIds = threadEvents.Select(x => x.ThreadEventID).ToList();
if (threadEventIds.Count == 0)
{
return;
}
var targetRows = await connection.QueryAsync<ThreadEventTargetRow>(
"SELECT PlotEventID, PlotLineID FROM dbo.PlotEventTargetPlotLines WHERE PlotEventID IN @ThreadEventIDs;",
new { ThreadEventIDs = threadEventIds });
var targetsByEvent = targetRows
.GroupBy(x => x.PlotEventID)
.ToDictionary(x => x.Key, x => (IReadOnlyList<int>)x.Select(row => row.PlotLineID).ToList());
foreach (var threadEvent in threadEvents)
{
if (targetsByEvent.TryGetValue(threadEvent.ThreadEventID, out var targets))
{
threadEvent.TargetPlotLineIDs = targets;
}
}
}
private sealed class ThreadEventTargetRow
{
public int PlotEventID { get; set; }
public int PlotLineID { get; set; }
}
}
public sealed class TimelineSettingsRepository(ISqlConnectionFactory connectionFactory) : ITimelineSettingsRepository
{
public async Task<ProjectTimelineSettings?> GetAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<ProjectTimelineSettings>(
"dbo.ProjectTimelineSettings_Get",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<SceneMetricType>> ListMetricTypesAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneMetricType>(
"dbo.ProjectTimelineSettings_MetricTypes",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<ProjectTimelineMetricSetting>> ListMetricSettingsAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<ProjectTimelineMetricSetting>(
"dbo.ProjectTimelineMetricSettings_List",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task SaveAsync(ProjectTimelineSettings settings, IEnumerable<int> metricTypeIds)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync(
"dbo.ProjectTimelineSettings_Save",
new
{
settings.ProjectID,
settings.ShowSceneCards,
settings.ShowMetricShape,
settings.ShowPlotLines,
settings.ShowStoryAssets,
settings.ShowCharacterAppearances,
settings.ShowWarnings,
MetricTypeIDs = string.Join(',', metricTypeIds.Where(id => id > 0).Distinct())
},
commandType: CommandType.StoredProcedure);
}
}
public sealed class SceneMetricTypeRepository(ISqlConnectionFactory connectionFactory) : ISceneMetricTypeRepository
{
public async Task<IReadOnlyList<SceneMetricType>> ListForManagementAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneMetricType>(
"dbo.SceneMetricType_ListForProjectManagement",
new { ProjectID = projectId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<SceneMetricType?> GetAsync(int metricTypeId, int projectId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<SceneMetricType>(
"dbo.SceneMetricType_GetForProject",
new { MetricTypeID = metricTypeId, ProjectID = projectId },
commandType: CommandType.StoredProcedure);
}
public async Task<int> SaveAsync(SceneMetricType metric)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.SceneMetricType_SaveForProject",
new
{
metric.MetricTypeID,
metric.ProjectID,
metric.MetricName,
metric.Description,
metric.MinValue,
metric.MaxValue,
metric.DefaultValue,
metric.SortOrder,
metric.IsActive
},
commandType: CommandType.StoredProcedure);
}
public async Task<bool> SetActiveAsync(int metricTypeId, int projectId, bool isActive)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<bool>(
"dbo.SceneMetricType_SetActiveForProject",
new { MetricTypeID = metricTypeId, ProjectID = projectId, IsActive = isActive },
commandType: CommandType.StoredProcedure);
}
public async Task<bool> MoveAsync(int metricTypeId, int projectId, string direction)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<bool>(
"dbo.SceneMetricType_MoveForProject",
new { MetricTypeID = metricTypeId, ProjectID = projectId, Direction = direction },
commandType: CommandType.StoredProcedure);
}
public async Task AddMissingDefaultsAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
await connection.ExecuteAsync("dbo.SceneMetricType_AddMissingDefaultsForProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
}
public async Task<bool> RemoveUnusedAsync(int metricTypeId, int projectId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<bool>(
"dbo.SceneMetricType_RemoveUnusedForProject",
new { MetricTypeID = metricTypeId, ProjectID = projectId },
commandType: CommandType.StoredProcedure);
}
public async Task<bool> HasProjectMetricsAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<bool>("dbo.SceneMetricType_ProjectHasMetrics", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
}
}
public sealed class TimelinePresetRepository(ISqlConnectionFactory connectionFactory) : ITimelinePresetRepository
{
public async Task<IReadOnlyList<TimelineViewPreset>> ListAsync(int projectId, int? bookId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<TimelineViewPreset>(
"dbo.TimelineViewPreset_List",
new { ProjectID = projectId, BookID = bookId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<TimelineViewPreset?> GetAsync(int timelineViewPresetId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<TimelineViewPreset>(
"dbo.TimelineViewPreset_Get",
new { TimelineViewPresetID = timelineViewPresetId },
commandType: CommandType.StoredProcedure);
}
public async Task<int> SaveAsync(TimelineViewPreset preset)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<PlotLookupData> 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<PlotLineType>()).ToList(),
PlotImportance = (await result.ReadAsync<PlotImportance>()).ToList(),
ThreadTypes = (await result.ReadAsync<ThreadType>()).ToList(),
ThreadStatuses = (await result.ReadAsync<ThreadStatus>()).ToList(),
ThreadEventTypes = (await result.ReadAsync<ThreadEventType>()).ToList(),
PlotEventTypes = (await result.ReadAsync<PlotEventType>()).ToList()
};
}
public async Task<IReadOnlyList<PlotLineItem>> ListPlotLinesAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<PlotLineItem>("dbo.PlotLine_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<PlotLineItem?> GetPlotLineAsync(int plotLineId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<PlotLineItem>("dbo.PlotLine_Get", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure);
}
public async Task<int> SavePlotLineAsync(PlotLineItem plotLine)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.PlotLine_Save",
new
{
plotLine.PlotLineID,
plotLine.ProjectID,
plotLine.BookID,
plotLine.PlotLineName,
plotLine.PlotLineTypeID,
plotLine.PlotImportanceID,
plotLine.Description,
plotLine.ParentPlotLineID,
plotLine.EmergesFromPlotLineID,
plotLine.SortOrder,
plotLine.Colour,
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<IReadOnlyList<PlotThread>> ListPlotThreadsByProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<PlotThread>("dbo.PlotThread_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<PlotThread>> ListPlotThreadsByPlotLineAsync(int plotLineId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<PlotThread>("dbo.PlotThread_ListByPlotLine", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<PlotThread?> GetPlotThreadAsync(int plotThreadId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<PlotThread>("dbo.PlotThread_Get", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure);
}
public async Task<int> SavePlotThreadAsync(PlotThread plotThread)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<ThreadEvent>> ListThreadEventsBySceneAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = (await connection.QueryAsync<ThreadEvent>("dbo.ThreadEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure)).ToList();
await PopulateThreadEventTargetsAsync(connection, rows);
return rows;
}
public async Task<IReadOnlyList<ThreadEvent>> ListThreadEventsByPlotThreadAsync(int plotThreadId)
{
using var connection = connectionFactory.CreateConnection();
var rows = (await connection.QueryAsync<ThreadEvent>("dbo.ThreadEvent_ListByPlotThread", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure)).ToList();
await PopulateThreadEventTargetsAsync(connection, rows);
return rows;
}
private static async Task PopulateThreadEventTargetsAsync(IDbConnection connection, IReadOnlyList<ThreadEvent> threadEvents)
{
var threadEventIds = threadEvents.Select(x => x.ThreadEventID).ToList();
if (threadEventIds.Count == 0)
{
return;
}
var targetRows = await connection.QueryAsync<ThreadEventTargetRow>(
"SELECT PlotEventID, PlotLineID FROM dbo.PlotEventTargetPlotLines WHERE PlotEventID IN @ThreadEventIDs;",
new { ThreadEventIDs = threadEventIds });
var targetsByEvent = targetRows
.GroupBy(x => x.PlotEventID)
.ToDictionary(x => x.Key, x => (IReadOnlyList<int>)x.Select(row => row.PlotLineID).ToList());
foreach (var threadEvent in threadEvents)
{
if (targetsByEvent.TryGetValue(threadEvent.ThreadEventID, out var targets))
{
threadEvent.TargetPlotLineIDs = targets;
}
}
}
private sealed class ThreadEventTargetRow
{
public int PlotEventID { get; set; }
public int PlotLineID { get; set; }
}
public async Task<int> SaveThreadEventAsync(ThreadEvent threadEvent)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.ThreadEvent_Save",
new
{
threadEvent.ThreadEventID,
threadEvent.PlotThreadID,
threadEvent.SceneID,
threadEvent.EventTypeID,
threadEvent.PlotEventTypeID,
threadEvent.TargetPlotLineID,
threadEvent.EventTitle,
threadEvent.EventDescription
},
commandType: CommandType.StoredProcedure);
}
public async Task SaveThreadEventTargetsAsync(int threadEventId, IEnumerable<int> plotLineIds)
{
using var connection = connectionFactory.CreateConnection();
var targetIds = plotLineIds.Where(id => id > 0).Distinct().ToList();
await connection.ExecuteAsync(
"DELETE FROM dbo.PlotEventTargetPlotLines WHERE PlotEventID = @ThreadEventID;",
new { ThreadEventID = threadEventId });
if (targetIds.Count == 0)
{
return;
}
await connection.ExecuteAsync(
"INSERT dbo.PlotEventTargetPlotLines (PlotEventID, PlotLineID) VALUES (@ThreadEventID, @PlotLineID);",
targetIds.Select(plotLineId => new { ThreadEventID = threadEventId, PlotLineID = plotLineId }));
}
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<IReadOnlyList<SceneOption>> ListSceneOptionsAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneOption>("dbo.SceneOption_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
}
public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : IAssetRepository
{
public async Task<AssetLookupData> 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<AssetKind>()).ToList(),
AssetStates = (await result.ReadAsync<AssetState>()).ToList(),
AssetEventTypes = (await result.ReadAsync<AssetEventType>()).ToList(),
AssetDependencyTypes = (await result.ReadAsync<AssetDependencyType>()).ToList(),
AssetCustodyEventTypes = (await result.ReadAsync<AssetCustodyEventType>()).ToList(),
CustodyRoles = (await result.ReadAsync<CustodyRole>()).ToList()
};
}
public async Task<IReadOnlyList<StoryAsset>> ListAssetsAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<StoryAsset>("dbo.StoryAsset_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<StoryAsset?> GetAssetAsync(int storyAssetId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<StoryAsset>("dbo.StoryAsset_Get", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure);
}
public async Task<int> SaveAssetAsync(StoryAsset asset)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.StoryAsset_Save",
new
{
asset.StoryAssetID,
asset.ProjectID,
asset.AssetName,
asset.AssetKindID,
asset.Description,
asset.Importance,
asset.CurrentStateID,
asset.CurrentLocationID,
asset.IsResolved,
asset.ShowInQuickAddBar
},
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<IReadOnlyList<AssetEvent>> ListEventsBySceneAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<AssetEvent>("dbo.AssetEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<AssetEvent>> ListEventsByAssetAsync(int storyAssetId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<AssetEvent>("dbo.AssetEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveEventAsync(AssetEvent assetEvent)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<AssetDependency>> ListDependenciesByAssetAsync(int storyAssetId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<AssetDependency>("dbo.AssetDependency_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveDependencyAsync(AssetDependency dependency)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<AssetCustodyEvent>> ListCustodyBySceneAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<AssetCustodyEvent>("dbo.AssetCustodyEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return await WithValidCustodianSummariesAsync(connection, rows.ToList());
}
public async Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyByAssetAsync(int storyAssetId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<AssetCustodyEvent>("dbo.AssetCustodyEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure);
return await WithValidCustodianSummariesAsync(connection, rows.ToList());
}
public async Task<IReadOnlyList<AssetCustodyCharacter>> ListCustodyCharactersByProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<AssetCustodyCharacter>(
"""
SELECT acec.AssetCustodyEventID,
ace.StoryAssetID,
ace.SceneID,
acec.CharacterID,
c.CharacterName,
acec.CharacterNameText,
cr.RoleName,
ace.UpdatedDate
FROM dbo.AssetCustodyEventCharacters acec
INNER JOIN dbo.AssetCustodyEvents ace ON ace.AssetCustodyEventID = acec.AssetCustodyEventID
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID
LEFT JOIN dbo.Characters c ON c.CharacterID = acec.CharacterID
LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = acec.CustodyRoleID
WHERE sa.ProjectID = @ProjectID
AND sa.IsArchived = 0
AND acec.CharacterID IS NOT NULL
AND NULLIF(LTRIM(RTRIM(c.CharacterName)), N'') IS NOT NULL;
""",
new { ProjectID = projectId });
return rows.ToList();
}
private static async Task<IReadOnlyList<AssetCustodyEvent>> WithValidCustodianSummariesAsync(IDbConnection connection, List<AssetCustodyEvent> rows)
{
if (rows.Count == 0)
{
return rows;
}
var eventIds = rows.Select(x => x.AssetCustodyEventID).Distinct().ToList();
var validCustodians = await connection.QueryAsync<AssetCustodySummaryRow>(
"""
SELECT acec.AssetCustodyEventID,
c.CharacterName,
cr.RoleName
FROM dbo.AssetCustodyEventCharacters acec
INNER JOIN dbo.Characters c ON c.CharacterID = acec.CharacterID
LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = acec.CustodyRoleID
WHERE acec.AssetCustodyEventID IN @AssetCustodyEventIDs
AND acec.CharacterID IS NOT NULL
AND NULLIF(LTRIM(RTRIM(c.CharacterName)), N'') IS NOT NULL;
""",
new { AssetCustodyEventIDs = eventIds });
var summaries = validCustodians
.GroupBy(x => x.AssetCustodyEventID)
.ToDictionary(
x => x.Key,
x => string.Join(", ", x
.Select(row => string.IsNullOrWhiteSpace(row.RoleName)
? row.CharacterName
: $"{row.CharacterName} ({row.RoleName})")
.Where(label => !string.IsNullOrWhiteSpace(label))
.Distinct(StringComparer.OrdinalIgnoreCase)));
foreach (var row in rows)
{
row.CustodianSummary = summaries.GetValueOrDefault(row.AssetCustodyEventID);
}
return rows.Where(x => !string.IsNullOrWhiteSpace(x.CustodianSummary)).ToList();
}
private sealed class AssetCustodySummaryRow
{
public int AssetCustodyEventID { get; set; }
public string CharacterName { get; set; } = string.Empty;
public string? RoleName { get; set; }
}
public async Task<int> SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable<int> custodianCharacterIds, int custodyRoleId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<StoryAsset> Assets, IReadOnlyList<AssetEvent> 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<StoryAsset>()).ToList();
var events = (await result.ReadAsync<AssetEvent>()).ToList();
return (assets, events);
}
public async Task<IReadOnlyList<AssetDependencyIssue>> CheckDependencyIssuesAsync(int storyAssetId, int sceneId, int? toStateId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<AssetDependencyIssue>(
"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<CharacterLookupData> 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<CharacterRoleInSceneType>()).ToList(),
PresenceTypes = (await result.ReadAsync<PresenceType>()).ToList(),
AttributeTypes = (await result.ReadAsync<CharacterAttributeType>()).ToList(),
KnowledgeStates = (await result.ReadAsync<KnowledgeState>()).ToList(),
RelationshipTypes = (await result.ReadAsync<RelationshipType>()).ToList(),
RelationshipStates = (await result.ReadAsync<RelationshipState>()).ToList(),
RelationshipCategories = (await result.ReadAsync<RelationshipCategory>()).ToList()
};
}
public async Task<IReadOnlyList<Character>> ListCharactersAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<Character>("dbo.Character_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<Character?> GetCharacterAsync(int characterId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<Character>("dbo.Character_Get", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
}
public async Task<int> SaveCharacterAsync(Character character)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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.CharacterImportance,
character.ShowInQuickAddBar,
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<IReadOnlyList<SceneCharacter>> ListSceneCharactersAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<SceneCharacter>("dbo.SceneCharacter_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveSceneCharacterAsync(SceneCharacter sceneCharacter)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<CharacterAttributeEvent>> ListAttributeEventsByCharacterAsync(int characterId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<CharacterAttributeEvent>("dbo.CharacterAttributeEvent_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveAttributeEventAsync(CharacterAttributeEvent attributeEvent)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.CharacterAttributeEvent_Save",
new
{
attributeEvent.CharacterAttributeEventID,
attributeEvent.CharacterID,
attributeEvent.CharacterAttributeTypeID,
attributeEvent.SceneID,
attributeEvent.AttributeValue,
attributeEvent.Description
},
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<CharacterKnowledgeItem>> ListKnowledgeBySceneAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<CharacterKnowledgeItem>("dbo.CharacterKnowledge_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<CharacterKnowledgeItem>> ListKnowledgeByCharacterAsync(int characterId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<CharacterKnowledgeItem>("dbo.CharacterKnowledge_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveKnowledgeAsync(CharacterKnowledgeItem knowledge)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<CharacterRelationship>> ListRelationshipsByProjectAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<CharacterRelationship>("dbo.Relationship_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<CharacterRelationship>> ListRelationshipsByCharacterAsync(int characterId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<CharacterRelationship>("dbo.Relationship_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<CharacterRelationship?> GetRelationshipAsync(int characterRelationshipId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<CharacterRelationship>(
"dbo.Relationship_Get",
new { CharacterRelationshipID = characterRelationshipId },
commandType: CommandType.StoredProcedure);
}
public async Task<int> SaveRelationshipAsync(CharacterRelationship relationship)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"dbo.Relationship_Save",
new
{
relationship.CharacterRelationshipID,
relationship.ProjectID,
relationship.CharacterAID,
relationship.CharacterBID,
relationship.RelationshipTypeID,
relationship.IsPermanent,
relationship.StartSceneID,
relationship.EndSceneID,
relationship.IsKnownToReader,
relationship.Notes,
relationship.IsInitialRelationship,
relationship.InitialBookID,
relationship.ReaderInitiallyKnows,
relationship.InitialRelationshipStateID,
relationship.InitialIntensity,
relationship.IsReciprocal
},
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<CharacterRelationship>> ListInitialRelationshipsByCharacterAsync(int characterId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<CharacterRelationship>("dbo.Relationship_ListInitialByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
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<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsBySceneAsync(int sceneId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<RelationshipEvent>("dbo.RelationshipEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<RelationshipEvent>> ListRelationshipEventsByCharacterAsync(int characterId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<RelationshipEvent>("dbo.RelationshipEvent_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> SaveRelationshipEventAsync(RelationshipEvent relationshipEvent)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<Character> Characters, IReadOnlyList<SceneCharacter> 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<Character>()).ToList();
var appearances = (await result.ReadAsync<SceneCharacter>()).ToList();
return (characters, appearances);
}
public async Task<RelationshipMapData> GetRelationshipMapAsync(int projectId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync("dbo.RelationshipMap_GetByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure);
return new RelationshipMapData
{
Characters = (await result.ReadAsync<Character>()).ToList(),
Books = (await result.ReadAsync<Book>()).ToList(),
Chapters = (await result.ReadAsync<Chapter>()).ToList(),
Scenes = (await result.ReadAsync<Scene>()).ToList(),
Relationships = (await result.ReadAsync<CharacterRelationship>()).ToList(),
RelationshipEvents = (await result.ReadAsync<RelationshipEvent>()).ToList(),
RelationshipCategories = (await result.ReadAsync<RelationshipCategory>()).ToList()
};
}
}