feat: Ingestion Pipeline Stabilization and WASM Service Proxies (#42)
This PR stabilizes the Nexus Ingestion Engine by implementing functional service proxies for the Blazor WASM client and refining the backend infrastructure for real-time progress tracking and database compatibility. ### Key Changes - **Infrastructure Stabilization**: - Implemented production-grade `EbookRepository` with PostgreSQL `EF.Functions.ILike` support. - Enforced `IsReadyForReading = false` state for newly added ebooks (resolves #35). - Updated `SignalRSyncBroadcaster` to support targeted user messaging and ingestion-specific progress updates (resolves #37). - **WASM Client Functional Proxies**: - Replaced "Throwing" dummy services with `WasmEbookRepository`, `WasmSyncBroadcaster`, `WasmBookStorageService`, and `WasmEmbeddingGenerator`. - These services proxy requests to the backend via a new set of Minimal API endpoints in `NexusReader.Web`. - **Domain Refinement**: - Added `IsReadyForReading` flag to the `Ebook` entity to manage background AI processing states. ### Related Issues - Fixes #35 - Fixes #36 - Fixes #37 --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #42 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #42.
This commit is contained in:
@@ -1,30 +1,27 @@
|
||||
using FluentResults;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NexusReader.Application.Abstractions.Messaging;
|
||||
using NexusReader.Application.Abstractions.Persistence;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
using NexusReader.Data.Persistence;
|
||||
using NexusReader.Domain.Entities;
|
||||
|
||||
namespace NexusReader.Application.Commands.Library;
|
||||
|
||||
public class IngestEbookCommandHandler : IRequestHandler<IngestEbookCommand, Result<Guid>>
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private readonly IEbookRepository _ebookRepository;
|
||||
private readonly IBookStorageService _storageService;
|
||||
|
||||
public IngestEbookCommandHandler(
|
||||
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||
IEbookRepository ebookRepository,
|
||||
IBookStorageService storageService)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_ebookRepository = ebookRepository;
|
||||
_storageService = storageService;
|
||||
}
|
||||
|
||||
public async Task<Result<Guid>> Handle(IngestEbookCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
string epubPath;
|
||||
string? coverUrl;
|
||||
|
||||
@@ -36,6 +33,10 @@ public class IngestEbookCommandHandler : IRequestHandler<IngestEbookCommand, Res
|
||||
? await _storageService.SaveCoverAsync(request.CoverImage, $"{request.Title}_cover.jpg")
|
||||
: null;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
return Result.Fail(new Error($"Storage I/O failure: {ex.Message}").CausedBy(ex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error($"Storage failure: {ex.Message}").CausedBy(ex));
|
||||
@@ -43,17 +44,16 @@ public class IngestEbookCommandHandler : IRequestHandler<IngestEbookCommand, Res
|
||||
|
||||
try
|
||||
{
|
||||
// 2. Resolve Author
|
||||
var authorName = string.IsNullOrWhiteSpace(request.AuthorName) ? "Unknown Author" : request.AuthorName.Trim();
|
||||
|
||||
// Use case-insensitive comparison
|
||||
var author = await context.Authors
|
||||
.FirstOrDefaultAsync(a => a.Name.ToLower() == authorName.ToLower(), cancellationToken);
|
||||
// 2. Resolve Author (case-insensitive via repository)
|
||||
var authorName = string.IsNullOrWhiteSpace(request.AuthorName)
|
||||
? "Unknown Author"
|
||||
: request.AuthorName.Trim();
|
||||
|
||||
var author = await _ebookRepository.FindAuthorByNameAsync(authorName, cancellationToken);
|
||||
if (author == null)
|
||||
{
|
||||
author = new Author { Name = authorName };
|
||||
context.Authors.Add(author);
|
||||
_ebookRepository.AddAuthor(author);
|
||||
}
|
||||
|
||||
// 3. Create Ebook
|
||||
@@ -61,25 +61,21 @@ public class IngestEbookCommandHandler : IRequestHandler<IngestEbookCommand, Res
|
||||
{
|
||||
Title = request.Title,
|
||||
Author = author,
|
||||
FilePath = epubPath, // Relative URL from wwwroot
|
||||
FilePath = epubPath,
|
||||
CoverUrl = coverUrl,
|
||||
UserId = request.UserId,
|
||||
TenantId = request.TenantId,
|
||||
AddedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
context.Ebooks.Add(ebook);
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
_ebookRepository.AddEbook(ebook);
|
||||
await _ebookRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(ebook.Id);
|
||||
}
|
||||
catch (DbUpdateException ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error($"Database error during ingestion: {ex.Message}").CausedBy(ex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error($"Unexpected error during ingestion: {ex.Message}").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user