Rename Project concept to Novel across the stack

Renames the domain concept from Project to Novel throughout the backend
(entities, DTOs, services, endpoints, ProjectAccessService/Permission,
ProjectId foreign keys), MCP server (tool names and routes), and the
React/Vite frontend (types, hooks, routes, components). Adds a new EF
Core migration (RenameProjectToNovel) using RenameTable/RenameColumn to
preserve existing data instead of dropping/recreating tables. Updates
CLAUDE.md's structure section to reference Novels/ instead of Projects/.
This commit is contained in:
James Wampler
2026-08-17 23:03:09 -07:00
parent 0ab4f568b5
commit 4313c8f206
95 changed files with 3192 additions and 1660 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,363 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Novelly.Api.Data.Migrations
{
/// <inheritdoc />
public partial class RenameProjectToNovel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Chapters_Projects_ProjectId",
table: "Chapters");
migrationBuilder.DropForeignKey(
name: "FK_Characters_Projects_ProjectId",
table: "Characters");
migrationBuilder.DropForeignKey(
name: "FK_Conversations_Projects_ProjectId",
table: "Conversations");
migrationBuilder.DropForeignKey(
name: "FK_OpenQuestions_Projects_ProjectId",
table: "OpenQuestions");
migrationBuilder.DropForeignKey(
name: "FK_Tags_Projects_ProjectId",
table: "Tags");
migrationBuilder.DropForeignKey(
name: "FK_ProjectMembers_Projects_ProjectId",
table: "ProjectMembers");
migrationBuilder.RenameTable(
name: "Projects",
newName: "Novels");
migrationBuilder.RenameTable(
name: "ProjectMembers",
newName: "NovelMembers");
migrationBuilder.RenameColumn(
name: "ProjectId",
table: "Tags",
newName: "NovelId");
migrationBuilder.RenameIndex(
name: "IX_Tags_ProjectId_Name",
table: "Tags",
newName: "IX_Tags_NovelId_Name");
migrationBuilder.RenameColumn(
name: "ProjectId",
table: "OpenQuestions",
newName: "NovelId");
migrationBuilder.RenameIndex(
name: "IX_OpenQuestions_ProjectId",
table: "OpenQuestions",
newName: "IX_OpenQuestions_NovelId");
migrationBuilder.RenameColumn(
name: "ProjectId",
table: "ImportJobs",
newName: "NovelId");
migrationBuilder.RenameColumn(
name: "ProjectId",
table: "Conversations",
newName: "NovelId");
migrationBuilder.RenameIndex(
name: "IX_Conversations_ProjectId",
table: "Conversations",
newName: "IX_Conversations_NovelId");
migrationBuilder.RenameColumn(
name: "ProjectId",
table: "Characters",
newName: "NovelId");
migrationBuilder.RenameIndex(
name: "IX_Characters_ProjectId",
table: "Characters",
newName: "IX_Characters_NovelId");
migrationBuilder.RenameColumn(
name: "ProjectId",
table: "Chapters",
newName: "NovelId");
migrationBuilder.RenameIndex(
name: "IX_Chapters_ProjectId_Number",
table: "Chapters",
newName: "IX_Chapters_NovelId_Number");
migrationBuilder.RenameColumn(
name: "ProjectId",
table: "NovelMembers",
newName: "NovelId");
migrationBuilder.RenameColumn(
name: "ProjectRole",
table: "NovelMembers",
newName: "NovelRole");
migrationBuilder.RenameIndex(
name: "IX_ProjectMembers_ProjectId_UserId",
table: "NovelMembers",
newName: "IX_NovelMembers_NovelId_UserId");
migrationBuilder.RenameIndex(
name: "IX_ProjectMembers_UserId",
table: "NovelMembers",
newName: "IX_NovelMembers_UserId");
migrationBuilder.RenameIndex(
name: "IX_Projects_OwnerId",
table: "Novels",
newName: "IX_Novels_OwnerId");
migrationBuilder.DropForeignKey(
name: "FK_ProjectMembers_AspNetUsers_UserId",
table: "NovelMembers");
migrationBuilder.AddForeignKey(
name: "FK_NovelMembers_AspNetUsers_UserId",
table: "NovelMembers",
column: "UserId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_NovelMembers_Novels_NovelId",
table: "NovelMembers",
column: "NovelId",
principalTable: "Novels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Chapters_Novels_NovelId",
table: "Chapters",
column: "NovelId",
principalTable: "Novels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Characters_Novels_NovelId",
table: "Characters",
column: "NovelId",
principalTable: "Novels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Conversations_Novels_NovelId",
table: "Conversations",
column: "NovelId",
principalTable: "Novels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_OpenQuestions_Novels_NovelId",
table: "OpenQuestions",
column: "NovelId",
principalTable: "Novels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Tags_Novels_NovelId",
table: "Tags",
column: "NovelId",
principalTable: "Novels",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Chapters_Novels_NovelId",
table: "Chapters");
migrationBuilder.DropForeignKey(
name: "FK_Characters_Novels_NovelId",
table: "Characters");
migrationBuilder.DropForeignKey(
name: "FK_Conversations_Novels_NovelId",
table: "Conversations");
migrationBuilder.DropForeignKey(
name: "FK_OpenQuestions_Novels_NovelId",
table: "OpenQuestions");
migrationBuilder.DropForeignKey(
name: "FK_Tags_Novels_NovelId",
table: "Tags");
migrationBuilder.DropForeignKey(
name: "FK_NovelMembers_Novels_NovelId",
table: "NovelMembers");
migrationBuilder.DropForeignKey(
name: "FK_NovelMembers_AspNetUsers_UserId",
table: "NovelMembers");
migrationBuilder.AddForeignKey(
name: "FK_ProjectMembers_AspNetUsers_UserId",
table: "NovelMembers",
column: "UserId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.RenameIndex(
name: "IX_Novels_OwnerId",
table: "Novels",
newName: "IX_Projects_OwnerId");
migrationBuilder.RenameIndex(
name: "IX_NovelMembers_UserId",
table: "NovelMembers",
newName: "IX_ProjectMembers_UserId");
migrationBuilder.RenameIndex(
name: "IX_NovelMembers_NovelId_UserId",
table: "NovelMembers",
newName: "IX_ProjectMembers_ProjectId_UserId");
migrationBuilder.RenameColumn(
name: "NovelRole",
table: "NovelMembers",
newName: "ProjectRole");
migrationBuilder.RenameColumn(
name: "NovelId",
table: "NovelMembers",
newName: "ProjectId");
migrationBuilder.RenameIndex(
name: "IX_Chapters_NovelId_Number",
table: "Chapters",
newName: "IX_Chapters_ProjectId_Number");
migrationBuilder.RenameColumn(
name: "NovelId",
table: "Chapters",
newName: "ProjectId");
migrationBuilder.RenameIndex(
name: "IX_Characters_NovelId",
table: "Characters",
newName: "IX_Characters_ProjectId");
migrationBuilder.RenameColumn(
name: "NovelId",
table: "Characters",
newName: "ProjectId");
migrationBuilder.RenameIndex(
name: "IX_Conversations_NovelId",
table: "Conversations",
newName: "IX_Conversations_ProjectId");
migrationBuilder.RenameColumn(
name: "NovelId",
table: "Conversations",
newName: "ProjectId");
migrationBuilder.RenameColumn(
name: "NovelId",
table: "ImportJobs",
newName: "ProjectId");
migrationBuilder.RenameIndex(
name: "IX_OpenQuestions_NovelId",
table: "OpenQuestions",
newName: "IX_OpenQuestions_ProjectId");
migrationBuilder.RenameColumn(
name: "NovelId",
table: "OpenQuestions",
newName: "ProjectId");
migrationBuilder.RenameIndex(
name: "IX_Tags_NovelId_Name",
table: "Tags",
newName: "IX_Tags_ProjectId_Name");
migrationBuilder.RenameColumn(
name: "NovelId",
table: "Tags",
newName: "ProjectId");
migrationBuilder.RenameTable(
name: "NovelMembers",
newName: "ProjectMembers");
migrationBuilder.RenameTable(
name: "Novels",
newName: "Projects");
migrationBuilder.AddForeignKey(
name: "FK_ProjectMembers_Projects_ProjectId",
table: "ProjectMembers",
column: "ProjectId",
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Chapters_Projects_ProjectId",
table: "Chapters",
column: "ProjectId",
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Characters_Projects_ProjectId",
table: "Characters",
column: "ProjectId",
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Conversations_Projects_ProjectId",
table: "Conversations",
column: "ProjectId",
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_OpenQuestions_Projects_ProjectId",
table: "OpenQuestions",
column: "ProjectId",
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_Tags_Projects_ProjectId",
table: "Tags",
column: "ProjectId",
principalTable: "Projects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}
@@ -163,7 +163,7 @@ namespace Novelly.Api.Data.Migrations
b.Property<long>("CreatedAt")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
b.Property<Guid>("NovelId")
.HasColumnType("TEXT");
b.Property<string>("Title")
@@ -176,7 +176,7 @@ namespace Novelly.Api.Data.Migrations
b.HasKey("Id");
b.HasIndex("ProjectId");
b.HasIndex("NovelId");
b.ToTable("Conversations");
});
@@ -264,12 +264,12 @@ namespace Novelly.Api.Data.Migrations
b.Property<string>("Notes")
.HasColumnType("TEXT");
b.Property<Guid>("NovelId")
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.Property<string>("Prose")
.HasColumnType("TEXT");
@@ -300,7 +300,7 @@ namespace Novelly.Api.Data.Migrations
b.HasKey("Id");
b.HasIndex("ProjectId", "Number");
b.HasIndex("NovelId", "Number");
b.ToTable("Chapters");
});
@@ -355,15 +355,15 @@ namespace Novelly.Api.Data.Migrations
b.Property<string>("Notes")
.HasColumnType("TEXT");
b.Property<Guid>("NovelId")
.HasColumnType("TEXT");
b.Property<string>("Occupation")
.HasColumnType("TEXT");
b.Property<string>("Personality")
.HasColumnType("TEXT");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.Property<string>("Pronouns")
.HasColumnType("TEXT");
@@ -389,7 +389,7 @@ namespace Novelly.Api.Data.Migrations
b.HasKey("Id");
b.HasIndex("ProjectId");
b.HasIndex("NovelId");
b.HasIndex("RevealedInChapterId");
@@ -591,7 +591,7 @@ namespace Novelly.Api.Data.Migrations
b.Property<long>("CreatedAt")
.HasColumnType("INTEGER");
b.Property<Guid?>("ProjectId")
b.Property<Guid?>("NovelId")
.HasColumnType("TEXT");
b.Property<Guid?>("RequestedByUserId")
@@ -620,7 +620,7 @@ namespace Novelly.Api.Data.Migrations
b.ToTable("ImportJobs");
});
modelBuilder.Entity("Novelly.Api.Projects.Project", b =>
modelBuilder.Entity("Novelly.Api.Novels.Novel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
@@ -667,7 +667,7 @@ namespace Novelly.Api.Data.Migrations
b.HasIndex("OwnerId");
b.ToTable("Projects");
b.ToTable("Novels");
});
modelBuilder.Entity("Novelly.Api.Questions.OpenQuestion", b =>
@@ -688,7 +688,7 @@ namespace Novelly.Api.Data.Migrations
b.Property<string>("Detail")
.HasColumnType("TEXT");
b.Property<Guid>("ProjectId")
b.Property<Guid>("NovelId")
.HasColumnType("TEXT");
b.Property<string>("Question")
@@ -711,7 +711,7 @@ namespace Novelly.Api.Data.Migrations
b.HasIndex("CharacterId");
b.HasIndex("ProjectId");
b.HasIndex("NovelId");
b.ToTable("OpenQuestions");
});
@@ -734,17 +734,50 @@ namespace Novelly.Api.Data.Migrations
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<Guid>("ProjectId")
b.Property<Guid>("NovelId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ProjectId", "Name")
b.HasIndex("NovelId", "Name")
.IsUnique();
b.ToTable("Tags");
});
modelBuilder.Entity("Novelly.Api.Users.NovelMember", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<long>("GrantedAt")
.HasColumnType("INTEGER");
b.Property<Guid>("GrantedByUserId")
.HasColumnType("TEXT");
b.Property<Guid>("NovelId")
.HasColumnType("TEXT");
b.Property<string>("NovelRole")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("NovelId", "UserId")
.IsUnique();
b.ToTable("NovelMembers");
});
modelBuilder.Entity("Novelly.Api.Users.NovellyUser", b =>
{
b.Property<Guid>("Id")
@@ -823,39 +856,6 @@ namespace Novelly.Api.Data.Migrations
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Novelly.Api.Users.ProjectMember", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<long>("GrantedAt")
.HasColumnType("INTEGER");
b.Property<Guid>("GrantedByUserId")
.HasColumnType("TEXT");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.Property<string>("ProjectRole")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("ProjectId", "UserId")
.IsUnique();
b.ToTable("ProjectMembers");
});
modelBuilder.Entity("BeatCharacter", b =>
{
b.HasOne("Novelly.Api.Beats.Beat", null)
@@ -960,13 +960,13 @@ namespace Novelly.Api.Data.Migrations
modelBuilder.Entity("Novelly.Api.Agent.AgentConversation", b =>
{
b.HasOne("Novelly.Api.Projects.Project", "Project")
b.HasOne("Novelly.Api.Novels.Novel", "Novel")
.WithMany("Conversations")
.HasForeignKey("ProjectId")
.HasForeignKey("NovelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
b.Navigation("Novel");
});
modelBuilder.Entity("Novelly.Api.Agent.AgentMessage", b =>
@@ -993,20 +993,20 @@ namespace Novelly.Api.Data.Migrations
modelBuilder.Entity("Novelly.Api.Chapters.Chapter", b =>
{
b.HasOne("Novelly.Api.Projects.Project", "Project")
b.HasOne("Novelly.Api.Novels.Novel", "Novel")
.WithMany("Chapters")
.HasForeignKey("ProjectId")
.HasForeignKey("NovelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
b.Navigation("Novel");
});
modelBuilder.Entity("Novelly.Api.Characters.Character", b =>
{
b.HasOne("Novelly.Api.Projects.Project", "Project")
b.HasOne("Novelly.Api.Novels.Novel", "Novel")
.WithMany("Characters")
.HasForeignKey("ProjectId")
.HasForeignKey("NovelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1020,7 +1020,7 @@ namespace Novelly.Api.Data.Migrations
.HasForeignKey("SameCharacterAsId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Project");
b.Navigation("Novel");
b.Navigation("RevealedInChapter");
@@ -1064,7 +1064,7 @@ namespace Novelly.Api.Data.Migrations
b.Navigation("RelatedCharacter");
});
modelBuilder.Entity("Novelly.Api.Projects.Project", b =>
modelBuilder.Entity("Novelly.Api.Novels.Novel", b =>
{
b.HasOne("Novelly.Api.Users.NovellyUser", "Owner")
.WithMany()
@@ -1086,9 +1086,9 @@ namespace Novelly.Api.Data.Migrations
.HasForeignKey("CharacterId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("Novelly.Api.Projects.Project", "Project")
b.HasOne("Novelly.Api.Novels.Novel", "Novel")
.WithMany()
.HasForeignKey("ProjectId")
.HasForeignKey("NovelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1096,25 +1096,25 @@ namespace Novelly.Api.Data.Migrations
b.Navigation("Character");
b.Navigation("Project");
b.Navigation("Novel");
});
modelBuilder.Entity("Novelly.Api.Tags.Tag", b =>
{
b.HasOne("Novelly.Api.Projects.Project", "Project")
b.HasOne("Novelly.Api.Novels.Novel", "Novel")
.WithMany("Tags")
.HasForeignKey("ProjectId")
.HasForeignKey("NovelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
b.Navigation("Novel");
});
modelBuilder.Entity("Novelly.Api.Users.ProjectMember", b =>
modelBuilder.Entity("Novelly.Api.Users.NovelMember", b =>
{
b.HasOne("Novelly.Api.Projects.Project", "Project")
b.HasOne("Novelly.Api.Novels.Novel", "Novel")
.WithMany("Members")
.HasForeignKey("ProjectId")
.HasForeignKey("NovelId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -1124,7 +1124,7 @@ namespace Novelly.Api.Data.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
b.Navigation("Novel");
b.Navigation("User");
});
@@ -1148,7 +1148,7 @@ namespace Novelly.Api.Data.Migrations
b.Navigation("Relationships");
});
modelBuilder.Entity("Novelly.Api.Projects.Project", b =>
modelBuilder.Entity("Novelly.Api.Novels.Novel", b =>
{
b.Navigation("Chapters");
+5 -5
View File
@@ -7,7 +7,7 @@ using Novelly.Api.Chapters;
using Novelly.Api.Characters;
using Novelly.Api.Genres;
using Novelly.Api.Imports;
using Novelly.Api.Projects;
using Novelly.Api.Novels;
using Novelly.Api.Questions;
using Novelly.Api.Tags;
using Novelly.Api.Users;
@@ -18,7 +18,7 @@ internal class UtcTicksConverter() : ValueConverter<DateTimeOffset, long>(value
public class NovelDbContext(DbContextOptions<NovelDbContext> options) : IdentityUserContext<NovellyUser, Guid>(options), INovelDbContext
{
public DbSet<Project> Projects => Set<Project>();
public DbSet<Novel> Novels => Set<Novel>();
public DbSet<Character> Characters => Set<Character>();
public DbSet<CharacterRelationship> CharacterRelationships => Set<CharacterRelationship>();
public DbSet<CharacterArcStage> CharacterArcStages => Set<CharacterArcStage>();
@@ -30,7 +30,7 @@ public class NovelDbContext(DbContextOptions<NovelDbContext> options) : Identity
public DbSet<AgentMessage> AgentMessages => Set<AgentMessage>();
public DbSet<ImportJob> ImportJobs => Set<ImportJob>();
public DbSet<Genre> Genres => Set<Genre>();
public DbSet<ProjectMember> ProjectMembers => Set<ProjectMember>();
public DbSet<NovelMember> NovelMembers => Set<NovelMember>();
Task<int> INovelDbContext.SaveChangesAsync(CancellationToken cancellationToken) => base.SaveChangesAsync(cancellationToken);
@@ -45,7 +45,7 @@ public class NovelDbContext(DbContextOptions<NovelDbContext> options) : Identity
public interface INovelDbContext
{
DbSet<Project> Projects { get; }
DbSet<Novel> Novels { get; }
DbSet<Character> Characters { get; }
DbSet<CharacterRelationship> CharacterRelationships { get; }
DbSet<CharacterArcStage> CharacterArcStages { get; }
@@ -58,7 +58,7 @@ public interface INovelDbContext
DbSet<ImportJob> ImportJobs { get; }
DbSet<Genre> Genres { get; }
DbSet<NovellyUser> Users { get; }
DbSet<ProjectMember> ProjectMembers { get; }
DbSet<NovelMember> NovelMembers { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}