Add character aliases/identity links, move-beats, keyboard help overlay

- Characters can carry aliases and be linked as the same underlying
  person (canonical SameCharacterAsId, optional reveal chapter/note),
  surfaced through the API, MCP tools, agent toolset, and web UI.
- Characters page redesigned as a filterable/sortable table (name+
  aliases, role, importance, occupation, tags) instead of a sidebar
  list, to stay usable as the cast grows.
- Beats can be moved between chapters (BeatService.MoveAsync + MCP/
  agent tool + endpoint).
- Add a keyboard-shortcuts help overlay (HelpButton/HelpOverlayContext)
  wired into the project layout.
- CLAUDE.md: require every frontend component to carry a unique id
  attribute; apply it to CharacterMultiSelect and MarkdownEditor.
This commit is contained in:
James Wampler
2026-08-17 17:26:50 -07:00
parent b4f4b35e3c
commit f124b9b4bb
32 changed files with 3002 additions and 356 deletions
+61 -3
View File
@@ -151,7 +151,8 @@ public class NovelAgentToolset(
JsonInput.String(input, "arc_summary"),
JsonInput.String(input, "voice"),
JsonInput.String(input, "notes"),
JsonInput.Strings(input, "tags")), ct), c => c.ToResponse(), "Project", projectId));
JsonInput.Strings(input, "tags"),
JsonInput.Strings(input, "aliases")), ct), c => c.ToResponse(), "Project", projectId));
yield return new AgentTool(
"update_character",
@@ -181,7 +182,42 @@ public class NovelAgentToolset(
JsonInput.String(input, "arc_summary"),
JsonInput.String(input, "voice"),
JsonInput.String(input, "notes"),
JsonInput.Strings(input, "tags")), ct), c => c.ToResponse(), "Character", characterId);
JsonInput.Strings(input, "tags"),
JsonInput.Strings(input, "aliases")), ct), c => c.ToResponse(), "Character", characterId);
});
yield return new AgentTool(
"link_character_identity",
"Record that a character is really another character — e.g. one introduced under one name "
+ "who is later revealed to be a character already in the project under another name. Both "
+ "keep their own dossier and beats; the canonical identity is whichever character you link to.",
new JsonSchemaBuilder()
.Str("character_id", "Id of the character being revealed as someone else.", required: true)
.Str("same_character_as_id", "Id of the character this one really is.", required: true)
.Str("revealed_in_chapter_id", "Id of the chapter where the reveal happens, if any.")
.Str("note", "Context on the reveal, e.g. how and why the disguise held.")
.Build(),
async (_, input, ct) =>
{
var characterId = JsonInput.RequiredGuid(input, "character_id");
return await OrNotFound(characters.LinkIdentityAsync(
characterId,
new LinkCharacterIdentityRequest(
JsonInput.RequiredGuid(input, "same_character_as_id"),
JsonInput.Guid(input, "revealed_in_chapter_id"),
JsonInput.String(input, "note")), ct), c => c.ToResponse(), "Character", characterId);
});
yield return new AgentTool(
"unlink_character_identity",
"Remove a character's identity link, restoring it to its own separate identity.",
new JsonSchemaBuilder()
.Str("character_id", "Id of the character to unlink.", required: true)
.Build(),
async (_, input, ct) =>
{
var characterId = JsonInput.RequiredGuid(input, "character_id");
return await DeletedOrNotFound(characters.UnlinkIdentityAsync(characterId, ct), "Character", characterId);
});
yield return new AgentTool(
@@ -289,6 +325,27 @@ public class NovelAgentToolset(
.Where(g => g != Guid.Empty)]), ct), list => list.Select(b => b.ToResponse()), "Chapter", chapterId);
});
yield return new AgentTool(
"move_beats",
"Move one or more beats from one chapter to another, appending them to the target "
+ "chapter's end in the order given.",
new JsonSchemaBuilder()
.Str("chapter_id", "Id of the beats' current chapter.", required: true)
.Str("target_chapter_id", "Id of the chapter to move the beats into.", required: true)
.StringArray("beat_ids", "Ids of the beats to move.", required: true)
.Build(),
async (_, input, ct) =>
{
var chapterId = JsonInput.RequiredGuid(input, "chapter_id");
return await OrNotFound(beats.MoveAsync(
chapterId,
new MoveBeatsRequest(
JsonInput.RequiredGuid(input, "target_chapter_id"),
[.. (JsonInput.Strings(input, "beat_ids") ?? [])
.Select(id => Guid.TryParse(id, out var g) ? g : Guid.Empty)
.Where(g => g != Guid.Empty)]), ct), list => list.Select(b => b.ToResponse()), "Chapter", chapterId);
});
yield return new AgentTool(
"list_tags",
"List the project's tags with how many characters, chapters and beats carry each. "
@@ -596,7 +653,8 @@ public class NovelAgentToolset(
.Str("arc_summary", "How they change over the course of the book.")
.Str("voice", "Speech patterns and register that make their dialogue theirs.")
.Str("notes", "Anything else worth recording.")
.StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.");
.StringArray("tags", "Tags for cross-referencing. Replaces the existing tags.")
.StringArray("aliases", "Other names this character is known by. Replaces the existing aliases.");
}
private static JsonSchemaBuilder BeatSchema() =>
+18
View File
@@ -119,6 +119,24 @@ public class AssignCharacterToBeatsRequestValidator : IModelValidator<AssignChar
}
}
public record MoveBeatsRequest(Guid TargetChapterId, IReadOnlyList<Guid> BeatIds);
public class MoveBeatsRequestValidator : IModelValidator<MoveBeatsRequest>
{
public ValidationResult Validate(MoveBeatsRequest model)
{
var result = new ValidationResult();
if (model.TargetChapterId == Guid.Empty)
result.AddError("TargetChapterId", "'Target Chapter Id' must not be empty.");
if (model.BeatIds is null || model.BeatIds.Count == 0)
result.AddError("BeatIds", "'Beat Ids' must not be empty.");
return result;
}
}
public static class BeatMapping
{
public static BeatResponse ToResponse(this Beat b) => new(
+5
View File
@@ -39,6 +39,11 @@ public static class BeatEndpoints
(await service.AssignCharacterAsync(chapterId, request, ct))?.Select(b => b.ToResponse()).ToList().ToApiResult())
.WithSummary("Add a character to several beats at once, leaving each beat's existing characters alone.");
chapterScoped.MapPost("/move", async (
Guid chapterId, MoveBeatsRequest request, BeatService service, CancellationToken ct) =>
(await service.MoveAsync(chapterId, request, ct))?.Select(b => b.ToResponse()).ToList().ToApiResult())
.WithSummary("Move one or more beats to another chapter, appending them to its end.");
app.MapGet("/api/characters/{characterId:guid}/beats", async (
Guid characterId, BeatService service, CancellationToken ct) =>
(await service.ListForCharacterAsync(characterId, ct))?.Select(b => b.ToCharacterBeatResponse()).ToList().ToApiResult())
+58 -1
View File
@@ -17,7 +17,8 @@ public class BeatService(
IModelValidator<CreateBeatRequest> createValidator,
IModelValidator<UpdateBeatRequest> updateValidator,
IModelValidator<ReorderBeatsRequest> reorderValidator,
IModelValidator<AssignCharacterToBeatsRequest> assignCharacterValidator)
IModelValidator<AssignCharacterToBeatsRequest> assignCharacterValidator,
IModelValidator<MoveBeatsRequest> moveValidator)
{
public async Task<IReadOnlyList<Beat>> ListAsync(Guid chapterId, CancellationToken ct = default)
{
@@ -266,6 +267,62 @@ public class BeatService(
return await ListAsync(chapterId, ct);
}
public async Task<IReadOnlyList<Beat>?> MoveAsync(
Guid chapterId, MoveBeatsRequest request, CancellationToken ct = default)
{
Guard.Default(chapterId, nameof(chapterId));
Guard.Null(request, nameof(request));
moveValidator.Validate(request).ThrowIfInvalid(logger);
logger.LogInformation(
"Moving {Count} beats from chapter {ChapterId} to chapter {TargetChapterId}",
request.BeatIds.Count, chapterId, request.TargetChapterId);
var chapter = await db.Chapters.FirstOrDefaultAsync(c => c.Id == chapterId, ct);
if (chapter is null)
{
logger.LogWarning("Rejected beat move: chapter {ChapterId} not found", chapterId);
return null;
}
var targetChapter = await db.Chapters.FirstOrDefaultAsync(
c => c.Id == request.TargetChapterId && c.ProjectId == chapter.ProjectId, ct);
if (targetChapter is null)
{
logger.LogWarning(
"Rejected beat move: target chapter {TargetChapterId} not found in project {ProjectId}",
request.TargetChapterId, chapter.ProjectId);
return null;
}
await access.RequireAsync(chapter.ProjectId, ProjectPermission.Write, ct);
var beats = await Query().Where(b => b.ChapterId == chapterId && request.BeatIds.Contains(b.Id)).ToListAsync(ct);
var missing = request.BeatIds.Where(id => beats.All(b => b.Id != id)).ToList();
if (missing.Count > 0)
{
logger.LogWarning("Rejected beat move: chapter {ChapterId} referenced missing beat {BeatId}", chapterId, missing[0]);
return null;
}
if (chapterId == request.TargetChapterId)
{
return beats;
}
var nextSortOrder = await NextSortOrderAsync(request.TargetChapterId, ct);
foreach (var beatId in request.BeatIds)
{
var beat = beats.Single(b => b.Id == beatId);
beat.ChapterId = request.TargetChapterId;
beat.SortOrder = nextSortOrder++;
beat.UpdatedAt = DateTimeOffset.UtcNow;
}
await db.SaveChangesAsync(ct);
return beats;
}
private async Task<List<Character>> ResolveCharactersAsync(Guid projectId, IReadOnlyList<Guid> characterIds, CancellationToken ct)
{
logger.LogDebug("Resolving {Count} characters for project {ProjectId}", characterIds.Count, projectId);
+18
View File
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Projects;
using Novelly.Api.Tags;
@@ -38,6 +39,16 @@ public class Character
public string? Notes { get; set; }
public List<string> Aliases { get; set; } = [];
public Guid? SameCharacterAsId { get; set; }
public Character? SameCharacterAs { get; set; }
public List<Character> OtherIdentities { get; set; } = [];
public Guid? RevealedInChapterId { get; set; }
public Chapter? RevealedInChapter { get; set; }
public string? IdentityNote { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
@@ -72,12 +83,19 @@ public class CharacterEntityTypeConfiguration : IEntityTypeConfiguration<Charact
entity.Property(c => c.Role).HasConversion<string>().HasMaxLength(32);
entity.Property(c => c.Importance).HasConversion<string>().HasMaxLength(32);
entity.HasIndex(c => c.ProjectId);
entity.HasIndex(c => c.SameCharacterAsId);
entity.HasMany(c => c.Relationships).WithOne(r => r.Character!)
.HasForeignKey(r => r.CharacterId).OnDelete(DeleteBehavior.Cascade);
entity.HasMany(c => c.ArcStages).WithOne(s => s.Character!)
.HasForeignKey(s => s.CharacterId).OnDelete(DeleteBehavior.Cascade);
entity.HasOne(c => c.SameCharacterAs).WithMany(c => c.OtherIdentities)
.HasForeignKey(c => c.SameCharacterAsId).OnDelete(DeleteBehavior.SetNull);
entity.HasOne(c => c.RevealedInChapter).WithMany()
.HasForeignKey(c => c.RevealedInChapterId).OnDelete(DeleteBehavior.SetNull);
}
}
@@ -22,11 +22,20 @@ public record CharacterResponse(
string? ArcSummary,
string? Voice,
string? Notes,
IReadOnlyList<string> Aliases,
Guid? SameCharacterAsId,
string? SameCharacterAsName,
Guid? RevealedInChapterId,
int? RevealedInChapterNumber,
string? IdentityNote,
IReadOnlyList<CharacterIdentityResponse> OtherIdentities,
IReadOnlyList<RelationshipResponse> Relationships,
IReadOnlyList<TagResponse> Tags,
IReadOnlyList<ArcStageResponse> ArcStages,
DateTimeOffset UpdatedAt);
public record CharacterIdentityResponse(Guid Id, string Name);
public record RelationshipResponse(
Guid Id,
Guid RelatedCharacterId,
@@ -51,7 +60,8 @@ public record CreateCharacterRequest(
string? ArcSummary = null,
string? Voice = null,
string? Notes = null,
IReadOnlyList<string>? Tags = null);
IReadOnlyList<string>? Tags = null,
IReadOnlyList<string>? Aliases = null);
public class CreateCharacterRequestValidator : IModelValidator<CreateCharacterRequest>
{
@@ -63,7 +73,7 @@ public class CreateCharacterRequestValidator : IModelValidator<CreateCharacterRe
CharacterValidation.OptionalFields(
model.Age, model.Pronouns, model.Occupation, model.Appearance, model.Personality, model.Backstory,
model.Want, model.Need, model.InternalConflict, model.ExternalConflict, model.ArcSummary, model.Voice,
model.Notes, model.Tags, result);
model.Notes, model.Tags, model.Aliases, result);
return result;
}
@@ -86,7 +96,8 @@ public record UpdateCharacterRequest(
string? ArcSummary = null,
string? Voice = null,
string? Notes = null,
IReadOnlyList<string>? Tags = null);
IReadOnlyList<string>? Tags = null,
IReadOnlyList<string>? Aliases = null);
public class UpdateCharacterRequestValidator : IModelValidator<UpdateCharacterRequest>
{
@@ -98,7 +109,7 @@ public class UpdateCharacterRequestValidator : IModelValidator<UpdateCharacterRe
CharacterValidation.OptionalFields(
model.Age, model.Pronouns, model.Occupation, model.Appearance, model.Personality, model.Backstory,
model.Want, model.Need, model.InternalConflict, model.ExternalConflict, model.ArcSummary, model.Voice,
model.Notes, model.Tags, result);
model.Notes, model.Tags, model.Aliases, result);
return result;
}
@@ -109,7 +120,7 @@ file static class CharacterValidation
public static void OptionalFields(
string? age, string? pronouns, string? occupation, string? appearance, string? personality, string? backstory,
string? want, string? need, string? internalConflict, string? externalConflict, string? arcSummary, string? voice,
string? notes, IReadOnlyList<string>? tags, ValidationResult result)
string? notes, IReadOnlyList<string>? tags, IReadOnlyList<string>? aliases, ValidationResult result)
{
Cap(age, "Age", 100, result);
Cap(pronouns, "Pronouns", 100, result);
@@ -127,6 +138,18 @@ file static class CharacterValidation
if (tags is not null && tags.Any(string.IsNullOrWhiteSpace))
result.AddError("Tags", "'Tags' must not contain blank entries.");
if (aliases is not null)
{
if (aliases.Any(string.IsNullOrWhiteSpace))
result.AddError("Aliases", "'Aliases' must not contain blank entries.");
if (aliases.Any(a => a.Length > 200))
result.AddError("Aliases", "'Aliases' entries must be 200 characters or fewer.");
if (aliases.Count > 25)
result.AddError("Aliases", "'Aliases' must contain 25 entries or fewer.");
}
}
private static void Cap(string? value, string field, int max, ValidationResult result)
@@ -157,6 +180,26 @@ public class CreateRelationshipRequestValidator : IModelValidator<CreateRelation
}
}
public record LinkCharacterIdentityRequest(
Guid SameCharacterAsId,
Guid? RevealedInChapterId = null,
string? Note = null);
public class LinkCharacterIdentityRequestValidator : IModelValidator<LinkCharacterIdentityRequest>
{
public ValidationResult Validate(LinkCharacterIdentityRequest model)
{
var result = new ValidationResult();
if (model.SameCharacterAsId == Guid.Empty)
result.AddError("SameCharacterAsId", "'Same Character As Id' must not be empty.");
result.AddOptionalTextErrors("Note", "Note", model.Note, 2000);
return result;
}
}
public record ArcStageResponse(
Guid Id,
Guid CharacterId,
@@ -240,6 +283,13 @@ public static class CharacterMapping
c.Id, c.ProjectId, c.Name, c.Role, c.Importance, c.Age, c.Pronouns, c.Occupation,
c.Appearance, c.Personality, c.Backstory, c.Want, c.Need,
c.InternalConflict, c.ExternalConflict, c.ArcSummary, c.Voice, c.Notes,
[.. c.Aliases],
c.SameCharacterAsId,
c.SameCharacterAs?.Name,
c.RevealedInChapterId,
c.RevealedInChapter?.Number,
c.IdentityNote,
[.. c.OtherIdentities.OrderBy(o => o.Name).Select(o => new CharacterIdentityResponse(o.Id, o.Name))],
[.. c.Relationships.Select(r => new RelationshipResponse(
r.Id,
r.RelatedCharacterId,
@@ -56,6 +56,16 @@ public static class CharacterEndpoints
await service.RemoveRelationshipAsync(relationshipId, ct) ? Results.NoContent() : Results.NotFound())
.WithSummary("Remove a relationship.");
characters.MapPut("/{id:guid}/identity", async (
Guid id, LinkCharacterIdentityRequest request, CharacterService service, CancellationToken ct) =>
(await service.LinkIdentityAsync(id, request, ct))?.ToResponse().ToApiResult())
.WithSummary("Link this character as another identity of a character in the same project.");
characters.MapDelete("/{id:guid}/identity", async (
Guid id, CharacterService service, CancellationToken ct) =>
await service.UnlinkIdentityAsync(id, ct) ? Results.NoContent() : Results.NotFound())
.WithSummary("Remove this character's identity link.");
characters.MapGet("/{id:guid}/arc", async (
Guid id, CharacterArcService service, CancellationToken ct) =>
Results.Ok((await service.ListAsync(id, ct)).Select(s => s.ToResponse())))
+107 -2
View File
@@ -14,7 +14,8 @@ public class CharacterService(
ILogger<CharacterService> logger,
IModelValidator<CreateCharacterRequest> createValidator,
IModelValidator<UpdateCharacterRequest> updateValidator,
IModelValidator<CreateRelationshipRequest> relationshipValidator)
IModelValidator<CreateRelationshipRequest> relationshipValidator,
IModelValidator<LinkCharacterIdentityRequest> identityValidator)
{
public async Task<IReadOnlyList<Character>> ListAsync(Guid projectId, CancellationToken ct = default)
{
@@ -97,6 +98,11 @@ public class CharacterService(
character.Tags = await tags.ResolveAsync(projectId, names, ct);
}
if (request.Aliases is { } aliases)
{
character.Aliases = [.. aliases];
}
db.Characters.Add(character);
await db.SaveChangesAsync(ct);
@@ -142,6 +148,11 @@ public class CharacterService(
character.Tags = await tags.ResolveAsync(character.ProjectId, names, ct);
}
if (request.Aliases is { } aliases)
{
character.Aliases = [.. aliases];
}
await db.SaveChangesAsync(ct);
return (await FindAsync(id, ct))!;
}
@@ -229,13 +240,107 @@ public class CharacterService(
return true;
}
public async Task<Character?> LinkIdentityAsync(
Guid characterId, LinkCharacterIdentityRequest request, CancellationToken ct = default)
{
Guard.Default(characterId, nameof(characterId));
Guard.Null(request, nameof(request));
identityValidator.Validate(request).ThrowIfInvalid(logger);
logger.LogInformation(
"Linking character {CharacterId} to identity {SameCharacterAsId}", characterId, request.SameCharacterAsId);
var character = await FindAsync(characterId, ct);
if (character is null)
{
return null;
}
await access.RequireAsync(character.ProjectId, ProjectPermission.Write, ct);
if (request.SameCharacterAsId == characterId)
{
logger.LogWarning("Rejected identity link: character {CharacterId} cannot be linked to itself", characterId);
throw new InvalidOperationException("A character cannot be linked to itself.");
}
var target = await db.Characters
.Include(c => c.SameCharacterAs)
.FirstOrDefaultAsync(c => c.Id == request.SameCharacterAsId, ct);
if (target is null)
{
logger.LogWarning("Rejected identity link: target character {SameCharacterAsId} not found", request.SameCharacterAsId);
return null;
}
if (target.ProjectId != character.ProjectId)
{
logger.LogWarning(
"Rejected identity link: character {CharacterId} and {SameCharacterAsId} belong to different projects",
characterId, request.SameCharacterAsId);
throw new InvalidOperationException("Characters must belong to the same project to be linked.");
}
if (await db.Characters.AnyAsync(c => c.SameCharacterAsId == characterId, ct))
{
logger.LogWarning("Rejected identity link: character {CharacterId} already has other identities pointing to it", characterId);
throw new InvalidOperationException("This character already has other identities linked to it and cannot itself be an alias.");
}
var canonical = target.SameCharacterAsId ?? target.Id;
if (request.RevealedInChapterId is { } chapterId)
{
var chapterInProject = await db.Chapters.AnyAsync(c => c.Id == chapterId && c.ProjectId == character.ProjectId, ct);
if (!chapterInProject)
{
logger.LogWarning("Rejected identity link: chapter {ChapterId} not in project {ProjectId}", chapterId, character.ProjectId);
throw new InvalidOperationException("The reveal chapter must belong to the same project.");
}
}
character.SameCharacterAsId = canonical;
character.RevealedInChapterId = request.RevealedInChapterId;
character.IdentityNote = request.Note;
character.UpdatedAt = DateTimeOffset.UtcNow;
await db.SaveChangesAsync(ct);
return (await FindAsync(characterId, ct))!;
}
public async Task<bool> UnlinkIdentityAsync(Guid characterId, CancellationToken ct = default)
{
Guard.Default(characterId, nameof(characterId));
logger.LogInformation("Unlinking identity for character {CharacterId}", characterId);
var character = await FindAsync(characterId, ct);
if (character is null)
{
return false;
}
await access.RequireAsync(character.ProjectId, ProjectPermission.Write, ct);
character.SameCharacterAsId = null;
character.RevealedInChapterId = null;
character.IdentityNote = null;
character.UpdatedAt = DateTimeOffset.UtcNow;
await db.SaveChangesAsync(ct);
return true;
}
private IQueryable<Character> Query() =>
db.Characters
.Include(c => c.Relationships)
.ThenInclude(r => r.RelatedCharacter)
.Include(c => c.Tags)
.Include(c => c.ArcStages)
.ThenInclude(s => s.Chapter);
.ThenInclude(s => s.Chapter)
.Include(c => c.SameCharacterAs)
.Include(c => c.OtherIdentities)
.Include(c => c.RevealedInChapter);
private async Task<Character?> FindAsync(Guid id, CancellationToken ct)
{
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,102 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Novelly.Api.Data.Migrations
{
/// <inheritdoc />
public partial class AddCharacterAliasesAndIdentityLinks : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Aliases",
table: "Characters",
type: "TEXT",
nullable: false,
defaultValue: "[]");
migrationBuilder.AddColumn<string>(
name: "IdentityNote",
table: "Characters",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "RevealedInChapterId",
table: "Characters",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "SameCharacterAsId",
table: "Characters",
type: "TEXT",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Characters_RevealedInChapterId",
table: "Characters",
column: "RevealedInChapterId");
migrationBuilder.CreateIndex(
name: "IX_Characters_SameCharacterAsId",
table: "Characters",
column: "SameCharacterAsId");
migrationBuilder.AddForeignKey(
name: "FK_Characters_Chapters_RevealedInChapterId",
table: "Characters",
column: "RevealedInChapterId",
principalTable: "Chapters",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
migrationBuilder.AddForeignKey(
name: "FK_Characters_Characters_SameCharacterAsId",
table: "Characters",
column: "SameCharacterAsId",
principalTable: "Characters",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Characters_Chapters_RevealedInChapterId",
table: "Characters");
migrationBuilder.DropForeignKey(
name: "FK_Characters_Characters_SameCharacterAsId",
table: "Characters");
migrationBuilder.DropIndex(
name: "IX_Characters_RevealedInChapterId",
table: "Characters");
migrationBuilder.DropIndex(
name: "IX_Characters_SameCharacterAsId",
table: "Characters");
migrationBuilder.DropColumn(
name: "Aliases",
table: "Characters");
migrationBuilder.DropColumn(
name: "IdentityNote",
table: "Characters");
migrationBuilder.DropColumn(
name: "RevealedInChapterId",
table: "Characters");
migrationBuilder.DropColumn(
name: "SameCharacterAsId",
table: "Characters");
}
}
}
@@ -299,6 +299,10 @@ namespace Novelly.Api.Data.Migrations
b.Property<string>("Age")
.HasColumnType("TEXT");
b.PrimitiveCollection<string>("Aliases")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Appearance")
.HasColumnType("TEXT");
@@ -314,6 +318,9 @@ namespace Novelly.Api.Data.Migrations
b.Property<string>("ExternalConflict")
.HasColumnType("TEXT");
b.Property<string>("IdentityNote")
.HasColumnType("TEXT");
b.Property<string>("Importance")
.IsRequired()
.HasMaxLength(32)
@@ -345,11 +352,17 @@ namespace Novelly.Api.Data.Migrations
b.Property<string>("Pronouns")
.HasColumnType("TEXT");
b.Property<Guid?>("RevealedInChapterId")
.HasColumnType("TEXT");
b.Property<string>("Role")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid?>("SameCharacterAsId")
.HasColumnType("TEXT");
b.Property<long>("UpdatedAt")
.HasColumnType("INTEGER");
@@ -363,6 +376,10 @@ namespace Novelly.Api.Data.Migrations
b.HasIndex("ProjectId");
b.HasIndex("RevealedInChapterId");
b.HasIndex("SameCharacterAsId");
b.ToTable("Characters");
});
@@ -963,7 +980,21 @@ namespace Novelly.Api.Data.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Novelly.Api.Chapters.Chapter", "RevealedInChapter")
.WithMany()
.HasForeignKey("RevealedInChapterId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("Novelly.Api.Characters.Character", "SameCharacterAs")
.WithMany("OtherIdentities")
.HasForeignKey("SameCharacterAsId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Project");
b.Navigation("RevealedInChapter");
b.Navigation("SameCharacterAs");
});
modelBuilder.Entity("Novelly.Api.Characters.CharacterArcStage", b =>
@@ -1082,6 +1113,8 @@ namespace Novelly.Api.Data.Migrations
{
b.Navigation("ArcStages");
b.Navigation("OtherIdentities");
b.Navigation("Relationships");
});