Drop the Scene entity/grouping in favor of chapters carrying prose directly and beats belonging to many characters. Add markdown editor + character multi-select components to the web client. Remove all XML doc and inline comments across the touched C#/TS/CSS files in favor of self-documenting names, and record that convention in CLAUDE.md. Add .mcp.json (local MCP server config, no secrets) and ignore .idea/.
128 lines
4.0 KiB
C#
128 lines
4.0 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Novelly.Api.Common;
|
|
using Novelly.Api.Common.Validation;
|
|
using Novelly.Api.Data;
|
|
|
|
namespace Novelly.Api.Projects;
|
|
|
|
public class ProjectService(
|
|
INovelDbContext db,
|
|
ILogger<ProjectService> logger,
|
|
IModelValidator<CreateProjectRequest> createValidator,
|
|
IModelValidator<UpdateProjectRequest> updateValidator)
|
|
{
|
|
public async Task<IReadOnlyList<ProjectSummaryResponse>> ListAsync(CancellationToken ct = default)
|
|
{
|
|
logger.LogInformation("Listing projects");
|
|
|
|
return await db.Projects
|
|
.OrderByDescending(p => p.UpdatedAt)
|
|
.Select(p => new ProjectSummaryResponse(
|
|
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<Project?> GetAsync(Guid id, CancellationToken ct = default)
|
|
{
|
|
Guard.Default(id, nameof(id));
|
|
|
|
logger.LogInformation("Getting project {ProjectId}", id);
|
|
return await FindAsync(id, ct);
|
|
}
|
|
|
|
public async Task<Project> CreateAsync(CreateProjectRequest request, CancellationToken ct = default)
|
|
{
|
|
Guard.Null(request, nameof(request));
|
|
createValidator.Validate(request).ThrowIfInvalid();
|
|
|
|
logger.LogInformation("Creating project {Title}", request.Title);
|
|
|
|
var project = new Project
|
|
{
|
|
Title = request.Title,
|
|
Author = request.Author,
|
|
Genre = request.Genre,
|
|
Logline = request.Logline,
|
|
Synopsis = request.Synopsis,
|
|
Notes = request.Notes,
|
|
TargetWordCount = request.TargetWordCount
|
|
};
|
|
|
|
db.Projects.Add(project);
|
|
await db.SaveChangesAsync(ct);
|
|
return project;
|
|
}
|
|
|
|
public async Task<Project?> UpdateAsync(Guid id, UpdateProjectRequest request, CancellationToken ct = default)
|
|
{
|
|
Guard.Default(id, nameof(id));
|
|
Guard.Null(request, nameof(request));
|
|
updateValidator.Validate(request).ThrowIfInvalid();
|
|
|
|
logger.LogInformation("Updating project {ProjectId}", id);
|
|
|
|
var project = await FindAsync(id, ct);
|
|
if (project is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
project.Title = Patch.Apply(project.Title, request.Title) ?? project.Title;
|
|
project.Author = Patch.Apply(project.Author, request.Author);
|
|
project.Genre = Patch.Apply(project.Genre, request.Genre);
|
|
project.Logline = Patch.Apply(project.Logline, request.Logline);
|
|
project.Synopsis = Patch.Apply(project.Synopsis, request.Synopsis);
|
|
project.Notes = Patch.Apply(project.Notes, request.Notes);
|
|
project.TargetWordCount = request.TargetWordCount ?? project.TargetWordCount;
|
|
project.Phase = request.Phase ?? project.Phase;
|
|
project.UpdatedAt = DateTimeOffset.UtcNow;
|
|
|
|
await db.SaveChangesAsync(ct);
|
|
return project;
|
|
}
|
|
|
|
public async Task<bool> DeleteAsync(Guid id, CancellationToken ct = default)
|
|
{
|
|
Guard.Default(id, nameof(id));
|
|
|
|
logger.LogInformation("Deleting project {ProjectId}", id);
|
|
|
|
var project = await FindAsync(id, ct);
|
|
if (project is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
db.Projects.Remove(project);
|
|
await db.SaveChangesAsync(ct);
|
|
return true;
|
|
}
|
|
|
|
private async Task<Project?> FindAsync(Guid id, CancellationToken ct)
|
|
{
|
|
logger.LogDebug("Finding project {ProjectId}", id);
|
|
|
|
var project = await db.Projects.FirstOrDefaultAsync(p => p.Id == id, ct);
|
|
if (project is null)
|
|
{
|
|
logger.LogInformation("Project {ProjectId} not found", id);
|
|
}
|
|
else
|
|
{
|
|
logger.LogDebug("Found project {ProjectId}", id);
|
|
}
|
|
|
|
return project;
|
|
}
|
|
}
|