feat: Ingestion Pipeline Stabilization and WASM Service Proxies #42
@@ -193,6 +193,9 @@ namespace NexusReader.Data.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsReadyForReading")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("LastChapter")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
+703
@@ -0,0 +1,703 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NexusReader.Data.Persistence;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Pgvector;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NexusReader.Data.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260513181743_AddEbookReadyFlag")]
|
||||
partial class AddEbookReadyFlag
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.Author", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Authors");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.Ebook", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("AddedDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("AuthorId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("CoverUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("FilePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsReadyForReading")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("LastChapter")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<int>("LastChapterIndex")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("LastReadDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<double>("Progress")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Ebooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnit", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("MetadataJson")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Vector>("Vector")
|
||||
.HasColumnType("vector(768)");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SourceId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.ToTable("KnowledgeUnits");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnitLink", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("RelationType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("SourceUnitId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("TargetUnitId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SourceUnitId");
|
||||
|
||||
b.HasIndex("TargetUnitId");
|
||||
|
||||
b.ToTable("KnowledgeUnitLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.NexusUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("AITokenLimit")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("AITokensUsed")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime?>("LastAiActionDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("LastReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("LastReadPageId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("SubscriptionPlanId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(1);
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.HasIndex("SubscriptionPlanId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.QuizResult", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CompletedDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Score")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Topic")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("TotalQuestions")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("QuizResults");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.SemanticKnowledgeCache", b =>
|
||||
{
|
||||
b.Property<string>("ContentHash")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("JsonData")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ModelId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("OriginalText")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PromptVersion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<string>("TenantId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Vector>("Vector")
|
||||
.HasColumnType("vector(1536)");
|
||||
|
||||
b.HasKey("ContentHash");
|
||||
|
||||
b.HasIndex("ContentHash")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.ToTable("SemanticKnowledgeCache");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.SubscriptionPlan", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AITokenLimit")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("IsUnlimitedTokens")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<decimal>("MonthlyPrice")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("PlanName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("StripeProductId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PlanName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SubscriptionPlans");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1,
|
||||
AITokenLimit = 5000,
|
||||
IsUnlimitedTokens = false,
|
||||
MonthlyPrice = 0m,
|
||||
PlanName = "Free",
|
||||
StripeProductId = "prod_Free789"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2,
|
||||
AITokenLimit = 10000,
|
||||
IsUnlimitedTokens = false,
|
||||
MonthlyPrice = 9.99m,
|
||||
PlanName = "Basic",
|
||||
StripeProductId = "prod_basic_placeholder"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3,
|
||||
AITokenLimit = 50000,
|
||||
IsUnlimitedTokens = false,
|
||||
MonthlyPrice = 19.99m,
|
||||
PlanName = "Pro",
|
||||
StripeProductId = "prod_pro_placeholder"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4,
|
||||
AITokenLimit = 1000000000,
|
||||
IsUnlimitedTokens = true,
|
||||
MonthlyPrice = 99.99m,
|
||||
PlanName = "Enterprise",
|
||||
StripeProductId = "prod_enterprise_placeholder"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("NexusReader.Domain.Entities.NexusUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("NexusReader.Domain.Entities.NexusUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("NexusReader.Domain.Entities.NexusUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("NexusReader.Domain.Entities.NexusUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.Ebook", b =>
|
||||
{
|
||||
b.HasOne("NexusReader.Domain.Entities.Author", "Author")
|
||||
.WithMany("Ebooks")
|
||||
.HasForeignKey("AuthorId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("NexusReader.Domain.Entities.NexusUser", "User")
|
||||
.WithMany("Ebooks")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Author");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnitLink", b =>
|
||||
{
|
||||
b.HasOne("NexusReader.Domain.Entities.KnowledgeUnit", "SourceUnit")
|
||||
.WithMany("OutgoingLinks")
|
||||
.HasForeignKey("SourceUnitId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("NexusReader.Domain.Entities.KnowledgeUnit", "TargetUnit")
|
||||
.WithMany("IncomingLinks")
|
||||
.HasForeignKey("TargetUnitId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("SourceUnit");
|
||||
|
||||
b.Navigation("TargetUnit");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.NexusUser", b =>
|
||||
{
|
||||
b.HasOne("NexusReader.Domain.Entities.SubscriptionPlan", "SubscriptionPlan")
|
||||
.WithMany()
|
||||
.HasForeignKey("SubscriptionPlanId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("SubscriptionPlan");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.QuizResult", b =>
|
||||
{
|
||||
b.HasOne("NexusReader.Domain.Entities.NexusUser", "User")
|
||||
.WithMany("QuizResults")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.Author", b =>
|
||||
{
|
||||
b.Navigation("Ebooks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnit", b =>
|
||||
{
|
||||
b.Navigation("IncomingLinks");
|
||||
|
||||
b.Navigation("OutgoingLinks");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("NexusReader.Domain.Entities.NexusUser", b =>
|
||||
{
|
||||
b.Navigation("Ebooks");
|
||||
|
||||
b.Navigation("QuizResults");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace NexusReader.Data.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEbookReadyFlag : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsReadyForReading",
|
||||
table: "Ebooks",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsReadyForReading",
|
||||
table: "Ebooks");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,12 +45,12 @@ builder.Services.AddHttpClient("NexusAPI", client =>
|
||||
|
||||
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
|
||||
|
||||
// Real WASM implementations for application abstractions
|
||||
// Dummy registrations for server-only handlers to satisfy DI validation in WASM
|
||||
builder.Services.AddSingleton<IDbContextFactory<AppDbContext>>(new ThrowingDbContextFactory());
|
||||
builder.Services.AddScoped<IEmbeddingGenerator<string, Embedding<float>>, WasmEmbeddingGenerator>();
|
||||
builder.Services.AddScoped<IBookStorageService, WasmBookStorageService>();
|
||||
builder.Services.AddScoped<IEbookRepository, WasmEbookRepository>();
|
||||
builder.Services.AddScoped<ISyncBroadcaster, WasmSyncBroadcaster>();
|
||||
builder.Services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(new ThrowingEmbeddingGenerator());
|
||||
builder.Services.AddSingleton<IBookStorageService>(new ThrowingBookStorageService());
|
||||
builder.Services.AddSingleton<IEbookRepository>(new ThrowingEbookRepository());
|
||||
|
mjasin marked this conversation as resolved
Outdated
|
||||
builder.Services.AddSingleton<ISyncBroadcaster>(new ThrowingSyncBroadcaster());
|
||||
|
||||
builder.Services.AddApplication();
|
||||
builder.Services.AddScoped<IEpubReader, WasmEpubReader>();
|
||||
@@ -60,5 +60,42 @@ await builder.Build().RunAsync();
|
||||
|
||||
public class ThrowingDbContextFactory : IDbContextFactory<AppDbContext>
|
||||
{
|
||||
public AppDbContext CreateDbContext() => throw new NotSupportedException("DbContext cannot be used in WASM client. Use API proxies for data access.");
|
||||
public AppDbContext CreateDbContext() => throw new NotSupportedException("DbContext cannot be used in WASM client.");
|
||||
}
|
||||
|
||||
public class ThrowingEmbeddingGenerator : IEmbeddingGenerator<string, Embedding<float>>
|
||||
{
|
||||
public void Dispose() { }
|
||||
public Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(IEnumerable<string> values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("Embedding generation cannot be used in WASM client.");
|
||||
public object? GetService(Type serviceType, object? serviceKey = null) => null;
|
||||
}
|
||||
|
||||
public class ThrowingBookStorageService : IBookStorageService
|
||||
{
|
||||
private const string ErrorMessage = "File storage operations are not supported in the WASM client. Use the API endpoint for ingestion.";
|
||||
|
||||
public Task<string> SaveEbookAsync(byte[] data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
||||
public Task<string> SaveEbookAsync(Stream data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
||||
public Task<string?> SaveCoverAsync(byte[] data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
||||
public Task<string?> SaveCoverAsync(Stream data, string fileName) => throw new NotSupportedException(ErrorMessage);
|
||||
}
|
||||
|
||||
public class ThrowingEbookRepository : IEbookRepository
|
||||
{
|
||||
private const string ErrorMessage = "Ebook repository operations are not supported in the WASM client. Use the API endpoint for data access.";
|
||||
|
||||
public Task<Author?> FindAuthorByNameAsync(string name, CancellationToken cancellationToken = default) => throw new NotSupportedException(ErrorMessage);
|
||||
public void AddAuthor(Author author) => throw new NotSupportedException(ErrorMessage);
|
||||
public void AddEbook(Ebook ebook) => throw new NotSupportedException(ErrorMessage);
|
||||
public Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) => throw new NotSupportedException(ErrorMessage);
|
||||
}
|
||||
|
||||
public class ThrowingSyncBroadcaster : ISyncBroadcaster
|
||||
{
|
||||
public Task BroadcastProgressAsync(string userId, string pageId, DateTime timestamp, string? excludedConnectionId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("Real-time broadcasting can only be performed by the server.");
|
||||
|
||||
public Task BroadcastIngestionProgressAsync(string userId, string message, double progress, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("Real-time broadcasting can only be performed by the server.");
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
using System.Net.Http.Json;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
|
||||
namespace NexusReader.Web.Client.Services;
|
||||
|
||||
public class WasmBookStorageService : IBookStorageService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public WasmBookStorageService(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<string> SaveEbookAsync(byte[] data, string fileName)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/storage/save/ebook", new { data, fileName });
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<StorageResponse>();
|
||||
return result?.Path ?? string.Empty;
|
||||
}
|
||||
|
||||
public async Task<string> SaveEbookAsync(Stream data, string fileName)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await data.CopyToAsync(ms);
|
||||
return await SaveEbookAsync(ms.ToArray(), fileName);
|
||||
}
|
||||
|
||||
public async Task<string?> SaveCoverAsync(byte[] data, string fileName)
|
||||
{
|
||||
if (data == null || data.Length == 0) return null;
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/storage/save/cover", new { data, fileName });
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<StorageResponse>();
|
||||
return result?.Path;
|
||||
}
|
||||
|
||||
public async Task<string?> SaveCoverAsync(Stream data, string fileName)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await data.CopyToAsync(ms);
|
||||
return await SaveCoverAsync(ms.ToArray(), fileName);
|
||||
}
|
||||
|
||||
private record StorageResponse(string Path);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
using System.Net.Http.Json;
|
||||
using NexusReader.Application.Abstractions.Persistence;
|
||||
using NexusReader.Domain.Entities;
|
||||
|
||||
namespace NexusReader.Web.Client.Services;
|
||||
|
||||
public class WasmEbookRepository : IEbookRepository
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public WasmEbookRepository(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<Author?> FindAuthorByNameAsync(string name, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/repository/author/find", new { name }, cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return await response.Content.ReadFromJsonAsync<Author>(cancellationToken: cancellationToken);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void AddAuthor(Author author)
|
||||
{
|
||||
// For a repository in WASM, we can't easily do 'void' fire-and-forget Add without a local state.
|
||||
// However, we can either queue it or just do nothing if the caller expects SaveChangesAsync to handle it.
|
||||
// But the common pattern for this app seems to be calling the API.
|
||||
// For now, we'll assume the entity will be sent during SaveChanges or a separate command.
|
||||
// Given the constraints, we'll mark it for later serialization or just throw if not supported.
|
||||
// Better yet: we'll implement a 'Real' enough version that tracks changes locally.
|
||||
_stagedAuthors.Add(author);
|
||||
}
|
||||
|
||||
public void AddEbook(Ebook ebook)
|
||||
{
|
||||
_stagedEbooks.Add(ebook);
|
||||
}
|
||||
|
||||
private readonly List<Author> _stagedAuthors = new();
|
||||
private readonly List<Ebook> _stagedEbooks = new();
|
||||
|
||||
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var author in _stagedAuthors)
|
||||
{
|
||||
await _httpClient.PostAsJsonAsync("/api/repository/author/add", author, cancellationToken);
|
||||
count++;
|
||||
}
|
||||
foreach (var ebook in _stagedEbooks)
|
||||
{
|
||||
await _httpClient.PostAsJsonAsync("/api/repository/ebook/add", ebook, cancellationToken);
|
||||
count++;
|
||||
}
|
||||
|
||||
_stagedAuthors.Clear();
|
||||
_stagedEbooks.Clear();
|
||||
|
||||
await _httpClient.PostAsync("/api/repository/save", null, cancellationToken);
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace NexusReader.Web.Client.Services;
|
||||
|
||||
public class WasmEmbeddingGenerator : IEmbeddingGenerator<string, Embedding<float>>
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public WasmEmbeddingGenerator(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public async Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(
|
||||
IEnumerable<string> values,
|
||||
EmbeddingGenerationOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/ai/embeddings", new { values, options }, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<GeneratedEmbeddings<Embedding<float>>>(cancellationToken: cancellationToken);
|
||||
return result ?? new GeneratedEmbeddings<Embedding<float>>();
|
||||
}
|
||||
|
||||
public object? GetService(Type serviceType, object? serviceKey = null)
|
||||
{
|
||||
if (serviceType == typeof(IEmbeddingGenerator<string, Embedding<float>>)) return this;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
using System.Net.Http.Json;
|
||||
using NexusReader.Application.Abstractions.Messaging;
|
||||
|
||||
namespace NexusReader.Web.Client.Services;
|
||||
|
||||
public class WasmSyncBroadcaster : ISyncBroadcaster
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public WasmSyncBroadcaster(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task BroadcastProgressAsync(
|
||||
string userId,
|
||||
string pageId,
|
||||
DateTime timestamp,
|
||||
string? excludedConnectionId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _httpClient.PostAsJsonAsync("/api/broadcaster/progress", new
|
||||
{
|
||||
userId,
|
||||
pageId,
|
||||
timestamp,
|
||||
excludedConnectionId
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task BroadcastIngestionProgressAsync(
|
||||
string userId,
|
||||
string message,
|
||||
double progress,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _httpClient.PostAsJsonAsync("/api/broadcaster/ingestion-progress", new
|
||||
{
|
||||
userId,
|
||||
message,
|
||||
progress
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -253,13 +253,6 @@ app.MapGet("/api/epub/{ebookId:guid}/{index:int}", async (Guid ebookId, int inde
|
||||
return Results.BadRequest(errorMsg);
|
||||
}).RequireAuthorization();
|
||||
|
||||
// Proxy API for AI services (Embeddings)
|
||||
app.MapPost("/api/ai/embeddings", async (EmbeddingsRequest request, IEmbeddingGenerator<string, Embedding<float>> generator) =>
|
||||
{
|
||||
var result = await generator.GenerateAsync(request.Values, request.Options);
|
||||
return Results.Ok(result);
|
||||
}).RequireAuthorization();
|
||||
|
||||
var knowledgeApi = app.MapGroup("/api/knowledge").RequireAuthorization("HasAvailableTokens");
|
||||
|
||||
knowledgeApi.MapPost("/", async (KnowledgeRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
||||
@@ -303,63 +296,6 @@ knowledgeApi.MapDelete("/", async (IKnowledgeService knowledgeService) =>
|
||||
return Results.BadRequest(errorMsg);
|
||||
});
|
||||
|
||||
// Proxy API for WASM Repository calls
|
||||
var repoApi = app.MapGroup("/api/repository").RequireAuthorization();
|
||||
|
||||
repoApi.MapPost("/author/find", async (AuthorFindRequest request, IEbookRepository repo) =>
|
||||
{
|
||||
var author = await repo.FindAuthorByNameAsync(request.Name);
|
||||
return author != null ? Results.Ok(author) : Results.NotFound();
|
||||
});
|
||||
|
||||
repoApi.MapPost("/author/add", (Author author, IEbookRepository repo) =>
|
||||
{
|
||||
repo.AddAuthor(author);
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
repoApi.MapPost("/ebook/add", (Ebook ebook, IEbookRepository repo) =>
|
||||
{
|
||||
repo.AddEbook(ebook);
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
repoApi.MapPost("/save", async (IEbookRepository repo) =>
|
||||
{
|
||||
await repo.SaveChangesAsync();
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
// Proxy API for WASM Broadcaster calls
|
||||
var broadcasterApi = app.MapGroup("/api/broadcaster").RequireAuthorization();
|
||||
|
||||
broadcasterApi.MapPost("/progress", async (BroadcastProgressRequest request, ISyncBroadcaster broadcaster) =>
|
||||
{
|
||||
await broadcaster.BroadcastProgressAsync(request.UserId, request.PageId, request.Timestamp, request.ExcludedConnectionId);
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
broadcasterApi.MapPost("/ingestion-progress", async (BroadcastIngestionProgressRequest request, ISyncBroadcaster broadcaster) =>
|
||||
{
|
||||
await broadcaster.BroadcastIngestionProgressAsync(request.UserId, request.Message, request.Progress);
|
||||
return Results.Ok();
|
||||
});
|
||||
|
||||
// Proxy API for WASM Storage calls
|
||||
var storageApi = app.MapGroup("/api/storage").RequireAuthorization();
|
||||
|
||||
storageApi.MapPost("/save/ebook", async (StorageRequest request, IBookStorageService storage) =>
|
||||
{
|
||||
var path = await storage.SaveEbookAsync(request.Data, request.FileName);
|
||||
return Results.Ok(new { Path = path });
|
||||
});
|
||||
|
||||
storageApi.MapPost("/save/cover", async (StorageRequest request, IBookStorageService storage) =>
|
||||
{
|
||||
var path = await storage.SaveCoverAsync(request.Data, request.FileName);
|
||||
return Results.Ok(new { Path = path });
|
||||
});
|
||||
|
||||
app.MapPost("/api/library/ingest", async ([FromBody] IngestEbookRequest request, ClaimsPrincipal user, IMediator mediator) =>
|
||||
{
|
||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
@@ -585,8 +521,3 @@ app.Run();
|
||||
|
||||
public record KnowledgeRequest(string Text);
|
||||
public record GroundednessRequest(string Answer, string Context);
|
||||
public record AuthorFindRequest(string Name);
|
||||
public record BroadcastProgressRequest(string UserId, string PageId, DateTime Timestamp, string? ExcludedConnectionId);
|
||||
public record BroadcastIngestionProgressRequest(string UserId, string Message, double Progress);
|
||||
public record StorageRequest(byte[] Data, string FileName);
|
||||
public record EmbeddingsRequest(IEnumerable<string> Values, EmbeddingGenerationOptions? Options);
|
||||
|
||||
Reference in New Issue
Block a user
Architectural Violation (Clean Architecture / CQRS): Building 'Wasm' proxies for Infrastructure interfaces (
IEbookRepository,ISyncBroadcaster, etc.) in order to satisfy DI for MediatR handlers executing locally on the WASM client is an anti-pattern. MediatR handlers that depend on server-side infrastructure must not be executed directly from client environments. The client should send an HTTP API request (or SignalR message), and the MediatR handler should be executed exclusively on the server.