Files
Nexus.Reader/src/NexusReader.Application/Features/Books/Commands/PublishBookVersionCommandHandler.cs
T
mjasin 8856fb1614 feat(creator): Refactor Creator flow, implement book creation pipeline & versioning, and setup Docker staging
- Relocate dashboard routing to /creator and editor workspace to /creator/edit/{BookId}
- Implement CreateBookCommand and handler with transactional default chapter seeding
- Implement PublishBookVersionCommand and GetCreatorDashboardDataQuery
- Build CreatorDashboard modal and UI components with customized dark input styles
- Add run-stage.sh script to automate staging environment setup, database migrations, and health checks
- Update developer workflow rules in GEMINI.md
2026-06-14 10:58:37 +02:00

113 lines
4.4 KiB
C#

using System;
using System.Threading;
using System.Threading.Tasks;
using FluentResults;
using MediatR;
using Microsoft.EntityFrameworkCore;
using NexusReader.Application.Abstractions.Messaging;
using NexusReader.Data.Persistence;
using NexusReader.Domain.Entities;
using NexusReader.Domain.Exceptions;
namespace NexusReader.Application.Features.Books.Commands;
/// <summary>
/// MediatR handler for publishing a Book version and setting up the next Working Draft.
/// </summary>
public class PublishBookVersionCommandHandler : ICommandHandler<PublishBookVersionCommand>
{
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
public PublishBookVersionCommandHandler(IDbContextFactory<AppDbContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async Task<Result> Handle(PublishBookVersionCommand request, CancellationToken cancellationToken)
{
using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
// Fetch the Book including its CurrentDraftRevision and all associated Chapters,
// enforcing that the book belongs to the requested TenantId and UserId to prevent cross-tenant data leaks.
var book = await dbContext.Books
.Include(b => b.CurrentDraftRevision)
.ThenInclude(r => r!.Chapters)
.FirstOrDefaultAsync(
b => b.Id == request.BookId && b.UserId == request.UserId && b.TenantId == request.TenantId,
cancellationToken);
if (book == null)
{
throw new BookNotFoundException(request.BookId);
}
var oldDraftRevision = book.CurrentDraftRevision;
if (oldDraftRevision == null)
{
return Result.Fail(new Error("The book does not have an active draft revision to publish."));
}
// Start ACID transaction
using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
try
{
// 1. Update the current draft revision: Set IsPublished = true, PublishedAt = now, VersionString = custom
oldDraftRevision.IsPublished = true;
oldDraftRevision.PublishedAt = DateTime.UtcNow;
oldDraftRevision.VersionString = request.CustomVersionString;
// 2. Point the Book.LivePublishedRevisionId to this newly frozen revision ID
book.LivePublishedRevisionId = oldDraftRevision.Id;
// 3. Execute Deep Snapshot: Instantiate a brand new BookRevision representing the next "Working Draft"
var newDraftRevision = new BookRevision
{
Id = Guid.NewGuid(),
BookId = book.Id,
VersionString = "Working Draft",
IsPublished = false,
CreatedAt = DateTime.UtcNow
};
dbContext.BookRevisions.Add(newDraftRevision);
// Replicate/clone chapters into new Chapter objects associated with the new draft revision.
// Reset identities by explicitly instantiating completely new Chapter objects with Guid.NewGuid().
foreach (var oldChapter in oldDraftRevision.Chapters)
{
var newChapter = new Chapter
{
Id = Guid.NewGuid(),
BookRevisionId = newDraftRevision.Id,
Title = oldChapter.Title,
MarkdownContent = oldChapter.MarkdownContent,
SortOrder = oldChapter.SortOrder
};
dbContext.Chapters.Add(newChapter);
}
// 4. Assign the new draft revision ID to Book.CurrentDraftRevisionId
book.CurrentDraftRevisionId = newDraftRevision.Id;
// Save changes and commit transaction
await dbContext.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
return Result.Ok();
}
catch (Exception ex)
{
try
{
await transaction.RollbackAsync(cancellationToken);
}
catch (Exception rollbackEx)
{
Console.WriteLine($"[PublishBookVersion] Transaction rollback failed: {rollbackEx.Message}");
}
return Result.Fail(new Error($"Failed to publish book version: {ex.Message}").CausedBy(ex));
}
}
}