using System.Linq.Expressions; using System.Text.Json; using System.Text.Json.Serialization; using DysonNetwork.Shared.Data; using DysonNetwork.Shared.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using NodaTime; using Quartz; namespace DysonNetwork.Wallet; public class AppDatabase( DbContextOptions options, IConfiguration configuration ) : DbContext(options) { public DbSet Wallets { get; set; } = null!; public DbSet WalletPockets { get; set; } = null!; public DbSet PaymentOrders { get; set; } = null!; public DbSet PaymentTransactions { get; set; } = null!; public DbSet WalletFunds { get; set; } = null!; public DbSet WalletFundRecipients { get; set; } = null!; public DbSet WalletSubscriptions { get; set; } = null!; public DbSet WalletGifts { get; set; } = null!; public DbSet WalletCoupons { get; set; } = null!; public DbSet Lotteries { get; set; } = null!; public DbSet LotteryRecords { get; set; } = null!; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql( configuration.GetConnectionString("App"), opt => opt .ConfigureDataSource(optSource => optSource .EnableDynamicJson() .ConfigureJsonOptions(new JsonSerializerOptions() { NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, }) ) .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) .UseNodaTime() ).UseSnakeCaseNamingConvention(); base.OnConfiguring(optionsBuilder); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity() .HasKey(pg => new { pg.GroupId, pg.Actor }); modelBuilder.Entity() .HasOne(pg => pg.Group) .WithMany(g => g.Members) .HasForeignKey(pg => pg.GroupId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.Entity() .HasKey(r => new { FromAccountId = r.AccountId, ToAccountId = r.RelatedId }); modelBuilder.Entity() .HasOne(r => r.Account) .WithMany(a => a.OutgoingRelationships) .HasForeignKey(r => r.AccountId); modelBuilder.Entity() .HasOne(r => r.Related) .WithMany(a => a.IncomingRelationships) .HasForeignKey(r => r.RelatedId); modelBuilder.Entity() .HasKey(pm => new { pm.RealmId, pm.AccountId }); modelBuilder.Entity() .HasOne(pm => pm.Realm) .WithMany(p => p.Members) .HasForeignKey(pm => pm.RealmId) .OnDelete(DeleteBehavior.Cascade); modelBuilder.ApplySoftDeleteFilters(); } public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { this.ApplyAuditableAndSoftDelete(); return await base.SaveChangesAsync(cancellationToken); } } public class AppDatabaseRecyclingJob(AppDatabase db, ILogger logger) : IJob { public async Task Execute(IJobExecutionContext context) { var now = SystemClock.Instance.GetCurrentInstant(); logger.LogInformation("Deleting soft-deleted records..."); var threshold = now - Duration.FromDays(7); var entityTypes = db.Model.GetEntityTypes() .Where(t => typeof(ModelBase).IsAssignableFrom(t.ClrType) && t.ClrType != typeof(ModelBase)) .Select(t => t.ClrType); foreach (var entityType in entityTypes) { var set = (IQueryable)db.GetType().GetMethod(nameof(DbContext.Set), Type.EmptyTypes)! .MakeGenericMethod(entityType).Invoke(db, null)!; var parameter = Expression.Parameter(entityType, "e"); var property = Expression.Property(parameter, nameof(ModelBase.DeletedAt)); var condition = Expression.LessThan(property, Expression.Constant(threshold, typeof(Instant?))); var notNull = Expression.NotEqual(property, Expression.Constant(null, typeof(Instant?))); var finalCondition = Expression.AndAlso(notNull, condition); var lambda = Expression.Lambda(finalCondition, parameter); var queryable = set.Provider.CreateQuery( Expression.Call( typeof(Queryable), "Where", [entityType], set.Expression, Expression.Quote(lambda) ) ); var toListAsync = typeof(EntityFrameworkQueryableExtensions) .GetMethod(nameof(EntityFrameworkQueryableExtensions.ToListAsync))! .MakeGenericMethod(entityType); var items = await (dynamic)toListAsync.Invoke(null, [queryable, CancellationToken.None])!; db.RemoveRange(items); } await db.SaveChangesAsync(); } } public class AppDatabaseFactory : IDesignTimeDbContextFactory { public AppDatabase CreateDbContext(string[] args) { var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); var optionsBuilder = new DbContextOptionsBuilder(); return new AppDatabase(optionsBuilder.Options, configuration); } }