using System.Diagnostics.CodeAnalysis; using EntityFramework.Exceptions.PostgreSQL; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Diagnostics; using Npgsql; namespace Foxnouns.Backend.Database; public class DatabaseContext(DbContextOptions options) : DbContext(options) { private static string GenerateConnectionString(Config.DatabaseConfig config) { return new NpgsqlConnectionStringBuilder(config.Url) { Pooling = config.EnablePooling ?? true, Timeout = config.Timeout ?? 5, MaxPoolSize = config.MaxPoolSize ?? 50, MinPoolSize = 0, ConnectionPruningInterval = 10, ConnectionIdleLifetime = 10, }.ConnectionString; } public static NpgsqlDataSource BuildDataSource(Config config) { var dataSourceBuilder = new NpgsqlDataSourceBuilder( GenerateConnectionString(config.Database) ); dataSourceBuilder.UseNodaTime(); dataSourceBuilder.UseJsonNet(); return dataSourceBuilder.Build(); } public static DbContextOptionsBuilder BuildOptions( DbContextOptionsBuilder options, NpgsqlDataSource dataSource, ILoggerFactory? loggerFactory ) => options .ConfigureWarnings(c => c.Ignore(CoreEventId.SaveChangesFailed)) .UseNpgsql(dataSource, o => o.UseNodaTime()) .UseLoggerFactory(loggerFactory) .UseSnakeCaseNamingConvention() .UseExceptionProcessor(); public DbSet Users { get; init; } public DbSet Members { get; init; } public DbSet AuthMethods { get; init; } public DbSet FediverseApplications { get; init; } public DbSet Tokens { get; init; } public DbSet Applications { get; init; } public DbSet TemporaryKeys { get; init; } public DbSet DataExports { get; init; } public DbSet PrideFlags { get; init; } public DbSet UserFlags { get; init; } public DbSet MemberFlags { get; init; } protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { // Snowflakes are stored as longs configurationBuilder.Properties().HaveConversion(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().HasIndex(u => u.Username).IsUnique(); modelBuilder.Entity().HasIndex(u => u.Sid).IsUnique(); modelBuilder.Entity().HasIndex(m => new { m.UserId, m.Name }).IsUnique(); modelBuilder.Entity().HasIndex(m => m.Sid).IsUnique(); modelBuilder.Entity().HasIndex(k => k.Key).IsUnique(); modelBuilder .Entity() .HasIndex(m => new { m.AuthType, m.RemoteId, m.FediverseApplicationId, }) .HasFilter("fediverse_application_id IS NOT NULL") .IsUnique(); modelBuilder.Entity().HasIndex(d => d.Filename).IsUnique(); modelBuilder .Entity() .HasIndex(m => new { m.AuthType, m.RemoteId }) .HasFilter("fediverse_application_id IS NULL") .IsUnique(); modelBuilder.Entity().Property(u => u.Sid).HasDefaultValueSql("find_free_user_sid()"); modelBuilder.Entity().Property(u => u.Fields).HasColumnType("jsonb"); modelBuilder.Entity().Property(u => u.Names).HasColumnType("jsonb"); modelBuilder.Entity().Property(u => u.Pronouns).HasColumnType("jsonb"); modelBuilder.Entity().Property(u => u.CustomPreferences).HasColumnType("jsonb"); modelBuilder.Entity().Property(u => u.Settings).HasColumnType("jsonb"); modelBuilder .Entity() .Property(m => m.Sid) .HasDefaultValueSql("find_free_member_sid()"); modelBuilder.Entity().Property(m => m.Fields).HasColumnType("jsonb"); modelBuilder.Entity().Property(m => m.Names).HasColumnType("jsonb"); modelBuilder.Entity().Property(m => m.Pronouns).HasColumnType("jsonb"); modelBuilder.Entity().Navigation(f => f.PrideFlag).AutoInclude(); modelBuilder.Entity().Navigation(f => f.PrideFlag).AutoInclude(); modelBuilder .HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeUserSid))!) .HasName("find_free_user_sid"); modelBuilder .HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeMemberSid))!) .HasName("find_free_member_sid"); } /// /// Dummy method that calls find_free_user_sid() when used in an EF Core query. /// public string FindFreeUserSid() => throw new NotSupportedException(); /// /// Dummy method that calls find_free_member_sid() when used in an EF Core query. /// public string FindFreeMemberSid() => throw new NotSupportedException(); } [SuppressMessage( "ReSharper", "UnusedType.Global", Justification = "Used by EF Core's migration generator" )] public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory { public DatabaseContext CreateDbContext(string[] args) { // Read the configuration file var config = new ConfigurationBuilder() .AddConfiguration() .Build() // Get the configuration as our config class .Get() ?? new(); var dataSource = DatabaseContext.BuildDataSource(config); var options = DatabaseContext .BuildOptions(new DbContextOptionsBuilder(), dataSource, null) .Options; return new DatabaseContext(options); } }