chore: init project

This commit is contained in:
cesnimda
2026-06-30 15:53:32 +02:00
commit f43ef5f945
94 changed files with 4405 additions and 0 deletions
@@ -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);
}
}