Replace chapter setting with multi-select locations; add chapter character summary

Chapters now carry many Locations (new Tags-style entity with cross-referencing)
instead of a single free-text Setting field, with a Locations tab on the novel
for browsing them and seeing every chapter set at each one. Also surfaces the
distinct characters appearing in a chapter's beats, linked, under the beat/word
count on the outline tab.
This commit is contained in:
James Wampler
2026-08-18 11:36:13 -07:00
parent 4313c8f206
commit c620ddd626
27 changed files with 2380 additions and 44 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,90 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Novelly.Api.Data.Migrations
{
/// <inheritdoc />
public partial class RenameSettingToLocations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Setting",
table: "Chapters");
migrationBuilder.CreateTable(
name: "Locations",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
NovelId = table.Column<Guid>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 120, nullable: false),
CreatedAt = table.Column<long>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Locations", x => x.Id);
table.ForeignKey(
name: "FK_Locations_Novels_NovelId",
column: x => x.NovelId,
principalTable: "Novels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ChapterLocations",
columns: table => new
{
ChaptersId = table.Column<Guid>(type: "TEXT", nullable: false),
LocationsId = table.Column<Guid>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChapterLocations", x => new { x.ChaptersId, x.LocationsId });
table.ForeignKey(
name: "FK_ChapterLocations_Chapters_ChaptersId",
column: x => x.ChaptersId,
principalTable: "Chapters",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ChapterLocations_Locations_LocationsId",
column: x => x.LocationsId,
principalTable: "Locations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ChapterLocations_LocationsId",
table: "ChapterLocations",
column: "LocationsId");
migrationBuilder.CreateIndex(
name: "IX_Locations_NovelId_Name",
table: "Locations",
columns: new[] { "NovelId", "Name" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ChapterLocations");
migrationBuilder.DropTable(
name: "Locations");
migrationBuilder.AddColumn<string>(
name: "Setting",
table: "Chapters",
type: "TEXT",
nullable: true);
}
}
}
@@ -62,6 +62,21 @@ namespace Novelly.Api.Data.Migrations
b.ToTable("BeatTags", (string)null);
});
modelBuilder.Entity("ChapterLocation", b =>
{
b.Property<Guid>("ChaptersId")
.HasColumnType("TEXT");
b.Property<Guid>("LocationsId")
.HasColumnType("TEXT");
b.HasKey("ChaptersId", "LocationsId");
b.HasIndex("LocationsId");
b.ToTable("ChapterLocations", (string)null);
});
modelBuilder.Entity("ChapterTag", b =>
{
b.Property<Guid>("ChaptersId")
@@ -273,9 +288,6 @@ namespace Novelly.Api.Data.Migrations
b.Property<string>("Prose")
.HasColumnType("TEXT");
b.Property<string>("Setting")
.HasColumnType("TEXT");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
@@ -620,6 +632,31 @@ namespace Novelly.Api.Data.Migrations
b.ToTable("ImportJobs");
});
modelBuilder.Entity("Novelly.Api.Locations.Location", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<long>("CreatedAt")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
.HasColumnType("TEXT");
b.Property<Guid>("NovelId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("NovelId", "Name")
.IsUnique();
b.ToTable("Locations");
});
modelBuilder.Entity("Novelly.Api.Novels.Novel", b =>
{
b.Property<Guid>("Id")
@@ -901,6 +938,21 @@ namespace Novelly.Api.Data.Migrations
.IsRequired();
});
modelBuilder.Entity("ChapterLocation", b =>
{
b.HasOne("Novelly.Api.Chapters.Chapter", null)
.WithMany()
.HasForeignKey("ChaptersId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Novelly.Api.Locations.Location", null)
.WithMany()
.HasForeignKey("LocationsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("ChapterTag", b =>
{
b.HasOne("Novelly.Api.Chapters.Chapter", null)
@@ -1064,6 +1116,17 @@ namespace Novelly.Api.Data.Migrations
b.Navigation("RelatedCharacter");
});
modelBuilder.Entity("Novelly.Api.Locations.Location", b =>
{
b.HasOne("Novelly.Api.Novels.Novel", "Novel")
.WithMany()
.HasForeignKey("NovelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Novel");
});
modelBuilder.Entity("Novelly.Api.Novels.Novel", b =>
{
b.HasOne("Novelly.Api.Users.NovellyUser", "Owner")
+3
View File
@@ -7,6 +7,7 @@ using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Genres;
using Novelly.Api.Imports;
using Novelly.Api.Locations;
using Novelly.Api.Novels;
using Novelly.Api.Questions;
using Novelly.Api.Tags;
@@ -24,6 +25,7 @@ public class NovelDbContext(DbContextOptions<NovelDbContext> options) : Identity
public DbSet<CharacterArcStage> CharacterArcStages => Set<CharacterArcStage>();
public DbSet<Beat> Beats => Set<Beat>();
public DbSet<Tag> Tags => Set<Tag>();
public DbSet<Location> Locations => Set<Location>();
public DbSet<Chapter> Chapters => Set<Chapter>();
public DbSet<OpenQuestion> OpenQuestions => Set<OpenQuestion>();
public DbSet<AgentConversation> Conversations => Set<AgentConversation>();
@@ -51,6 +53,7 @@ public interface INovelDbContext
DbSet<CharacterArcStage> CharacterArcStages { get; }
DbSet<Beat> Beats { get; }
DbSet<Tag> Tags { get; }
DbSet<Location> Locations { get; }
DbSet<Chapter> Chapters { get; }
DbSet<OpenQuestion> OpenQuestions { get; }
DbSet<AgentConversation> Conversations { get; }