Arc stages now group the beats that establish or pay off that stage of a
character's arc (many-to-many via ArcStageBeats), and carry a Result field
(renamed from Description) describing what the stage results in for the
character. Assigning a beat to a stage moves it out of any other stage of
the same character. New endpoint POST /api/arc-stages/{id}/beats, MCP tool
set_arc_stage_beats, and frontend grouping UI in CharacterArc/CharacterBeats.
Also records a relationship's reciprocal type so both characters' dossiers
show the correct direction (e.g. "sister" / "brother") instead of mirroring
the same label.
73 lines
3.8 KiB
C#
73 lines
3.8 KiB
C#
using Novelly.Api.Common;
|
|
using Novelly.Api.Common.Validation;
|
|
|
|
namespace Novelly.Api.Beats;
|
|
|
|
public static class BeatEndpoints
|
|
{
|
|
public static IEndpointRouteBuilder MapBeatEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
var chapterScoped = app.MapGroup("/api/chapters/{chapterId:guid}/beats").WithTags("Beats")
|
|
.AddEndpointFilter<RequestLoggingEndpointFilter>()
|
|
.AddEndpointFilter<ValidationEndpointFilter>();
|
|
|
|
chapterScoped.MapGet("/", async (Guid chapterId, BeatService service, CancellationToken ct) =>
|
|
Results.Ok((await service.ListAsync(chapterId, ct)).Select(b => b.ToResponse())))
|
|
.WithSummary("Read a chapter's outline: its beats, in order.");
|
|
|
|
chapterScoped.MapPost("/", async (
|
|
Guid chapterId, CreateBeatRequest request, BeatService service, CancellationToken ct) =>
|
|
{
|
|
var beat = await service.CreateAsync(chapterId, request, ct);
|
|
if (beat is null)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
var created = beat.ToResponse();
|
|
return Results.Created($"/api/beats/{created.Id}", created);
|
|
})
|
|
.WithSummary("Add a beat to a chapter's outline.");
|
|
|
|
chapterScoped.MapPost("/reorder", async (
|
|
Guid chapterId, ReorderBeatsRequest request, BeatService service, CancellationToken ct) =>
|
|
(await service.ReorderAsync(chapterId, request, ct))?.Select(b => b.ToResponse()).ToList().ToApiResult())
|
|
.WithSummary("Renumber a chapter's beats to match the order given.");
|
|
|
|
chapterScoped.MapPost("/assign-character", async (
|
|
Guid chapterId, AssignCharacterToBeatsRequest request, BeatService service, CancellationToken ct) =>
|
|
(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(characterId)).ToList().ToApiResult())
|
|
.WithTags("Beats")
|
|
.WithSummary("Every beat this character appears in, in manuscript order.");
|
|
|
|
var beats = app.MapGroup("/api/beats").WithTags("Beats")
|
|
.AddEndpointFilter<RequestLoggingEndpointFilter>()
|
|
.AddEndpointFilter<ValidationEndpointFilter>();
|
|
|
|
beats.MapGet("/{id:guid}", async (Guid id, BeatService service, CancellationToken ct) =>
|
|
(await service.GetAsync(id, ct))?.ToResponse().ToApiResult())
|
|
.WithSummary("Read one beat.");
|
|
|
|
beats.MapPatch("/{id:guid}", async (
|
|
Guid id, UpdateBeatRequest request, BeatService service, CancellationToken ct) =>
|
|
(await service.UpdateAsync(id, request, ct))?.ToResponse().ToApiResult())
|
|
.WithSummary("Update a beat. Sending a tag list replaces the beat's tags.");
|
|
|
|
beats.MapDelete("/{id:guid}", async (Guid id, BeatService service, CancellationToken ct) =>
|
|
await service.DeleteAsync(id, ct) ? Results.NoContent() : Results.NotFound())
|
|
.WithSummary("Delete a beat.");
|
|
|
|
return app;
|
|
}
|
|
}
|