Files
novelly/src/Novelly.Api/Novels/NovelService.cs
T
James Wampler 4313c8f206 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/.
2026-08-17 23:03:09 -07:00

134 lines
4.2 KiB
C#

using Microsoft.EntityFrameworkCore;
using Novelly.Api.Common;
using Novelly.Api.Common.Validation;
using Novelly.Api.Data;
using Novelly.Api.Users;
namespace Novelly.Api.Novels;
public class NovelService(
INovelDbContext db,
NovelAccessService access,
INovelUserContext userContext,
ILogger<NovelService> logger,
IModelValidator<CreateNovelRequest> createValidator,
IModelValidator<UpdateNovelRequest> updateValidator)
{
public async Task<IReadOnlyList<NovelSummaryResponse>> ListAsync(CancellationToken ct = default)
{
logger.LogInformation("Listing novels");
return await access.VisibleNovels()
.OrderByDescending(p => p.UpdatedAt)
.Select(p => new NovelSummaryResponse(
p.Id,
p.Title,
p.Author,
p.Genre,
p.Logline,
p.TargetWordCount,
p.Phase,
p.Characters.Count,
p.Chapters.Count,
p.Chapters.Sum(c => (int?)c.WordCount) ?? 0,
p.UpdatedAt))
.ToListAsync(ct);
}
public async Task<Novel?> GetAsync(Guid id, CancellationToken ct = default)
{
Guard.Default(id, nameof(id));
logger.LogInformation("Getting novel {NovelId}", id);
var novel = await FindAsync(id, ct);
if (novel is null) return null;
await access.RequireAsync(id, NovelPermission.Read, ct);
return novel;
}
public async Task<Novel> CreateAsync(CreateNovelRequest request, CancellationToken ct = default)
{
Guard.Null(request, nameof(request));
createValidator.Validate(request).ThrowIfInvalid(logger);
access.RequireCanCreateNovel();
logger.LogInformation("Creating novel {Title}", request.Title);
var novel = new Novel
{
Title = request.Title,
Author = request.Author,
Genre = request.Genre,
Logline = request.Logline,
Synopsis = request.Synopsis,
Notes = request.Notes,
TargetWordCount = request.TargetWordCount,
OwnerId = userContext.UserId
};
db.Novels.Add(novel);
await db.SaveChangesAsync(ct);
return novel;
}
public async Task<Novel?> UpdateAsync(Guid id, UpdateNovelRequest request, CancellationToken ct = default)
{
Guard.Default(id, nameof(id));
Guard.Null(request, nameof(request));
updateValidator.Validate(request).ThrowIfInvalid(logger);
logger.LogInformation("Updating novel {NovelId}", id);
var novel = await FindAsync(id, ct);
if (novel is null) return null;
await access.RequireAsync(id, NovelPermission.Write, ct);
novel.Title = Patch.Apply(novel.Title, request.Title) ?? novel.Title;
novel.Author = Patch.Apply(novel.Author, request.Author);
novel.Genre = Patch.Apply(novel.Genre, request.Genre);
novel.Logline = Patch.Apply(novel.Logline, request.Logline);
novel.Synopsis = Patch.Apply(novel.Synopsis, request.Synopsis);
novel.Notes = Patch.Apply(novel.Notes, request.Notes);
novel.TargetWordCount = request.TargetWordCount ?? novel.TargetWordCount;
novel.Phase = request.Phase ?? novel.Phase;
novel.UpdatedAt = DateTimeOffset.UtcNow;
await db.SaveChangesAsync(ct);
return novel;
}
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
{
Guard.Default(id, nameof(id));
logger.LogInformation("Deleting novel {NovelId}", id);
var novel = await FindAsync(id, ct);
if (novel is null) return false;
await access.RequireAsync(id, NovelPermission.DeleteContent, ct);
db.Novels.Remove(novel);
await db.SaveChangesAsync(ct);
return true;
}
private async Task<Novel?> FindAsync(Guid id, CancellationToken ct)
{
logger.LogDebug("Finding novel {NovelId}", id);
var novel = await db.Novels.FirstOrDefaultAsync(p => p.Id == id, ct);
if (novel is null)
{
logger.LogWarning("Novel {NovelId} not found", id);
return novel;
}
logger.LogDebug("Found novel {NovelId}", id);
return novel;
}
}