238 lines
9.6 KiB
C#
238 lines
9.6 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, string? importedProjectTitle);
|
|
}
|
|
|
|
public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : IImportRepository
|
|
{
|
|
private const string ImportMarker = "Imported via JSON Import Phase 1B";
|
|
|
|
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, string? importedProjectTitle)
|
|
{
|
|
using var connection = connectionFactory.CreateConnection();
|
|
connection.Open();
|
|
using var transaction = connection.BeginTransaction();
|
|
|
|
try
|
|
{
|
|
var booksCreated = 0;
|
|
var chaptersCreated = 0;
|
|
var scenesCreated = 0;
|
|
var skippedFields = GetSkippedOptionalFields(package);
|
|
var projectName = string.IsNullOrWhiteSpace(importedProjectTitle)
|
|
? Clean(package.Project.Title)
|
|
: Clean(importedProjectTitle);
|
|
var lookupIds = await GetLookupIdsAsync(connection, transaction);
|
|
var projectId = await connection.QuerySingleAsync<int>(
|
|
"dbo.Project_Save",
|
|
new
|
|
{
|
|
ProjectID = 0,
|
|
ProjectName = projectName,
|
|
Description = CombineText(
|
|
package.Project.Description,
|
|
package.Project.Notes,
|
|
ImportMetadata(package))
|
|
},
|
|
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);
|
|
booksCreated++;
|
|
|
|
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);
|
|
chaptersCreated++;
|
|
|
|
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);
|
|
scenesCreated++;
|
|
}
|
|
}
|
|
}
|
|
|
|
transaction.Commit();
|
|
return new ImportResult
|
|
{
|
|
Succeeded = true,
|
|
ProjectID = projectId,
|
|
ProjectName = projectName,
|
|
BooksCreated = booksCreated,
|
|
ChaptersCreated = chaptersCreated,
|
|
ScenesCreated = scenesCreated,
|
|
SkippedOptionalFields = skippedFields,
|
|
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 ImportMetadata(PlotLineImportPackage package)
|
|
{
|
|
var source = string.IsNullOrWhiteSpace(package.Source) ? null : $"Source: {package.Source.Trim()}";
|
|
return CombineText(ImportMarker, $"Imported at {DateTime.UtcNow:yyyy-MM-dd HH:mm} UTC", source) ?? ImportMarker;
|
|
}
|
|
|
|
private static IReadOnlyList<string> GetSkippedOptionalFields(PlotLineImportPackage package)
|
|
{
|
|
var skipped = new List<string>();
|
|
if (package.Books.Any(book => book.Chapters.Any(chapter => chapter.Scenes.Any(scene => !string.IsNullOrWhiteSpace(scene.PovCharacterName)))))
|
|
{
|
|
skipped.Add("POV character names were stored as plain text in scene summaries; Character records were not created.");
|
|
}
|
|
|
|
if (package.Books.Any(book => book.Chapters.Any(chapter => chapter.Scenes.Any(scene => !string.IsNullOrWhiteSpace(scene.LocationName)))))
|
|
{
|
|
skipped.Add("Location names were stored as plain text in scene summaries; Location records were not created.");
|
|
}
|
|
|
|
return skipped;
|
|
}
|
|
|
|
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);
|
|
}
|