PlotDirector/PlotLine/Data/WordCompanionRepository.cs
2026-06-14 10:04:41 +01:00

237 lines
11 KiB
C#

using System.Data;
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public interface IWordCompanionRepository
{
Task<IReadOnlyList<WordCompanionProjectDto>> ListProjectsAsync(int userId);
Task<IReadOnlyList<WordCompanionBookDto>> ListBooksAsync(int projectId, int userId);
Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId, int userId);
Task<IReadOnlyList<WordCompanionChapterDto>> ListChaptersAsync(int bookId, int userId);
Task<WordCompanionResolveSceneResponse> ResolveSceneAsync(int bookId, int userId, string chapterTitle, string sceneTitle);
Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId, int userId);
Task<WordCompanionCreateChapterResponse?> CreateChapterAsync(int bookId, int userId, WordCompanionCreateChapterRequest request);
Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, int userId, WordCompanionCreateSceneRequest request);
Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request);
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request);
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, int userId, WordCompanionUpdateSceneLinksRequest request);
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, int userId, int actualWords, DateTime? lastWorkedOn);
}
public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFactory) : IWordCompanionRepository
{
public async Task<IReadOnlyList<WordCompanionProjectDto>> ListProjectsAsync(int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WordCompanionProjectDto>(
"dbo.WordCompanion_Project_List",
new { UserID = userId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<WordCompanionBookDto>> ListBooksAsync(int projectId, int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WordCompanionBookDto>(
"dbo.WordCompanion_Book_ListByProject",
new { ProjectID = projectId, UserID = userId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId, int userId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync(
"dbo.WordCompanion_Book_Structure",
new { BookID = bookId, UserID = userId },
commandType: CommandType.StoredProcedure);
var header = await result.ReadSingleOrDefaultAsync<WordCompanionBookStructureResponse>();
if (header is null)
{
return null;
}
var chapters = (await result.ReadAsync<WordCompanionChapterStructureDto>()).ToList();
var scenes = (await result.ReadAsync<WordCompanionSceneStructureRow>()).ToList();
var scenesByChapter = scenes.GroupBy(x => x.ChapterId).ToDictionary(x => x.Key, x => x.Select(scene => new WordCompanionSceneStructureDto
{
SceneId = scene.SceneId,
Title = scene.Title,
SortOrder = scene.SortOrder,
RevisionStatus = scene.RevisionStatus,
EstimatedWords = scene.EstimatedWords,
ActualWords = scene.ActualWords
}).ToList());
foreach (var chapter in chapters)
{
chapter.Scenes = scenesByChapter.TryGetValue(chapter.ChapterId, out var chapterScenes) ? chapterScenes : [];
}
return new WordCompanionBookStructureResponse { BookId = header.BookId, Chapters = chapters };
}
public async Task<IReadOnlyList<WordCompanionChapterDto>> ListChaptersAsync(int bookId, int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WordCompanionChapterDto>(
"dbo.WordCompanion_Chapter_List",
new { BookID = bookId, UserID = userId },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<WordCompanionResolveSceneResponse> ResolveSceneAsync(int bookId, int userId, string chapterTitle, string sceneTitle)
{
using var connection = connectionFactory.CreateConnection();
var response = await connection.QuerySingleAsync<WordCompanionResolveSceneResponse>(
"dbo.WordCompanion_Scene_Resolve",
new { BookID = bookId, UserID = userId, ChapterTitle = chapterTitle, SceneTitle = sceneTitle },
commandType: CommandType.StoredProcedure);
return response;
}
public async Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId, int userId)
{
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync(
"dbo.WordCompanion_Scene_Companion",
new { SceneID = sceneId, UserID = userId },
commandType: CommandType.StoredProcedure);
var scene = await result.ReadSingleOrDefaultAsync<WordCompanionSceneCompanionResponse>();
if (scene is null)
{
return null;
}
return new WordCompanionSceneCompanionResponse
{
SceneId = scene.SceneId,
ChapterId = scene.ChapterId,
ChapterTitle = scene.ChapterTitle,
SceneTitle = scene.SceneTitle,
ActualWords = scene.ActualWords,
EstimatedWords = scene.EstimatedWords,
WritingBrief = scene.WritingBrief,
Blocked = scene.Blocked,
BlockedReason = scene.BlockedReason,
PrimaryLocationId = scene.PrimaryLocationId,
Characters = (await result.ReadAsync<WordCompanionLinkItemDto>()).ToList(),
Assets = (await result.ReadAsync<WordCompanionLinkItemDto>()).ToList(),
Locations = (await result.ReadAsync<WordCompanionLinkItemDto>()).ToList()
};
}
public async Task<WordCompanionCreateChapterResponse?> CreateChapterAsync(int bookId, int userId, WordCompanionCreateChapterRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionCreateChapterResponse>(
"dbo.WordCompanion_Chapter_Create",
new { BookID = bookId, UserID = userId, request.Title, request.SortOrder },
commandType: CommandType.StoredProcedure);
}
public async Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, int userId, WordCompanionCreateSceneRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionCreateSceneResponse>(
"dbo.WordCompanion_Scene_Create",
new { BookID = bookId, ChapterID = chapterId, UserID = userId, request.SceneTitle },
commandType: CommandType.StoredProcedure);
}
public async Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionSyncSceneResponse>(
"dbo.WordCompanion_Scene_Sync",
new
{
BookID = bookId,
UserID = userId,
request.ChapterTitle,
request.ChapterSortOrder,
request.SceneTitle,
request.SceneSortOrder,
request.ActualWords,
request.DocumentId,
request.ContentHash
},
commandType: CommandType.StoredProcedure);
}
public async Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request)
{
var chapters = request.Chapters.Select(chapter => new
{
chapter.Title,
chapter.SortOrder,
chapter.WordCount,
Scenes = chapter.Scenes.Select(scene => new
{
scene.Title,
scene.SortOrder,
scene.WordCount,
scene.ContentHash
})
});
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionSyncManuscriptResponse>(
"dbo.WordCompanion_Manuscript_Sync",
new
{
BookID = bookId,
UserID = userId,
request.DocumentId,
StructureJson = System.Text.Json.JsonSerializer.Serialize(chapters)
},
commandType: CommandType.StoredProcedure);
}
public async Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, int userId, WordCompanionUpdateSceneLinksRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionUpdateSceneLinksResponse>(
"dbo.WordCompanion_SceneLinks_Update",
new
{
SceneID = sceneId,
UserID = userId,
CharacterIds = ToCsv(request.CharacterIds),
AssetIds = ToCsv(request.AssetIds),
LocationIds = ToCsv(request.LocationIds),
request.PrimaryLocationId
},
commandType: CommandType.StoredProcedure);
}
public async Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, int userId, int actualWords, DateTime? lastWorkedOn)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionUpdateWordCountResponse>(
"dbo.WordCompanion_Scene_UpdateWordCount",
new { SceneID = sceneId, UserID = userId, ActualWords = actualWords, LastWorkedOn = lastWorkedOn },
commandType: CommandType.StoredProcedure);
}
private static string ToCsv(IEnumerable<int> ids) => string.Join(',', ids.Where(id => id > 0).Distinct());
private sealed class WordCompanionSceneStructureRow
{
public int ChapterId { get; set; }
public int SceneId { get; set; }
public string Title { get; set; } = string.Empty;
public int SortOrder { get; set; }
public string RevisionStatus { get; set; } = string.Empty;
public int? EstimatedWords { get; set; }
public int? ActualWords { get; set; }
}
}