Add outline import feature; drop Dto naming, map entities at the API boundary
Services now return entities; endpoints (and the agent toolsets) map to *Response records instead of services building wire DTOs themselves. Also brings in the outline-import agent, MCP tool, ledger and web dialog that were already in progress on disk.
This commit is contained in:
@@ -20,27 +20,25 @@ public class BeatService(
|
||||
IModelValidator<UpdateBeatRequest> updateValidator,
|
||||
IModelValidator<ReorderBeatsRequest> reorderValidator)
|
||||
{
|
||||
public async Task<IReadOnlyList<BeatDto>> ListAsync(Guid chapterId, CancellationToken ct = default)
|
||||
public async Task<IReadOnlyList<Beat>> ListAsync(Guid chapterId, CancellationToken ct = default)
|
||||
{
|
||||
Guard.Default(chapterId, nameof(chapterId));
|
||||
|
||||
logger.LogInformation("Listing beats for chapter {ChapterId}", chapterId);
|
||||
|
||||
var beats = await Query()
|
||||
return await Query()
|
||||
.Where(b => b.ChapterId == chapterId)
|
||||
.OrderBy(b => b.SortOrder)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return [.. beats.Select(b => b.ToDto())];
|
||||
}
|
||||
|
||||
/// <summary>Null when no beat has this id — a lookup miss is expected, not exceptional.</summary>
|
||||
public async Task<BeatDto?> GetAsync(Guid id, CancellationToken ct = default)
|
||||
public async Task<Beat?> GetAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
Guard.Default(id, nameof(id));
|
||||
|
||||
logger.LogInformation("Getting beat {BeatId}", id);
|
||||
return (await FindAsync(id, ct))?.ToDto();
|
||||
return await FindAsync(id, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -49,7 +47,7 @@ public class BeatService(
|
||||
/// straight to the beat in that chapter's outline. Null when no character has this id;
|
||||
/// an empty list means the character exists but has no beats yet.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<CharacterBeatDto>?> ListForCharacterAsync(
|
||||
public async Task<IReadOnlyList<Beat>?> ListForCharacterAsync(
|
||||
Guid characterId, CancellationToken ct = default)
|
||||
{
|
||||
Guard.Default(characterId, nameof(characterId));
|
||||
@@ -73,21 +71,10 @@ public class BeatService(
|
||||
.. beats
|
||||
.OrderBy(b => b.Chapter?.Number ?? 0)
|
||||
.ThenBy(b => b.SortOrder)
|
||||
.Select(b => new CharacterBeatDto(
|
||||
b.Id,
|
||||
b.ChapterId,
|
||||
b.Chapter?.Number ?? 0,
|
||||
b.Chapter?.Title ?? "(unknown chapter)",
|
||||
b.SortOrder,
|
||||
b.Title,
|
||||
b.WhatHappened,
|
||||
b.WhatsNext,
|
||||
b.SceneId,
|
||||
b.Scene?.Title))
|
||||
];
|
||||
}
|
||||
|
||||
public async Task<BeatDto> CreateAsync(Guid chapterId, CreateBeatRequest request, CancellationToken ct = default)
|
||||
public async Task<Beat> CreateAsync(Guid chapterId, CreateBeatRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Guard.Default(chapterId, nameof(chapterId));
|
||||
Guard.Null(request, nameof(request));
|
||||
@@ -124,10 +111,10 @@ public class BeatService(
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
// Just created it — the reload is only to pick up includes, not to check existence.
|
||||
return (await FindAsync(beat.Id, ct))!.ToDto();
|
||||
return (await FindAsync(beat.Id, ct))!;
|
||||
}
|
||||
|
||||
public async Task<BeatDto?> UpdateAsync(Guid id, UpdateBeatRequest request, CancellationToken ct = default)
|
||||
public async Task<Beat?> UpdateAsync(Guid id, UpdateBeatRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Guard.Default(id, nameof(id));
|
||||
Guard.Null(request, nameof(request));
|
||||
@@ -154,10 +141,10 @@ public class BeatService(
|
||||
|
||||
beat.Title = Patch.Apply(beat.Title, request.Title) ?? beat.Title;
|
||||
beat.SortOrder = request.SortOrder ?? beat.SortOrder;
|
||||
beat.CharacterId = request.CharacterId ?? beat.CharacterId;
|
||||
beat.CharacterId = request.ClearCharacter ? null : request.CharacterId ?? beat.CharacterId;
|
||||
beat.WhatHappened = Patch.Apply(beat.WhatHappened, request.WhatHappened);
|
||||
beat.WhatsNext = Patch.Apply(beat.WhatsNext, request.WhatsNext);
|
||||
beat.SceneId = request.SceneId ?? beat.SceneId;
|
||||
beat.SceneId = request.ClearScene ? null : request.SceneId ?? beat.SceneId;
|
||||
beat.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
if (request.Tags is { } names)
|
||||
@@ -166,7 +153,7 @@ public class BeatService(
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return (await FindAsync(id, ct))!.ToDto();
|
||||
return (await FindAsync(id, ct))!;
|
||||
}
|
||||
|
||||
/// <summary>True if a beat was deleted; false if no beat had this id.</summary>
|
||||
@@ -191,7 +178,7 @@ public class BeatService(
|
||||
/// Renumbers a chapter's beats to match the order given. Sending the whole list beats
|
||||
/// patching sort orders one at a time, which is fiddly to get right from a drag handle.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BeatDto>> ReorderAsync(
|
||||
public async Task<IReadOnlyList<Beat>> ReorderAsync(
|
||||
Guid chapterId, ReorderBeatsRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Guard.Default(chapterId, nameof(chapterId));
|
||||
|
||||
Reference in New Issue
Block a user