// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . 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) => 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; } = null!; public DbSet Members { get; init; } = null!; public DbSet AuthMethods { get; init; } = null!; public DbSet FediverseApplications { get; init; } = null!; public DbSet Tokens { get; init; } = null!; public DbSet Applications { get; init; } = null!; public DbSet TemporaryKeys { get; init; } = null!; public DbSet DataExports { get; init; } = null!; public DbSet PrideFlags { get; init; } = null!; public DbSet UserFlags { get; init; } = null!; public DbSet MemberFlags { get; init; } = null!; 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(d => d.Filename).IsUnique(); // Two indexes on auth_methods, one for fediverse auth and one for all other types. modelBuilder .Entity() .HasIndex(m => new { m.AuthType, m.RemoteId, m.FediverseApplicationId, }) .HasFilter("fediverse_application_id IS NOT NULL") .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 Config config = new ConfigurationBuilder() .AddConfiguration() .Build() // Get the configuration as our config class .Get() ?? new Config(); NpgsqlDataSource dataSource = DatabaseContext.BuildDataSource(config); DbContextOptions options = DatabaseContext .BuildOptions(new DbContextOptionsBuilder(), dataSource, null) .Options; return new DatabaseContext(options); } }