PlotDirector/PlotLine/Data/ImportRepository.cs
2026-06-01 20:47:20 +01:00

198 lines
7.8 KiB
C#

using System.Data;
using System.Text;
using Dapper;
using PlotLine.Models;
namespace PlotLine.Data;
public interface IImportRepository
{
Task<bool> ProjectTitleExistsAsync(string projectTitle);
Task<ImportResult> ImportAsync(PlotLineImportPackage package);
}
public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : IImportRepository
{
private const string ImportMarker = "Imported via JSON Import Phase 1A";
public async Task<bool> ProjectTitleExistsAsync(string projectTitle)
{
using var connection = connectionFactory.CreateConnection();
var count = await connection.ExecuteScalarAsync<int>(
"""
SELECT COUNT(1)
FROM dbo.Projects
WHERE IsArchived = 0 AND ProjectName = @ProjectName;
""",
new { ProjectName = projectTitle });
return count > 0;
}
public async Task<ImportResult> ImportAsync(PlotLineImportPackage package)
{
using var connection = connectionFactory.CreateConnection();
connection.Open();
using var transaction = connection.BeginTransaction();
try
{
var lookupIds = await GetLookupIdsAsync(connection, transaction);
var projectId = await connection.QuerySingleAsync<int>(
"dbo.Project_Save",
new
{
ProjectID = 0,
ProjectName = Clean(package.Project.Title),
Description = CombineText(package.Project.Description, package.Project.Notes, ImportMarker)
},
transaction,
commandType: CommandType.StoredProcedure);
foreach (var book in package.Books.OrderBy(x => x.SeriesOrder))
{
var bookId = await connection.QuerySingleAsync<int>(
"dbo.Book_Save",
new
{
BookID = 0,
ProjectID = projectId,
BookTitle = Clean(FormatBookTitle(book)),
BookNumber = book.SeriesOrder,
Description = CombineText(
book.Description,
book.DependsOnPreviousBook ? "Depends on previous book." : null,
book.Notes,
ImportMarker)
},
transaction,
commandType: CommandType.StoredProcedure);
foreach (var chapter in book.Chapters.OrderBy(x => x.Order))
{
var chapterId = await connection.QuerySingleAsync<int>(
"dbo.Chapter_Save",
new
{
ChapterID = 0,
BookID = bookId,
chapter.ChapterNumber,
ChapterTitle = Clean(chapter.Title),
Summary = CombineText(chapter.Summary, chapter.Notes, ImportMarker),
RevisionStatusID = lookupIds.RevisionStatusID
},
transaction,
commandType: CommandType.StoredProcedure);
foreach (var scene in chapter.Scenes.OrderBy(x => x.Order))
{
await connection.QuerySingleAsync<int>(
"dbo.Scene_Save",
new
{
SceneID = 0,
ChapterID = chapterId,
scene.SceneNumber,
SceneTitle = Clean(scene.Title),
Summary = CombineText(
scene.Summary,
MetadataLine("POV", scene.PovCharacterName),
MetadataLine("Location", scene.LocationName),
scene.Notes,
ImportMarker),
TimeModeID = lookupIds.TimeModeID,
StartDateTime = (DateTime?)null,
EndDateTime = (DateTime?)null,
DurationAmount = (decimal?)null,
DurationUnitID = (int?)null,
RelativeTimeText = Truncate(scene.DateTimeText, 200),
TimeConfidenceID = lookupIds.TimeConfidenceID,
ScenePurposeNotes = CombineText(scene.PurposeText),
SceneOutcomeNotes = CombineText(scene.OutcomeText),
RevisionStatusID = lookupIds.RevisionStatusID
},
transaction,
commandType: CommandType.StoredProcedure);
}
}
}
transaction.Commit();
return new ImportResult
{
Succeeded = true,
ProjectID = projectId,
Message = "Import complete."
};
}
catch
{
transaction.Rollback();
throw;
}
}
private static async Task<ImportLookupIds> GetLookupIdsAsync(IDbConnection connection, IDbTransaction transaction)
{
var revisionStatusId = await connection.QuerySingleOrDefaultAsync<int?>(
"SELECT TOP (1) RevisionStatusID FROM dbo.RevisionStatuses WHERE StatusName = N'Planned' ORDER BY SortOrder;",
transaction: transaction);
var timeModeId = await connection.QuerySingleOrDefaultAsync<int?>(
"SELECT TOP (1) TimeModeID FROM dbo.TimeModes WHERE TimeModeName = N'Relative' ORDER BY SortOrder;",
transaction: transaction);
var timeConfidenceId = await connection.QuerySingleOrDefaultAsync<int?>(
"SELECT TOP (1) TimeConfidenceID FROM dbo.TimeConfidences WHERE TimeConfidenceName = N'Unknown' ORDER BY SortOrder;",
transaction: transaction);
if (!revisionStatusId.HasValue || !timeModeId.HasValue || !timeConfidenceId.HasValue)
{
throw new InvalidOperationException("Import lookup values are missing. Run the core database script before importing.");
}
return new ImportLookupIds(revisionStatusId.Value, timeModeId.Value, timeConfidenceId.Value);
}
private static string FormatBookTitle(ImportBookDto book)
=> string.IsNullOrWhiteSpace(book.Subtitle) ? book.Title : $"{book.Title}: {book.Subtitle}";
private static string Clean(string? value) => (value ?? string.Empty).Trim();
private static string? MetadataLine(string label, string? value)
=> string.IsNullOrWhiteSpace(value) ? null : $"{label}: {value.Trim()}";
private static string? Truncate(string? value, int maxLength)
{
var clean = Clean(value);
return clean.Length == 0 ? null : clean[..Math.Min(clean.Length, maxLength)];
}
private static string? CombineText(params string?[] values)
{
var parts = values
.Select(Clean)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (parts.Count == 0)
{
return null;
}
var builder = new StringBuilder();
foreach (var part in parts)
{
if (builder.Length > 0)
{
builder.AppendLine().AppendLine();
}
builder.Append(part);
}
return builder.ToString();
}
private sealed record ImportLookupIds(int RevisionStatusID, int TimeModeID, int TimeConfidenceID);
}