Remove chapter-level POV character
Character assignment now lives on beats only, per batch-assign. Drops Chapter.PovCharacterId (entity, contracts, service, agent/import/MCP tools, web UI) and the backing column via migration.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Novelly.Api.Beats;
|
||||
using Novelly.Api.Characters;
|
||||
using Novelly.Api.Common;
|
||||
using Novelly.Api.Projects;
|
||||
using Novelly.Api.Tags;
|
||||
@@ -18,9 +19,6 @@ public class Chapter
|
||||
|
||||
public string? Summary { get; set; }
|
||||
|
||||
public Guid? PovCharacterId { get; set; }
|
||||
public Character? PovCharacter { get; set; }
|
||||
|
||||
public string? Setting { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
|
||||
@@ -38,3 +36,13 @@ public class Chapter
|
||||
|
||||
public List<Tag> Tags { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ChapterEntityTypeConfiguration : IEntityTypeConfiguration<Chapter>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Chapter> entity)
|
||||
{
|
||||
entity.Property(c => c.Title).IsRequired().HasMaxLength(300);
|
||||
entity.Property(c => c.Status).HasConversion<string>().HasMaxLength(32);
|
||||
entity.HasIndex(c => new { c.ProjectId, c.Number });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ public record ChapterSummaryResponse(
|
||||
int Number,
|
||||
string Title,
|
||||
string? Summary,
|
||||
Guid? PovCharacterId,
|
||||
string? PovCharacterName,
|
||||
string? Setting,
|
||||
DraftStatus Status,
|
||||
int? TargetWordCount,
|
||||
@@ -27,8 +25,6 @@ public record ChapterResponse(
|
||||
int Number,
|
||||
string Title,
|
||||
string? Summary,
|
||||
Guid? PovCharacterId,
|
||||
string? PovCharacterName,
|
||||
string? Setting,
|
||||
string? Notes,
|
||||
DraftStatus Status,
|
||||
@@ -43,7 +39,6 @@ public record CreateChapterRequest(
|
||||
string Title,
|
||||
int? Number = null,
|
||||
string? Summary = null,
|
||||
Guid? PovCharacterId = null,
|
||||
string? Setting = null,
|
||||
string? Notes = null,
|
||||
DraftStatus Status = DraftStatus.Planned,
|
||||
@@ -57,11 +52,7 @@ public class CreateChapterRequestValidator : IModelValidator<CreateChapterReques
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(model.Title))
|
||||
result.AddError("Title", "'Title' must not be empty.");
|
||||
else if (model.Title.Length > 200)
|
||||
result.AddError("Title", "'Title' must be 200 characters or fewer.");
|
||||
|
||||
result.AddRequiredTextErrors("Title", "Title", model.Title, 200);
|
||||
ChapterValidation.OptionalFields(model.Number, model.Summary, model.Setting, model.Notes, model.TargetWordCount, model.Prose, model.Tags, result);
|
||||
|
||||
return result;
|
||||
@@ -72,7 +63,6 @@ public record UpdateChapterRequest(
|
||||
string? Title = null,
|
||||
int? Number = null,
|
||||
string? Summary = null,
|
||||
Guid? PovCharacterId = null,
|
||||
string? Setting = null,
|
||||
string? Notes = null,
|
||||
DraftStatus? Status = null,
|
||||
@@ -86,14 +76,7 @@ public class UpdateChapterRequestValidator : IModelValidator<UpdateChapterReques
|
||||
{
|
||||
var result = new ValidationResult();
|
||||
|
||||
if (model.Title is not null)
|
||||
{
|
||||
if (model.Title.Length == 0)
|
||||
result.AddError("Title", "'Title' can not be cleared — a chapter always needs one.");
|
||||
else if (model.Title.Length > 200)
|
||||
result.AddError("Title", "'Title' must be 200 characters or fewer.");
|
||||
}
|
||||
|
||||
result.AddUnclearableTextErrors("Title", "Title", model.Title, "a chapter", 200);
|
||||
ChapterValidation.OptionalFields(model.Number, model.Summary, model.Setting, model.Notes, model.TargetWordCount, model.Prose, model.Tags, result);
|
||||
|
||||
return result;
|
||||
@@ -133,7 +116,7 @@ public static class ChapterMapping
|
||||
{
|
||||
public static ChapterResponse ToResponse(this Chapter c) => new(
|
||||
c.Id, c.ProjectId, c.Number, c.Title, c.Summary,
|
||||
c.PovCharacterId, c.PovCharacter?.Name, c.Setting, c.Notes,
|
||||
c.Setting, c.Notes,
|
||||
c.Status, c.TargetWordCount,
|
||||
[.. c.Beats.OrderBy(b => b.SortOrder).Select(b => b.ToResponse())],
|
||||
c.Prose, c.WordCount,
|
||||
@@ -142,7 +125,7 @@ public static class ChapterMapping
|
||||
|
||||
public static ChapterSummaryResponse ToSummaryResponse(this Chapter c) => new(
|
||||
c.Id, c.ProjectId, c.Number, c.Title, c.Summary,
|
||||
c.PovCharacterId, c.PovCharacter?.Name, c.Setting, c.Status, c.TargetWordCount,
|
||||
c.Setting, c.Status, c.TargetWordCount,
|
||||
c.Beats.Count, c.WordCount,
|
||||
[.. c.Tags.OrderBy(t => t.Name).Select(t => t.ToResponse())],
|
||||
c.UpdatedAt);
|
||||
|
||||
@@ -20,7 +20,6 @@ public class ChapterService(
|
||||
logger.LogInformation("Listing chapters for project {ProjectId}", projectId);
|
||||
|
||||
return await db.Chapters
|
||||
.Include(c => c.PovCharacter)
|
||||
.Include(c => c.Beats)
|
||||
.Include(c => c.Tags)
|
||||
.Where(c => c.ProjectId == projectId)
|
||||
@@ -40,13 +39,13 @@ public class ChapterService(
|
||||
{
|
||||
Guard.Default(projectId, nameof(projectId));
|
||||
Guard.Null(request, nameof(request));
|
||||
createValidator.Validate(request).ThrowIfInvalid();
|
||||
createValidator.Validate(request).ThrowIfInvalid(logger);
|
||||
|
||||
logger.LogInformation("Creating chapter {Title} for project {ProjectId}", request.Title, projectId);
|
||||
|
||||
if (!await db.Projects.AnyAsync(p => p.Id == projectId, ct))
|
||||
{
|
||||
logger.LogInformation("Rejected chapter creation: project {ProjectId} not found", projectId);
|
||||
logger.LogWarning("Rejected chapter creation: project {ProjectId} not found", projectId);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -56,7 +55,6 @@ public class ChapterService(
|
||||
Title = request.Title,
|
||||
Number = request.Number ?? await NextChapterNumberAsync(projectId, ct),
|
||||
Summary = request.Summary,
|
||||
PovCharacterId = request.PovCharacterId,
|
||||
Setting = request.Setting,
|
||||
Notes = request.Notes,
|
||||
Status = request.Status,
|
||||
@@ -80,7 +78,7 @@ public class ChapterService(
|
||||
{
|
||||
Guard.Default(id, nameof(id));
|
||||
Guard.Null(request, nameof(request));
|
||||
updateValidator.Validate(request).ThrowIfInvalid();
|
||||
updateValidator.Validate(request).ThrowIfInvalid(logger);
|
||||
|
||||
logger.LogInformation("Updating chapter {ChapterId}", id);
|
||||
|
||||
@@ -93,7 +91,6 @@ public class ChapterService(
|
||||
chapter.Title = Patch.Apply(chapter.Title, request.Title) ?? chapter.Title;
|
||||
chapter.Number = request.Number ?? chapter.Number;
|
||||
chapter.Summary = Patch.Apply(chapter.Summary, request.Summary);
|
||||
chapter.PovCharacterId = request.PovCharacterId ?? chapter.PovCharacterId;
|
||||
chapter.Setting = Patch.Apply(chapter.Setting, request.Setting);
|
||||
chapter.Notes = Patch.Apply(chapter.Notes, request.Notes);
|
||||
chapter.Status = request.Status ?? chapter.Status;
|
||||
@@ -151,7 +148,6 @@ public class ChapterService(
|
||||
logger.LogDebug("Finding chapter {ChapterId}", id);
|
||||
|
||||
var chapter = await db.Chapters
|
||||
.Include(c => c.PovCharacter)
|
||||
.Include(c => c.Beats).ThenInclude(b => b.Characters)
|
||||
.Include(c => c.Beats).ThenInclude(b => b.Tags)
|
||||
.Include(c => c.Tags)
|
||||
@@ -159,13 +155,11 @@ public class ChapterService(
|
||||
|
||||
if (chapter is null)
|
||||
{
|
||||
logger.LogInformation("Chapter {ChapterId} not found", id);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogDebug("Found chapter {ChapterId}", id);
|
||||
logger.LogWarning("Chapter {ChapterId} not found", id);
|
||||
return chapter;
|
||||
}
|
||||
|
||||
logger.LogDebug("Found chapter {ChapterId}", id);
|
||||
return chapter;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user