chore: init project
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
using InboxIntel.Application.Abstractions;
|
||||
using InboxIntel.Domain.Common;
|
||||
using InboxIntel.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Reflection;
|
||||
|
||||
namespace InboxIntel.Infrastructure.Persistence;
|
||||
|
||||
public class AppDbContext : DbContext, IAppDbContext
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<Email> Emails => Set<Email>();
|
||||
public DbSet<MailThread> Threads => Set<MailThread>();
|
||||
public DbSet<Sender> Senders => Set<Sender>();
|
||||
public DbSet<MailDomain> Domains => Set<MailDomain>();
|
||||
public DbSet<Attachment> Attachments => Set<Attachment>();
|
||||
public DbSet<Label> Labels => Set<Label>();
|
||||
public DbSet<EmailLabel> EmailLabels => Set<EmailLabel>();
|
||||
public DbSet<SyncState> SyncStates => Set<SyncState>();
|
||||
public DbSet<AnalyticsAggregate> AnalyticsAggregates => Set<AnalyticsAggregate>();
|
||||
public DbSet<WidgetLayout> WidgetLayouts => Set<WidgetLayout>();
|
||||
public DbSet<UnsubscribeItem> UnsubscribeItems => Set<UnsubscribeItem>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
|
||||
public override Task<int> SaveChangesAsync(CancellationToken ct = default)
|
||||
{
|
||||
foreach (var entry in ChangeTracker.Entries<AuditableEntity>())
|
||||
{
|
||||
if (entry.State == EntityState.Modified)
|
||||
entry.Entity.UpdatedAtUtc = DateTimeOffset.UtcNow;
|
||||
}
|
||||
return base.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace InboxIntel.Infrastructure.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Design-time factory so `dotnet ef migrations add ...` works without booting
|
||||
/// the full API host. Reads the connection string from the EF_CONNECTION env
|
||||
/// var, falling back to a local default.
|
||||
/// </summary>
|
||||
public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
|
||||
{
|
||||
public AppDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var conn = Environment.GetEnvironmentVariable("EF_CONNECTION")
|
||||
?? "Host=localhost;Port=5432;Database=inboxintel;Username=inboxintel;Password=inboxintel";
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseNpgsql(conn)
|
||||
.Options;
|
||||
return new AppDbContext(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using InboxIntel.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace InboxIntel.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class EmailConfiguration : IEntityTypeConfiguration<Email>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Email> b)
|
||||
{
|
||||
b.ToTable("emails");
|
||||
b.HasKey(e => e.Id);
|
||||
|
||||
b.Property(e => e.GmailMessageId).HasMaxLength(64).IsRequired();
|
||||
b.Property(e => e.Subject).HasMaxLength(1024);
|
||||
b.Property(e => e.Snippet).HasMaxLength(2048);
|
||||
b.Property(e => e.ListUnsubscribeRaw).HasMaxLength(2048);
|
||||
|
||||
// One Gmail message per user.
|
||||
b.HasIndex(e => new { e.UserId, e.GmailMessageId }).IsUnique();
|
||||
|
||||
// Indexes that power fast sender grouping, time-series, and inbox filters at 100k+ rows.
|
||||
b.HasIndex(e => new { e.UserId, e.SenderId });
|
||||
b.HasIndex(e => new { e.UserId, e.SentAtUtc });
|
||||
b.HasIndex(e => new { e.UserId, e.IsUnread });
|
||||
b.HasIndex(e => new { e.UserId, e.Category });
|
||||
b.HasIndex(e => new { e.UserId, e.IsInInbox });
|
||||
|
||||
b.HasOne(e => e.Thread)
|
||||
.WithMany(t => t.Emails)
|
||||
.HasForeignKey(e => e.ThreadId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne(e => e.Sender)
|
||||
.WithMany(s => s.Emails)
|
||||
.HasForeignKey(e => e.SenderId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// PostgreSQL full-text search: generated tsvector over subject + body,
|
||||
// with a GIN index. Maintained by the database, read-only in code.
|
||||
b.Property(e => e.SearchVector)
|
||||
.HasColumnType("tsvector")
|
||||
.HasComputedColumnSql(
|
||||
"to_tsvector('english', coalesce(\"Subject\",'') || ' ' || coalesce(\"BodyText\",''))",
|
||||
stored: true);
|
||||
b.HasIndex(e => e.SearchVector).HasMethod("GIN");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using InboxIntel.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace InboxIntel.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<User> b)
|
||||
{
|
||||
b.ToTable("users");
|
||||
b.HasKey(u => u.Id);
|
||||
b.Property(u => u.GoogleSubjectId).HasMaxLength(64).IsRequired();
|
||||
b.Property(u => u.Email).HasMaxLength(320).IsRequired();
|
||||
b.HasIndex(u => u.GoogleSubjectId).IsUnique();
|
||||
b.HasIndex(u => u.Email).IsUnique();
|
||||
// EncryptedRefreshToken is bytea; never indexed, never logged.
|
||||
}
|
||||
}
|
||||
|
||||
public class MailDomainConfiguration : IEntityTypeConfiguration<MailDomain>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<MailDomain> b)
|
||||
{
|
||||
b.ToTable("domains");
|
||||
b.HasKey(d => d.Id);
|
||||
b.Property(d => d.Name).HasMaxLength(255).IsRequired();
|
||||
b.HasIndex(d => new { d.UserId, d.Name }).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public class SenderConfiguration : IEntityTypeConfiguration<Sender>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Sender> b)
|
||||
{
|
||||
b.ToTable("senders");
|
||||
b.HasKey(s => s.Id);
|
||||
b.Property(s => s.Address).HasMaxLength(320).IsRequired();
|
||||
b.Property(s => s.DisplayName).HasMaxLength(255);
|
||||
b.HasIndex(s => new { s.UserId, s.Address }).IsUnique();
|
||||
b.HasIndex(s => new { s.UserId, s.EmailCount });
|
||||
b.HasOne(s => s.Domain).WithMany(d => d.Senders)
|
||||
.HasForeignKey(s => s.DomainId).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
public class MailThreadConfiguration : IEntityTypeConfiguration<MailThread>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<MailThread> b)
|
||||
{
|
||||
b.ToTable("threads");
|
||||
b.HasKey(t => t.Id);
|
||||
b.Property(t => t.GmailThreadId).HasMaxLength(64).IsRequired();
|
||||
b.Property(t => t.Subject).HasMaxLength(1024);
|
||||
b.HasIndex(t => new { t.UserId, t.GmailThreadId }).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public class AttachmentConfiguration : IEntityTypeConfiguration<Attachment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Attachment> b)
|
||||
{
|
||||
b.ToTable("attachments");
|
||||
b.HasKey(a => a.Id);
|
||||
b.Property(a => a.FileName).HasMaxLength(512);
|
||||
b.Property(a => a.MimeType).HasMaxLength(255);
|
||||
b.HasIndex(a => new { a.UserId, a.MimeType });
|
||||
b.HasOne(a => a.Email).WithMany(e => e.Attachments)
|
||||
.HasForeignKey(a => a.EmailId).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
public class LabelConfiguration : IEntityTypeConfiguration<Label>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Label> b)
|
||||
{
|
||||
b.ToTable("labels");
|
||||
b.HasKey(l => l.Id);
|
||||
b.Property(l => l.GmailLabelId).HasMaxLength(64).IsRequired();
|
||||
b.Property(l => l.Name).HasMaxLength(255).IsRequired();
|
||||
b.HasIndex(l => new { l.UserId, l.GmailLabelId }).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public class EmailLabelConfiguration : IEntityTypeConfiguration<EmailLabel>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<EmailLabel> b)
|
||||
{
|
||||
b.ToTable("email_labels");
|
||||
b.HasKey(el => new { el.EmailId, el.LabelId });
|
||||
b.HasOne(el => el.Email).WithMany(e => e.EmailLabels)
|
||||
.HasForeignKey(el => el.EmailId).OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(el => el.Label).WithMany(l => l.EmailLabels)
|
||||
.HasForeignKey(el => el.LabelId).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
public class SyncStateConfiguration : IEntityTypeConfiguration<SyncState>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SyncState> b)
|
||||
{
|
||||
b.ToTable("sync_states");
|
||||
b.HasKey(s => s.Id);
|
||||
b.HasIndex(s => s.UserId).IsUnique();
|
||||
b.Property(s => s.LastError).HasMaxLength(4000);
|
||||
}
|
||||
}
|
||||
|
||||
public class AnalyticsAggregateConfiguration : IEntityTypeConfiguration<AnalyticsAggregate>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AnalyticsAggregate> b)
|
||||
{
|
||||
b.ToTable("analytics_aggregates");
|
||||
b.HasKey(a => a.Id);
|
||||
b.HasIndex(a => new { a.UserId, a.Day }).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public class WidgetLayoutConfiguration : IEntityTypeConfiguration<WidgetLayout>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<WidgetLayout> b)
|
||||
{
|
||||
b.ToTable("widget_layouts");
|
||||
b.HasKey(w => w.Id);
|
||||
b.Property(w => w.WidgetKey).HasMaxLength(64).IsRequired();
|
||||
b.HasIndex(w => new { w.UserId, w.WidgetKey }).IsUnique();
|
||||
b.HasOne<User>().WithMany(u => u.WidgetLayouts)
|
||||
.HasForeignKey(w => w.UserId).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
|
||||
public class UnsubscribeItemConfiguration : IEntityTypeConfiguration<UnsubscribeItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<UnsubscribeItem> b)
|
||||
{
|
||||
b.ToTable("unsubscribe_items");
|
||||
b.HasKey(u => u.Id);
|
||||
b.Property(u => u.UnsubscribeTarget).HasMaxLength(2048);
|
||||
b.HasIndex(u => new { u.UserId, u.SenderId }).IsUnique();
|
||||
b.HasOne(u => u.Sender).WithMany()
|
||||
.HasForeignKey(u => u.SenderId).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user