PlotDirector/PlotLine/Data/WordCompanionRepository.cs

465 lines
22 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<WordCompanionCreateBookResponse?> CreateBookAsync(int projectId, int userId, WordCompanionCreateBookRequest request);
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);
Task<WordCompanionUpdateWordCountResponse?> UpdateRuntimeSceneWordCountAsync(int userId, WordCompanionRuntimeSceneWordCountRequest request);
Task<WordCompanionRuntimeCurrentSceneResponse?> ValidateRuntimeCurrentSceneAsync(int userId, WordCompanionRuntimeCurrentSceneRequest request);
Task<IReadOnlyList<WordCompanionRuntimeCharacterAliasDto>> ListRuntimeCharacterAliasesAsync(int userId, int projectId, Guid documentGuid);
Task<WordCompanionRuntimeCharacterSuggestionsResponse?> CreateRuntimeCharacterSuggestionsAsync(int userId, WordCompanionRuntimeCharacterSuggestionsRequest request);
Task<IReadOnlyList<WordCompanionRuntimeAssetAliasDto>> ListRuntimeAssetAliasesAsync(int userId, int projectId, Guid documentGuid);
Task<WordCompanionRuntimeAssetSuggestionsResponse?> CreateRuntimeAssetSuggestionsAsync(int userId, WordCompanionRuntimeAssetSuggestionsRequest request);
Task<IReadOnlyList<WordCompanionRuntimeLocationAliasDto>> ListRuntimeLocationAliasesAsync(int userId, int projectId, Guid documentGuid);
Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request);
Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(int userId, WordCompanionManuscriptImportRequest request);
Task<bool> HasProjectAccessAsync(int projectId, int userId);
Task<WordCompanionCreateDiscoveredCharactersResponse?> CreateDiscoveredCharactersAsync(int userId, WordCompanionCreateDiscoveredCharactersRequest request);
}
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<WordCompanionCreateBookResponse?> CreateBookAsync(int projectId, int userId, WordCompanionCreateBookRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionCreateBookResponse>(
"dbo.WordCompanion_Book_Create",
new { ProjectID = projectId, UserID = userId, request.Title },
commandType: CommandType.StoredProcedure);
}
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);
}
public async Task<WordCompanionUpdateWordCountResponse?> UpdateRuntimeSceneWordCountAsync(int userId, WordCompanionRuntimeSceneWordCountRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionUpdateWordCountResponse>(
"dbo.WordCompanion_Runtime_SceneWordCount_Update",
new
{
UserID = userId,
request.DocumentGuid,
request.BookId,
request.ChapterId,
request.SceneId,
request.WordCount
},
commandType: CommandType.StoredProcedure);
}
public async Task<WordCompanionRuntimeCurrentSceneResponse?> ValidateRuntimeCurrentSceneAsync(int userId, WordCompanionRuntimeCurrentSceneRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionRuntimeCurrentSceneResponse>(
"dbo.WordCompanion_Runtime_CurrentScene_Validate",
new
{
UserID = userId,
request.DocumentGuid,
request.ProjectId,
request.BookId,
request.ChapterId,
request.SceneId
},
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<WordCompanionRuntimeCharacterAliasDto>> ListRuntimeCharacterAliasesAsync(int userId, int projectId, Guid documentGuid)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WordCompanionRuntimeCharacterAliasDto>(
"dbo.WordCompanion_Runtime_CharacterAlias_List",
new { UserID = userId, ProjectID = projectId, DocumentGuid = documentGuid },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<WordCompanionRuntimeCharacterSuggestionsResponse?> CreateRuntimeCharacterSuggestionsAsync(int userId, WordCompanionRuntimeCharacterSuggestionsRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionRuntimeCharacterSuggestionsResponse>(
"dbo.WordCompanion_Runtime_CharacterSuggestions_Create",
new
{
UserID = userId,
request.DocumentGuid,
request.SceneId,
CharacterIds = ToCsv(request.CharacterIds)
},
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<WordCompanionRuntimeAssetAliasDto>> ListRuntimeAssetAliasesAsync(int userId, int projectId, Guid documentGuid)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WordCompanionRuntimeAssetAliasDto>(
"dbo.WordCompanion_Runtime_AssetAlias_List",
new { UserID = userId, ProjectID = projectId, DocumentGuid = documentGuid },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<WordCompanionRuntimeAssetSuggestionsResponse?> CreateRuntimeAssetSuggestionsAsync(int userId, WordCompanionRuntimeAssetSuggestionsRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionRuntimeAssetSuggestionsResponse>(
"dbo.WordCompanion_Runtime_AssetSuggestions_Create",
new
{
UserID = userId,
request.DocumentGuid,
request.SceneId,
AssetIds = ToCsv(request.AssetIds)
},
commandType: CommandType.StoredProcedure);
}
public async Task<IReadOnlyList<WordCompanionRuntimeLocationAliasDto>> ListRuntimeLocationAliasesAsync(int userId, int projectId, Guid documentGuid)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WordCompanionRuntimeLocationAliasDto>(
"dbo.WordCompanion_Runtime_LocationAlias_List",
new { UserID = userId, ProjectID = projectId, DocumentGuid = documentGuid },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionManuscriptAnalyseResponse>(
"dbo.WordCompanion_Manuscript_Analyse",
new
{
UserID = userId,
request.ProjectId,
request.BookId,
DocumentGuid = request.DocumentGuid,
request.ChapterCount,
request.SceneCount,
request.WordCount
},
commandType: CommandType.StoredProcedure);
}
public async Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(int userId, WordCompanionManuscriptImportRequest 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
})
});
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync(
"dbo.WordCompanion_Manuscript_Import",
new
{
UserID = userId,
request.ProjectId,
request.BookId,
DocumentGuid = request.DocumentGuid.GetValueOrDefault(),
request.BindingVersion,
request.ReplacePlanned,
StructureJson = System.Text.Json.JsonSerializer.Serialize(chapters)
},
commandType: CommandType.StoredProcedure);
var response = await result.ReadSingleOrDefaultAsync<WordCompanionManuscriptImportResponse>();
if (response is null)
{
return null;
}
var document = await result.ReadSingleOrDefaultAsync<WordCompanionManuscriptDocumentDto>();
return new WordCompanionManuscriptImportResponse
{
Imported = response.Imported,
RequiresReplaceConfirmation = response.RequiresReplaceConfirmation,
Message = response.Message,
ProjectId = response.ProjectId,
BookId = response.BookId,
ChaptersCreated = response.ChaptersCreated,
ScenesCreated = response.ScenesCreated,
ArchivedChapters = response.ArchivedChapters,
ArchivedScenes = response.ArchivedScenes,
ManuscriptDocument = document
};
}
public async Task<bool> HasProjectAccessAsync(int projectId, int userId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<bool>(
"dbo.WordCompanion_Project_HasAccess",
new { ProjectID = projectId, UserID = userId },
commandType: CommandType.StoredProcedure);
}
public async Task<WordCompanionCreateDiscoveredCharactersResponse?> CreateDiscoveredCharactersAsync(int userId, WordCompanionCreateDiscoveredCharactersRequest request)
{
var names = request.CandidateNames
.Select(name => new { Name = name })
.ToList();
using var connection = connectionFactory.CreateConnection();
using var result = await connection.QueryMultipleAsync(
"dbo.WordCompanion_Character_CreateDiscovered",
new
{
UserID = userId,
request.ProjectId,
CandidatesJson = System.Text.Json.JsonSerializer.Serialize(names)
},
commandType: CommandType.StoredProcedure);
var header = await result.ReadSingleOrDefaultAsync<WordCompanionCreateDiscoveredCharactersResponse>();
if (header is null)
{
return null;
}
var characters = (await result.ReadAsync<WordCompanionCreatedCharacterDto>()).ToList();
return new WordCompanionCreateDiscoveredCharactersResponse
{
ProjectId = header.ProjectId,
CreatedCount = header.CreatedCount,
SkippedCount = header.SkippedCount,
Message = header.Message,
Characters = characters
};
}
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; }
}
}